Handling keyboard appearance in iOS 11

24 August 2017

Handling the appearance of the keyboard in an iOS interface has always been something of a pain. The keyboard appears as overlay on top of the rest of the interface, so there’s the chance that an important part of the UI is obscured. Worst case, the control that the keyboard entry is appearing in can’t been seen.

There’s been various hacks and workarounds over the years, and a number of 3rd-party controls. But the gradual introduction of more and more AutoLayout features means that keyboard handling is now - almost - easy enough not to need the convenience of an external library.

Personally I’m not sure why there’s never been an Apple-implemented convenience method, but maybe the UIKit engineers are busy with other stuff. Here’s a quick, hacky approach to moving UI content in response to keyboard appearance, and reverting to the original position when the keyboard is dismissed.

The concept

The key (pun intended) is listening to the UIKeyboardWillShow and UIKeyboardWillHide notifications. These are fired just before the keyboard is animated onscreen, and just after it disappears. If you subscribe your view controller to these, you can use AutoLayout constraints to move your content around and out of the way of the keyboard.

To start the process from the code side, add two notification subscriptions to your view controller (the viewDidLoad function is as good a place as any):

override func viewDidLoad() {
  
    super.viewDidLoad()
    // Register this class for keyboard notifications
    
    NotificationCenter.default.addObserver(self,
      selector: #selector(self.keyboardWillShow(notification:)),
      name: NSNotification.Name.UIKeyboardWillShow, object: nil)
    
    NotificationCenter.default.addObserver(self,
      selector: #selector(self.keyboardWillHide(notification:)),
      name: NSNotification.Name.UIKeyboardWillHide, object: nil)
    
}

Then add the two stub functions that will handle the layout changes in response to the notifications:

@objc func keyboardWillShow(notification: Notification) { }

@objc func keyboardWillHide(notification: Notification) { }

Setting up the view

The trick to this approach is to embed the content that needs to move inside a container view, and adjust the position of the container when the keyboard appears and disappears.

The diagram shows the concept: the UITextField is embedded within the gray container UIView, and the position of the container is controlled by the blue NSLayoutConstraint between the container view and the Safe Area Layout guide (the green guide)

simulator

There are two options to handling the change in layout - if you change only the bottom space constraint, the gray container view will shrink; if you change both top and bottom constraints, the container view will move and retain its proportions (that’s the option that’s shown).

After adding the constraints in Interface Builder, add outlets to the View Controller and connect them:

// Outlet for the constraint between the bottom of the container view
// and the bottom of the safe area
@IBOutlet weak var containerViewBottomLayoutConstraint: NSLayoutConstraint!

// Outlet for the constraint between the top of the container view
// and the top of the safe area
@IBOutlet weak var containerViewTopLayoutConstraint: NSLayoutConstraint!

Finally, we’ll also need a variable to hold the original value of the bottom constraint, so that it can be set back after the keyboard disappears:

var containerViewBottomConstant: CGFloat = 0.0

This needs to be set after the view has appeared for the first time:

override func viewDidAppear(_ animated: Bool) {
    // Grab default spacing between background container and the safe area
    // when the view first appears
    containerViewBottomConstant = 
      containerViewBottomLayoutConstraint.constant
}

Handling keyboard appearance

When the keyboard appears or disappears, it fires a Notification event which you can subscribe to with a function that takes an optional Notification parameter. Contained inside this Notification is a userInfo Dictionary; this has various key/value pairs including (in the case of keyboard appearance and disappearance events) one with the key UIKeyboardBoundsUserInfoKey.

This is a CGRect containing the current size and location of the keyboard. The location we don’t care about, but the vertical size is of interest. By adding this to the constant of the bottom spacing constraint, the container view will be moved up out of the way of the keyboard:

@objc func keyboardWillShow(notification: Notification) {
    
    // If there's no userInfo in the notification,
    // can't do anything so just return
    guard let userInfo = notification.userInfo else {
        return
    }
    // If there's no keyboardRect in the notification,
    // can't do anything so just return
    guard let keyboardRect = userInfo["UIKeyboardBoundsUserInfoKey"] as? CGRect else {
        return
    }
    
    // Adjust the background container's spacing to the
    // bottom of the safe area by the height of the keyboard
    containerViewBottomLayoutConstraint.constant = 
      containerViewBottomConstant - keyboardRect.height
    
    // Adjust the background container's spacing to the
    // top of the safe area by the height of the keyboard
    containerViewTopLayoutConstraint.constant = 
      containerViewTopLayoutConstraint.constant - keyboardRect.height
}

If you don’t alter the value of the container view’s topLayoutConstraint, the view will be compressed by the height of the keyboard. Depending on your UI, it might be better to move the container view as a whole, which is the approach shown here.

Handing keyboard disappearance

Handling the removal of the keyboard is more or less the reverse:

@objc func keyboardWillHide(notification: Notification) {
    
    // Adjust the background container's spacing to the
    // top and bottom of the safe area back to the original values
    containerViewBottomLayoutConstraint.constant = 
      containerViewBottomConstant
    containerViewTopLayoutConstraint.constant = 0
    
}

With the two keyboard notification functions implemented, the final effect looks like this:

simulator