March 11, 2019

A simple protocol-oriented keyboard avoiding technique

I want to share some neat plug-and-play keyboard avoiding code that I drop into my iOS projects pretty frequently. Making sure your views aren't obscured by the keyboard is one of those tasks in iOS development that you don't want to have to solve from scratch every time you start a project if you don't have to. It's also something that I would avoid bringing in a dependency for because it is not too time consuming to implement yourself. Libraries like TPKeyboardAvoiding are amazing in the way they just work and handle edge cases, but if there's a bug in the library or it's not working for your setup, and you don't understand how the code works, you're pretty much SOL until the maintainer fixes it, or you fork the library and fix the bug yourself - and at that point, I think you might as well have rolled your own.

Managing the keyboard comes down to observing notifications that iOS sends when the keyboard is about to appear. These notifications contain information about the size and location of the keyboard. The four notifications that the system can send you are as follows:

If your views are in a UIScrollView, then avoiding the keyboard is just a matter of adjusting the contentOffset so that the frame of the input view isn't obscured by the keyboard.

Here's a protocol which describes all the moving parts that a UIViewController has to know about in order to avoid the keyboard.

Pretty simple, right? The purpose of the first two methods are to handle registering and de-registering your view for keyboard notifications. And the last two properties are required in order to actually do the work of moving your keyboard out of the way of the input view. One thing that is very important to note, is that the location of the activeInputViewFrame must be described in the coordinate system of the UIViewController's view in order for this solution to work. That means you might have to call the UIView instance method convert(_ rect: CGRect, from view: UIView?) -> CGRect a few times, starting with the superview of your active input view, and ending with UIViewController's view.

Through the magic of Swift, we can provide a default implementation of the first two methods in a protocol extension, then listen for the keyboard notifications in that extension and use the scrollView and activeInputViewFrame to do heavy lifting. The result is a protocol your view controllers can simply adopt and get keyboarding avoiding for free. Here's the extension in it's entirety:

The core of the solution is from line 32 to 38. What we're doing there is checking if the distance between the bottom of the active input view's frame and the top of the keyboard's frame is less than 0 - which would mean they overlap. We account for the space above the VC's view (perhaps occupied by a nav bar) by subtracting the view's safeAreaInset.top. If the frames overlap, all we have to do is adjust the content offset by the amount that they overlap. Here's a visual to make this clearer:

That's it! As the user navigates between inputs, all you have to do is keep resetting the activeInputViewFrame in order to keep the keyboard out of the way. Full disclosure, I haven't tested this as a one-size-fits-all solution. I've used this in a few very similar view hierarchies: you have a UIScrollView constrained to a VC's edges that that has some subviews which contain UITextField/ UITextView. You might need to do some finagling depending on your setup, but it's hopefully easy to trace what's going wrong by setting some breakpoints in the notification handlers - a benefit of this solution being rather concise.

I hope this helps unblock you or taught you something new! Please feel free to tweet me or comment below if you have questions.