Nothing ruins a perfectly good day at the office like the iOS keyboard showing up at the bottom half of your screen, covering buttons and text fields. Moving essential UI with the pace of the keyboard to keep it visible is a part of the mechanics of the UI that the user expects to work.
Unfortunately, we can’t take the mechanics of moving UI out of the way of the keyboard for granted. In fact, when they keyboard appears, it’s the developer’s job to try to keep up with it and UIKit does not make that task particularly easy.
One hot summer day in the Shape office, we decided that we had enough of fighting the keyboard and we wanted to fix it once and for all.
TL;DR: We are pretty happy with the result and we want you to use it as well. Check out SHPKeyboardAwareness on GitHub.
Now for the slightly longer version.
Keeping up with the keyboard
So how do we deal with the keyboard and move our precious UI out of the way? The official documentation has Apple’s take.
Assuming you are not using a UITableViewController
(which has some support for avoiding the keyboard), here are the basics:
Listen for notifications about when the keyboard appears and disappears.
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Delegate the UITextField and save it in an instance variable when it becomes first responder.
1 2 3 4 5 6 7 8 9 |
|
When the keyboard appears, adjust your scroll view (assuming you are using a scroll view) and reset it when the keyboard goes away.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
This code pretty much gets the job done, but has a number of drawbacks:
- Fragmented code.
- Not easily integrated.
- Code must be duplicated across view controllers and is not easily encapsulated.
- Requires even more fragments if mixed with UITextViews.
- Does not consider rotation (keyboard frame is in screen coordinates).
- Requires delegation of the input fields which may not always be convenient if the input field belongs to a subview.
Why is this so hard?
In order to make this work, we need to rely on two distinct events (we use the term text field to denote the input view, but it also covers text view):
- The text field becoming first responder.
- The keyboard-will-appear notification.
We need the text field so we can get its frame in the (scroll) view and we need the keyboard notification to know the frame of the keyboard and the animation curve and duration with which it enters the screen so we can move the text field out of the way in the same pace. These are two distinct events and with imperative programming, it gets ugly fast. We need to rely on a different paradigm to solve this in a nice way.
Enter Reactive Cocoa
Functional Reactive Programming is all over the place these days and at Shape we have really embraced it using Reactive Cocoa. If you don’t know the framework you should read the documentation. Even if you are not familiar with Reactive Cocoa, you can read on even though there may be some unfamiliar terms.
With Reactive Cocoa we can combine and merge distinct events and use their output as parameters to a function that makes the problem much easier to solve. Here’s how we do it:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Now we have achieved a unification of the two distinct events and merged them into a single event we can work on to achieve keyboard bliss. When fired, combinedShowSignal
will send a tuple of two NSNotification
objects which we can unwrap and perform the logic necessary to animate the changes to our UI with the keyboard animation. If you want to check out how that is done, please have a look at the code on GitHub.
Wrapping it up
We wanted to solve keyboard avoidance in the general case, encapsulating all the logic needed in a separate module. Also, it was a priority that the solution didn’t impose any design requirements or assumptions when integrating it into our projects. In other words, we wanted a very lean, decoupled and easy to use interface.
We decided on isolating all the code in a category, not on UIView
or UIViewController
but on NSObject
which may sound a bit odd. Read on. The interface comes in two flavors and this is what it looks like:
A Reactive Cocoa based interface:
1
|
|
A traditional interface:
1
|
|
So why an NSObject category? Any object that imports the header can call one of these methods and get either a ‘next’ or a callback when the keyboard is about to appear or disappear. So it’s up to you if you want to handle the keyboard from the view controller, a view or some helper object.
The traditional interface requires that the receiver implements a single method defined in the SHPKeyboardAwarenessClient
protocol to get the callback. Whenever the signal or callback is fired, a value of the type SHPKeyboardEvent
is provided, which is a simple container object, holding all relevant information to move the UI out of the way. It is thus the job of the receiver to decide how to deal with the keyboard event, but all the necessary bits of information is collected and delivered in a nice package.
Here’s an example where SHPKeyboardAwareness is used from a view controller, managing a collection view:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
|
Notice that nowhere do we unsubscribe from keyboard events. When the receiver is deallocated, the subscription is silently cancelled so there is no need to do any house cleaning at any point.
There’s another nice feature you might notice in the code sample above. We can store the original offset in the SHPKeyboardEvent
object on the ‘show’ event, so we can read it out and restore the collection view to its former state when the keyboard disappears. SHPKeyboardAwareness ensures that the same event instance is passed on ‘show’ and ‘hide’ events so that state can be saved and restored.
One more thing
SHPKeyboardAwareness has one last trick up its sleeve. In the example presented above, an event will be fired whenever a ‘UITextField’ or ‘UITextView’ will become first responder. There is a way to limit the scope however. When engaging keyboard awareness, you can pass in a view that you want events for. You will then only get keyboard events if the keyboard frame conflicts with the given view. This is useful, if for instance you have a container view with a text field and an action button inside and you want the entire container to clear the keyboard. The interface looks like this:
Reactive Cocoa based:
1
|
|
Traditional:
1
|
|
The result may look like this:
Conclusion
We really like how this project turned out and we’re very happy to release it to the world. In fact, SHPKeyboardAwareness is the first open source project from Shape. We encourage you to try it out and if you find any bugs or ways of improving it, pull requests are very welcome. Get SHPKeyboardAwareness here.