Your Friendly iOS Coder

Dabby [ダビー]

Adjusting to Keyboard Presentations Modularly

When creating an app that requires information from a user, chances are you use one or more textfields. If we have multiple fields embedded in a scroll view, we want to ensure that the contents of the scroll view do not become inaccessible when the system keyboard is displayed for the user to fill out a field. In this post, we investigate how to handle keyboard presentations in a modular way.

Keyboard Handling the Regular way

To handle keyboard presentation, iOS provides a set of notifications:

  • UIResponder.keyboardWillShowNotification => right before keyboard presentation
  • UIResponder.keyboardDidShowNotification => right after keyboard presentation
  • UIResponder.keyboardWillHideNotification => right before keyboard dismissal
  • UIResponder.keyboardDidHideNotification => right after keyboard dismissal

Each of these notifications has information in the userInfo that gives us information about the system keyboard:

  • UIResponder.keyboardFrameBeginUserInfoKey => Start frame of the keyboard before it is presented
  • UIResponder.keyboardFrameEndUserInfoKey => Final frame of the keyboard after it has been presented
  • UIResponder.keyboardAnimationDurationUserInfoKey => Duration of the the keyboard animation

Let’s say we have a table view that has some textfields. When a textfield is active, and the keyboard is presented, we want to ensure that the user can still scroll to the bottom of the table view without the keyboard obstructing any of the content. The regular way to do this will be as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class TableViewController: UITableViewController {
    ...

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(keyboardDidHide(notification:)), name: UIResponder.keyboardDidHideNotification, object: nil)
    }

    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)

        NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardWillShowNotification, object: nil)
        NotificationCenter.default.removeObserver(self, name: UIResponder.keyboardDidHideNotification, object: nil)
    }

    ...

    @objc dynamic private func keyboardWillShow(notification: Notification) {
        guard let endFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }

        // adjust content inset to account for keyboard size
        tableView.contentInset = UIEdgeInsets(top: 0.0, left: 0.0, bottom: endFrame.size.height, right: 0.0)
    }

    @objc dynamic private func keyboardDidHide(notification: Notification) {
        // reset content inset back to zero
        tableView.contentInset = UIEdgeInsets(top: 0.0, left: 0.0, bottom: 0.0, right: 0.0)
    }
}

This works, but it isn’t very reusable. If we had another view controller that needed to adjust it’s content due to keyboard presentation, we will have to duplicate this logic all over again. The good news is that we can do better!

Keyboard handling as a behaviour

Composition is a way of extending the functionality of a class by delegating behaviour to one of it’s children. If we consider how a View Controller handles keyboard presentation as a behaviour, it enables us to implement different behaviours, and inject said behaviours into the View Controller based on what functionality we want. Here are some examples of possible behaviours:

  • Adjusting scroll view content inset to account for keyboard presentation.
  • changing the bottom anchor of the view to account for the keyboard presentation.
  • Offseting the frame of all views on the screen by the presented keyboard size.

All of the above are behaviours that a view controller can perform when a keyboard is presented. The goal is to be able to reuse any of these behaviours whenever we want. In order to achieve this, we will break down the problem of handling keyboard presentation into two parts:

  1. Observing when a keyboard is presented, and extracting the necessary information about the keyboard presentation
  2. Behaving in a particular way to account for keyboard presentation using the information obtained from (1)

Observing keyboard presentations modularly

We want to create an abstraction that listens to keyboard notifications, and informs observers about the event. Our abstraction will only be able to inform a single observer via closures, but this can be easily extended to informing multiple observers. Consider the code below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
class KeyboardObserver: NSObject {

    struct KeyboardDisplayInfo {
        var beginFrame: CGRect
        var endFrame: CGRect
        var animationDuration: CGFloat

        init?(notification: Notification) {
            guard let endFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect,
                let beginFrame = notification.userInfo?[UIResponder.keyboardFrameBeginUserInfoKey] as? CGRect,
                let duration = notification.userInfo?[UIResponder.keyboardAnimationDurationUserInfoKey] as? CGFloat else {
                    return nil
            }
            self.beginFrame = beginFrame
            self.endFrame = endFrame
            self.animationDuration = duration
        }
    }

    var willShowKeyboard: ((KeyboardDisplayInfo) -> Void)?
    var didShowKeyboard: ((KeyboardDisplayInfo) -> Void)?
    var willHideKeyboard: ((KeyboardDisplayInfo) -> Void)?
    var didHideKeyboard: ((KeyboardDisplayInfo) -> Void)?

