Introducing SHPKeyboardAwareness - Avoid the iPhone Keyboard Covering Your Text Fields and Views

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
// Call this method somewhere in your view controller setup code.
- (void)registerForKeyboardNotifications
{
    [[NSNotificationCenter defaultCenter] addObserver:self
            selector:@selector(keyboardWasShown:)
            name:UIKeyboardDidShowNotification object:nil];

   [[NSNotificationCenter defaultCenter] addObserver:self
             selector:@selector(keyboardWillBeHidden:)
             name:UIKeyboardWillHideNotification object:nil];

}

Delegate the UITextField and save it in an instance variable when it becomes first responder.

1
2
3
4
5
6
7
8
9
- (void)textFieldDidBeginEditing:(UITextField *)textField
{
    activeField = textField;
}

- (void)textFieldDidEndEditing:(UITextField *)textField
{
    activeField = nil;
}

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
// Called when the UIKeyboardDidShowNotification is sent.
- (void)keyboardWasShown:(NSNotification*)aNotification
{
    NSDictionary* info = [aNotification userInfo];
    CGSize kbSize = [[info objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size;

    UIEdgeInsets contentInsets = UIEdgeInsetsMake(0.0, 0.0, kbSize.height, 0.0);
    scrollView.contentInset = contentInsets;
    scrollView.scrollIndicatorInsets = contentInsets;

    // If active text field is hidden by keyboard, scroll it so it’s visible
    // Your app might not need or want this behavior.
    CGRect aRect = self.view.frame;
    aRect.size.height -= kbSize.height;
    if (!CGRectContainsPoint(aRect, activeField.frame.origin) ) {
        [self.scrollView scrollRectToVisible:activeField.frame animated:YES];
    }
}

// Called when the UIKeyboardWillHideNotification is sent
- (void)keyboardWillBeHidden:(NSNotification*)aNotification
{
    UIEdgeInsets contentInsets = UIEdgeInsetsZero;
    scrollView.contentInset = contentInsets;
    scrollView.scrollIndicatorInsets = contentInsets;
}

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):

  1. The text field becoming first responder.
  2. 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
// shpka_rac_notifyUntilDealloc is our own convenience method that returns
// a signal with a notification that completes when the receiver deallocates.
RACSignal *keyboardSignal = [self shpka_rac_notifyUntilDealloc:UIKeyboardWillShowNotification];

// viewSignal is a signal that fires whenever a UITextField or UITextView becomes first responder.
RACSignal *viewSignal = [RACSignal merge:@[
  [self shpka_rac_notifyUntilDealloc:UITextFieldTextDidBeginEditingNotification],
  [self shpka_rac_notifyUntilDealloc:UITextViewTextDidBeginEditingNotification]]
];

// The two signals above, combined in a ‘zip’, meaning that one of the zipped signals
// will wait for the other before combinedShowSignal is fired.
RACSignal *combinedShowSignal = [RACSignal zip:@[viewSignal,keyboardSignal]];

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
- (RACSignal *)shp_keyboardAwarenessSignal;

A traditional interface:

1
- (void)shp_engageKeyboardAwareness;

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
- (void)viewDidLoad {
    [super viewDidLoad];

    // Subscribe to keyboard events. The receiver (self) will be automatically unsubscribed when deallocated
    [self shp_engageKeyboardAwareness];
}

- (void)keyboardTriggeredEvent:(SHPKeyboardEvent *)keyboardEvent {

    CGFloat offset = 0;
    switch (keyboardEvent.keyboardEventType) {
        case SHPKeyboardEventTypeShow:

            // Keyboard will appear. Calculate the new offset from the provided offset
            offset = collectionView.contentOffset.y - keyboardEvent.requiredViewOffset;

            // Save the current view offset into the event to retrieve it later
            keyboardEvent.originalOffset = collectionView.contentOffset.y;

            break;
        case SHPKeyboardEventTypeHide:

            // Keyboard will hide. Reset view offset to its state before keyboard appeared
            offset = keyboardEvent.originalOffset;

            break;
        default:
            break;
    }

    // Animate the offset change with the provided curve and duration
    [UIView animateWithDuration:keyboardEvent.keyboardAnimationDuration
                          delay:0
                        options:keyboardEvent.keyboardAnimationOptionCurve
                     animations:^{
        self.collectionView.contentOffset = CGPointMake(collectionView.contentOffset.x, offset);

                self.collectionView.contentInset = UIEdgeInsetsMake(0, 0, event.keyboardFrame.size.height, 0);
        self.collectionView.scrollIndicatorInsets = self.collectionView.contentInset;
    } completion:nil];
}

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
- (RACSignal *)shp_keyboardAwarenessSignalForView:(UIView *)view;

Traditional:

1
- (void)shp_engageKeyboardAwarenessForView:(UIView *)view;

The result may look like this:

SHPKeyboardAwareness in action

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.

Comments