Automating keyboard layouts

Normally, developing a keyboard interface starts with putting a UITextField, a UITextField, or another UIResponder into your application’s view hierarchy…

And then this component becomes a statement: “From here forth, I declare this view to be a keyboard input view”. Except… Well… it isn’t, or at least doesn’t act like it. Unless you manage the layout of your content by implementing your keyboard notifications, the app’s main view will not respond or adapt to the keyboard update, and all sort of messed up situations can arise from this, like the keyboard overlaying your UITextField, UIScrollViews not being able to scroll or UITableViews not being able to display their last rows of content.

So then you go about your code writing a very generic solution for handling your keyboard logic, something that’s start with:


override func viewDidLoad() {
  super.viewDidLoad()
  NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardWillShow:"), name: UIKeyboardWillShowNotification, object: nil)
  NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardWillHide:"), name: UIKeyboardWillShowNotification, object: nil)
}

This is code that all developers deal with at one point or another. And then you go and implement a solution for it. The problem is that this code is local to the view controller at hand. So then you go on with your app development and you realise the next view controller also has an input field, so then you write another implementation for viewDidLoad that looks EXACTLY LIKE THE ONE YOU JUST WROTE.

Not only that, the only difference between the solutions for managing the keyboard in this view controller and the one you just wrote is the name of the variables accessed. Before you know it, you’ve just rewritten 20 lines on code in two different views of your app to manage exactly the same thing. Sounds familiar?

So then comes the inevitable question I started asking myself: Can’t I just automate this?

YES YOU CAN.

All you will need is a couple of classes with less than 130 lines of code.

SPOILER ALERT
I’ve already gone ahead and written the full example, which you can download from my Github page, so go ahead and take a look at it (if you want).
SPOILER ALERT

The first step is understanding that we can ditch our implementations for both UIKeyboardWillShowNotification and UIKeyboardWillHideNotification. In reality all we really need to listen to is UIKeyboardWillChangeFrameNotification. The advantage to this is that this notification not only gets fired when the keyboard presents and dismisses itself, but also when it changes state by either undocking or splitting on the iPad or displaying the autocomplete bar on iPhone and iPad. It’s also as future proof as it can go. Implement this and you will be able to tackle 5 problems with one solution.

The other thing you need to be aware of is that UIKeyboardNotifications have a lot of information that are very relevant for animations, more importantly:

  • The keyboard’s start frame
  • The keyboard’s end frame
  • The animation curve
  • The animation duration

So let’s jump in and write some code. Starting with something small and simple, we’ll write a convenience method for managing animations that are in sync with the keyboard animation, using some of the keys described above:


private func throwKeyboardNotificationAssertionFailure() {
  assertionFailure("This method can only be called with a notification dictionary passed by a keyboard update notification. Could not find expected value keys for UIKeyboardAnimationDurationUserInfoKey and/or UIKeyboardAnimationCurveUserInfoKey values")
}

extension UIView {

  class func animateWithKeyboardNotificationUserInfo(notificationDictionary:[NSObject : AnyObject], animation:() -> Void) {
    guard let duration = notificationDictionary[UIKeyboardAnimationDurationUserInfoKey] as? NSNumber else {
      throwKeyboardNotificationAssertionFailure()
      return
    }

    guard let curve = notificationDictionary[UIKeyboardAnimationCurveUserInfoKey] as? NSNumber else {
      throwKeyboardNotificationAssertionFailure()
      return
    }

    UIView.animateWithDuration(NSTimeInterval(duration.doubleValue), delay: 0, options: [UIViewAnimationOptions(rawValue: curve.unsignedLongValue), UIViewAnimationOptions.BeginFromCurrentState], animations: animation, completion: nil);
  }

}

As you can see, we are leveraging some of the new cool features of Swift 2.0 in order to make sure that the data passed to this method is relevant to a UIKeyboard notification. By using: ‘guard let  = notificationDictionary[] else’ statements, we can ensure the correct usage of our code other developers and our future selves. In the case that these safety nets don’t pass, we can throw a generic assertion failure with relevant information that can help other developers diagnose and fix the issue in the future. Once we have passed these safety checks we can call our normal UIView animation code with the parameters passed by the UINotification object, and then perform any animation block as specified by the function’s second parameter.

These are the first 30 lines of code of our implementation. Now we have to implement the autoresizing view logic.

Due to the very specific nature of the view, its behaviour would only ever be relevant for the root view of a full screen view controller. We also want this to be a drop in replacement for UIView, as we want to keep the UIViewController to be free to subclass other feature sets, like networking or generic common base logic specific to view controllers. Unfortunately, we can’t check for this conformance at runtime, so we will have conform ourselves by giving our view a very thorough name. I’ve opted for “EPICAutoresizingKeyboardInputRootView”. I know, it’s a mouthful, but it clearly conveys the view’s intent and will decrease the risk of it being used unintentionally.

