Split views and unexpected keyboards

Last updated: Jan 17, 2020

When iOS 9 introduced the multi-tasking split views to the iPad it created a set of surprises for existing Apps. The most obvious was the need to adapt to slide over and split screen layouts. Less obvious is the need to always consider the keyboard. Take a look at the following split-screen setup:

If I tap on a text field in Safari on the left of the split the keyboard appears. Unfortunately the App on the right is not aware of this and does not adjust the content now hidden by the keyboard. There is no way to scroll down to view the hidden content without first dismissing the keyboard.

The solution is nothing new - we need to move content hidden by the keyboard. What is new is that we need to do this even for views that do not ever expect to interact with the keyboard. Let’s take a look at an easy solution to this common problem.

Moving Content Hidden by the Keyboard

There are a few approaches we can take to move content when the system shows the keyboard. Consider the scenario shown below where the App on the right has some static content which is a mix of UILabel and UIButton elements. Until iOS 9 there was never any possibility that this type of view could be hidden by the keyboard.

The approach that seems the easiest to me in these cases is to embed the content in a scroll view. We can then listen for the keyboard notifications and add a content inset to the scroll view when the keyboard is shown. This code could go in the view controller but I think it is more reusable to use a subclass of the scroll view.

Listening for keyboard notifications

Let’s get started with our subclass of UIScrollView that I will name AdaptiveScrollView:

final class AdaptiveScrollView: UIScrollView {

We do the usual dance to have some common setup code run when our view is initialised:

override init(frame: CGRect) {
  super.init(frame: frame)
  setup()
}

required init?(coder aDecoder: NSCoder) {
  super.init(coder: aDecoder)
  setup()
}

Our setup code listens to notifications for when the keyboard has been shown and when it is about to be hidden:

private func setup() {
  let defaultCenter = NotificationCenter.default
  defaultCenter.addObserver(self,
    selector: #selector(keyboardDidShow(_:)),
    name: UIResponder.keyboardDidShowNotification,
    object: nil)
  defaultCenter.addObserver(self,
    selector: #selector(keyboardWillHide(_:)),
    name: UIResponder.keyboardWillHideNotification,
    object: nil)
}

Handling when the keyboard is shown

With the setup done, let’s look at what we need to do when the keyboard is shown:

@objc private func keyboardDidShow(_ notification: Notification) {
  guard let userInfo = notification.userInfo,
    let keyboardFrame = userInfo[UIResponder.keyboardFrameEndUserInfoKey] 
    as? NSValue else {
    return
  }
      
  let keyboardSize = keyboardFrame.cgRectValue.size
  let contentInsets = UIEdgeInsets(top: 0.0, left: 0.0, bottom: keyboardSize.height, right: 0.0)
  adjustContentInsets(contentInsets)
}

First we check that the userInfo dictionary of the notification contains the keyboard frame and if so get the value. This is an NSValue containing a CGRect for the keyboard frame. The cGRectValue method returns the CGRect from the NSValue so that we can get the keyboard size. We are only interested in the keyboard height which we use to construct a content inset for our scroll view.

The adjustContentInsets function is what actually sets both the content inset and scroll indicator insets of the scroll view:

private func adjustContentInsets(_ contentInsets: UIEdgeInsets) {
  contentInset = contentInsets
  scrollIndicatorInsets = contentInsets
}

Note it is also possible to detect if the keyboard appearance was triggered locally by our app or by the other app running in the split view. The keyboardIsLocalUserInfoKey is new in iOS 9:

if let keyboardIsLocal = userInfo[UIResponder.keyboardIsLocalUserInfoKey] as? NSNumber {
  print("keyboard is local: \(keyboardIsLocal.boolValue)")
}

Handling when the keyboard is hidden

When the system informs us that it is about to hide the keyboard we just need to remove the content inset from the scroll view to put everything back to normal:

@objc private func keyboardWillHide(_ notification: Notification) {
  adjustContentInsets(.zero)
}

Applying our Adaptive Scroll View

With the hard work done we just need to embed everything in a scroll view (if it is not already) and remember to set the class to our adaptive subclass:

If all worked well we should be able to scroll our view all the way to the bottom with the split view keyboard showing:

Further Reading