Responding To Keyboard Events in iOS

Posted 1/30/2018.

Wherever users can enter text in our apps, we usually need to respond to keyboard events. Adjusting our app's UI when the keyboard is shown and dismissed is necessary to maintain a good user experience. But what if we have multiple screens in our app that need to respond to keyboard events?

In this tutorial we'll write a reusable KeyboardObserver class that allows view controllers to easily respond to keyboard events. It will abstract away NotificationCenter and define a simple delegate protocol that view controllers can implement.

Observer Delegate

Let's start with the public API of our keyboard observer, the delegate protocol.

protocol KeyboardObserverDelegate: class {
    func keyboardObserver(_ keyboardObserver: KeyboardObserver, didShowKeyboardWithAttributes attributes: KeyboardPresentationAttributes)
    func keyboardObserver(_ keyboardObserver: KeyboardObserver, didHideKeyboardWithAttributes attributes: KeyboardPresentationAttributes)
}

This is all our view controller will need to implement. Our keyboard observer class will receive the keyboard event notifications from NotificationCenter, which include a set of presentation attributes such as the beginning and ending frame of the keyboard, the duration of the presentation animation, and the animation curve used in the animation.

We haven't written the KeyboardPresentationAttributes struct that is passed back in both functions yet, so let's do that now:

struct KeyboardPresentationAttributes {

    let beginFrame: CGRect
    let endFrame: CGRect
    let animationDuration: Double
    let animationOptions: UIViewAnimationOptions

    init?(userInfo: [AnyHashable: Any]?) {
        guard
            let beginFrame = (userInfo?[UIKeyboardFrameBeginUserInfoKey] as AnyObject).cgRectValue,
            let endFrame = (userInfo?[UIKeyboardFrameEndUserInfoKey] as AnyObject).cgRectValue,
            let animationDuration = (userInfo?[UIKeyboardAnimationDurationUserInfoKey] as AnyObject).doubleValue,
            let animationCurve = (userInfo?[UIKeyboardAnimationCurveUserInfoKey] as AnyObject).uintValue
            else { return nil }
        self.beginFrame = beginFrame
        self.endFrame = endFrame
        self.animationDuration = animationDuration
        self.animationOptions = UIViewAnimationOptions(rawValue: animationCurve << 16)
    }

}

The keyboard event notification's attributes are stored in its userInfo, which we will pass into the initializer here. One important note is that the attributes dictionary contains the integer value for a UIViewAnimationCurve, but UIView animations require a value of type UIViewAnimationOptions. Bitshifting the UIViewAnimationCurve value is currently a reliable way to convert it into UIViewAnimationOptions.

Observer Class

Now that we have the delegate protocol and a convenient struct for representing the keyboard presentation attributes, we're ready to write the observer class:


class KeyboardObserver {

    weak var delegate: KeyboardObserverDelegate?

    init(delegate: KeyboardObserverDelegate?) {
        self.delegate = delegate
        addObservers()
    }

    private func addObservers() {
        NotificationCenter.default.addObserver(self, selector: #selector(toggleKeyboard), name: .UIKeyboardWillShow, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(toggleKeyboard), name: .UIKeyboardWillHide, object: nil)
    }

    deinit {
        NotificationCenter.default.removeObserver(self)
    }

    @objc private func toggleKeyboard(for notification: Notification) {
        guard let attributes = KeyboardPresentationAttributes(userInfo: notification.userInfo) else { return }
        switch notification.name {
        case .UIKeyboardWillShow:
            delegate?.keyboardObserver(self, didShowKeyboardWithAttributes: attributes)
        case .UIKeyboardWillHide:
            delegate?.keyboardObserver(self, didHideKeyboardWithAttributes: attributes)
        default:
            break
        }
    }

}

As you can see, it's pretty straightforward. The observer is initialized with a delegate, adds itself as a NotificationCenter observer for the keyboard event notifications, and removes itself as an observer when it is deinitialized. Both notifications use the toggleKeyboard selector, which creates a KeyboardPresentationAttributes value from the notification's userInfo dictionary and switches on the notification's name to determine which delegate method to call.

Using The Keyboard Observer

All that is left now is to add an instance of KeyboardObserver to a view controller and implement the KeyboardObserverDelegate protocol. First, let's look at the view controller setup:

class TextEntryViewController: UIViewController {

    @IBOutlet weak var textView: UITextView!
    @IBOutlet weak var textViewBottomConstraint: NSLayoutConstraint!

    var keyboardObserver: KeyboardObserver?
    let textViewPadding: CGFloat = 8

    override func viewDidLoad() {
        super.viewDidLoad()

        configureTextView()
        keyboardObserver = KeyboardObserver(delegate: self)
    }

    func configureTextView() {
        textView.layer.cornerRadius = 8
        textView.layer.borderWidth = 1
        textView.layer.borderColor = UIColor.black.cgColor
    }

}

All this view controller has is a text view constrained to the edges of the main view. We have an outlet for the bottom constraint, which we'll use to adjust the text view's height when the keyboard is shown and dismissed.

Here we've also added an instance of KeyboardObserver, which is set in viewDidLoad passing in self as the delegate. Let's now implement the delegate protocol:

extension TextEntryViewController: KeyboardObserverDelegate {

    func toggleKeyboard(with attributes: KeyboardPresentationAttributes) {
        UIView.animate(withDuration: attributes.animationDuration, delay: 0, options: attributes.animationOptions, animations: {
            self.textViewBottomConstraint.constant = attributes.endFrame.size.height + self.textViewPadding
            self.view.layoutIfNeeded()
        }, completion: nil)
    }

    func keyboardObserver(_ keyboardObserver: KeyboardObserver, didShowKeyboardWithAttributes attributes: KeyboardPresentationAttributes) {
        toggleKeyboard(with: attributes)
    }

    func keyboardObserver(_ keyboardObserver: KeyboardObserver, didHideKeyboardWithAttributes attributes: KeyboardPresentationAttributes) {
        toggleKeyboard(with: attributes)
    }

}

Both delegate method implementations call toggleKeyboard, which simply uses the keyboard presentation attributes to animate the textViewBottomConstraint.


Responding to keyboard events is a common requirement, and as engineers we are always looking for ways to centralize and reuse common patterns. Aside from allowing view controllers to add handling of keyboard notifications more easily, centralizing this logic in a separate class makes it easier to test. For example, as a next step we might use dependency injection to test the NotificationCenter logic, and doing this in one place is much better than testing this logic separately in every view controller that allows text input.

Next Steps