So here’s the start of our implementation for our autoresizing keyboard input root view:


class EPICAutoresizingKeyboardInputRootView : UIView {

  //MARK: - variables
  private var fullViewFrame : CGRect!

  //MARK: - lifecycle
  override init(frame: CGRect) {
    super.init(frame: frame)
    commonInit()
  }

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

  private func commonInit() {
    registerNotifications()
  }

  deinit {
    NSNotificationCenter.defaultCenter().removeObserver(self)
  }

  //MARK: - notification handling
  private func registerNotifications() {
    NSNotificationCenter.defaultCenter().addObserver(self, selector: Selector("keyboardWillChangeFrame:"), name: UIKeyboardWillChangeFrameNotification, object: nil)
  }

  //MARK: - layout
  override func layoutSubviews() {
    fullViewFrame = self.window!.frame
    super.layoutSubviews()
  }

}

This is fairly straightforward, we register for UIKeyboardWillChangeFrameNotification when the view is created, either programatically of from a Xib/Storyboard, and we make sure that the observer for this notification get’s removed before the object gets destroyed.

The important part is that the view always keeps a reference of it’s potential full size with the variable fullViewFrame, which gets updated every time the view lays out its subviews. The reason for this is so that the view can allow support for device rotations and screen changes, since device orientations always trigger a new layout pass. By referencing the view’s window frame at this point, we can be assured that the view will always hold a reference of what it must set itself to when the keyboard dismisses, regardless of rotation or window size.

Now we tie it all together by implementing “keyboardWillChangeFrame:”, all we need to do is add this to our class:


private func throwKeyboardNotificationAssertionFailure() {
  assertionFailure("This method should only be called using a keyboard update notification. Failed to retrieve the correct keyboard notification information.")
}

class EPICAutoresizingKeyboardInputRootView : UIView {

  internal func keyboardWillChangeFrame(notification:NSNotification) {
    guard let notificationDictionary = notification.userInfo else {
      throwKeyboardNotificationAssertionFailure()
      return
    }

    guard let keyboardFrame = notificationDictionary[UIKeyboardFrameEndUserInfoKey]?.CGRectValue else {
      throwKeyboardNotificationAssertionFailure()
      return
    }

    if !CGRectEqualToRect(keyboardFrame, CGRectZero) {
      let keyboardTop = keyboardFrame.origin.y
      let viewBottom = fullViewFrame!.origin.y + fullViewFrame!.size.height
      if keyboardTop >= viewBottom || keyboardTop + keyboardFrame.size.height < viewBottom {
        //keyboard will hide or is ipad split keyboard
        UIView.animateWithKeyboardNotificationUserInfo(notificationDictionary, animation: { () -> Void in
          self.frame = self.fullViewFrame!
        })
      } else {
        //keyboard will show
        var viewFrame = fullViewFrame!
        viewFrame.size.height = fullViewFrame!.size.height-(viewBottom-keyboardTop)
        UIView.animateWithKeyboardNotificationUserInfo(notificationDictionary, animation: { () -> Void in
          self.frame = viewFrame
        })
      }
    }
  }

}

We check our data first by using guard statements, once we are happy we compare the keyboard frame against the fullViewFrame variable we stored and update in our override of layoutSubviews. The logic here is pretty straightforward: if the keyboard’s top is below or equal to our view’s bottom or the keyboard’s bottom is above our view’s bottom, we show our view full screen, alternatively, we adapt the view’s height so that it only fills the portion of the view that’s not covered by the keyboard.

Great! So what can we do with this? For starters we can say goodbye to having to implement UIKeyboard notifications ever again. All we have to do is make sure the root view of our view controllers that will display a keyboard is a member of our EPICAutoresizingKeyboardInputRootView class. Then we can add constraints to our subviews in order to keep a proportional layout throughout the view’s resizing. Or we can add a UITableView or UIScrollview as subviews of our class and constrain them to full size so that it automatically scales it’s visible content. The possibilities are endless.

Just remember this one rule: EPICAutoKeyboardView is designed to work as the root view of a full screen view controller. Adding this view anywhere else in the view hierarchy or inside a child view controller whose frame is not full screen might produce unexpected behaviour. Do yourselves a favour and just stick to the rule of the system: Use this class only as the root view of view controllers being presented full screen, capiche?

Author: Danny Bravo

Director @ EPIC

1 thought on “Automating keyboard layouts”

Comments are closed.