    private let notificationCenter: NotificationCenter

    init(notificationCenter: NotificationCenter) {
        self.notificationCenter = notificationCenter
        super.init()

        notificationCenter.addObserver(self, selector: #selector(keyboardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil)
        notificationCenter.addObserver(self, selector: #selector(keyboardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil)
        notificationCenter.addObserver(self, selector: #selector(keyboardDidShow(notification:)), name: UIResponder.keyboardDidShowNotification, object: nil)
        notificationCenter.addObserver(self, selector: #selector(keyboardDidHide(notification:)), name: UIResponder.keyboardDidHideNotification, object: nil)
    }

    @objc dynamic private func keyboardWillShow(notification: Notification) {
        dispatchKeyboardEvent(for: notification, handler: willShowKeyboard)
    }

    @objc dynamic private func keyboardDidShow(notification: Notification) {
        dispatchKeyboardEvent(for: notification, handler: didShowKeyboard)
    }

    @objc dynamic private func keyboardWillHide(notification: Notification) {
        dispatchKeyboardEvent(for: notification, handler: willHideKeyboard)
    }

    @objc dynamic private func keyboardDidHide(notification: Notification) {
        dispatchKeyboardEvent(for: notification, handler: didHideKeyboard)
    }

    private func dispatchKeyboardEvent(for notification: Notification, handler: ((KeyboardDisplayInfo) -> Void)?) {
        guard let info = KeyboardDisplayInfo(notification: notification) else {
            return
        }
        handler?(info)
    }
}

In the code above we create a struct KeyboardDisplayInfo that holds all the information (contained in the notification object) about the keyboard to be presented. We then listen to all the keyboard related notifications, invoke the respective closures supplied by the observer. This KeyboardObserver class is the foundation that we will use for creating various keyboard behaviours. In this post, we will use this to implement the first of the aforementioned keyboard behaviours. Before we begin, let’s first define a protocol that all behaviours will need to conform to:

1
protocol KeyboardAdjustmentBehaviour { }

ContentInsetAdjustementBehaviour

This behaviour adjusts the supplied scroll view content inset to account for keyboard presentation.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class ContentInsetAdjustmentBehaviour: KeyboardAdjustmentBehaviour {
    weak var scrollView: UIScrollView?
    private let observer: KeyboardObserver

    init(observer: KeyboardObserver) {
        self.observer = observer

        observer.willShowKeyboard = { [weak self] info in
            // adjust content inset to account for keyboard size
            self?.scrollView?.contentInset = UIEdgeInsets(top: 0.0, left: 0.0, bottom: info.endFrame.size.height, right: 0.0)
        }

        observer.willHideKeyboard = { [weak self] info in
            // reset content inset back to zero
            self?.scrollView?.contentInset = UIEdgeInsets.zero
        }
    }
}

Believe it or not, this behaviour matches the behaviour we defined in our initial TableViewController code. We can now update our TableViewController code to use this behaviour as follows:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class TableViewController: UITableViewController {
    private let keyboardObserver = KeyboardObserver(notificationCenter: NotificationCenter.default)
    var keyboardAdjustmentBehaviour: KeyboardAdjustmentBehaviour?
    ...

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)

        let adjustmentBehaviour = ContentInsetAdjustmentBehaviour(observer: keyboardObserver)
        adjustmentBehaviour.scrollView = tableView
        keyboardAdjustmentBehaviour = adjustmentBehaviour
    }

    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)

        keyboardAdjustmentBehaviour = nil
    }

    ...

}

Notice how the view controller is no more concerned with listening for keyboard notifications. By abstracting the keyboard adjustment into a behaviour, the view controller can just declare the kind of adjustment behaviour it wants, and the rest gets handled. This makes the code very modular, as we can easily replace the behaviour with a different behaviour without having to change the view controller code. It also supports reuse as this behaviour can easily injected into any view controller that has a scroll view.

Conclusion

By using composition, we were able to create a modular keyboard adjustment behaviour that changes the scroll view’s content inset to account for the presented keyboard. This provides an isolated abstraction that can be easily tested, and reused in many view controllers without duplicating code. Composition enables us to abstract a lot of functionality as behaviours that can be easily injected into view controllers. See my post on resizable formsheets for another example of this.

Thanks for taking the time 🙏🏿

Find me on twitter or contact me if you have any questions or suggestions.