TMI #5: Scrollview for Keyboards in iOS

In TMI, Michael Helmbrecht gives overly detailed answers to popular mobile development questions.

Ever browsed the web for help, only to find the popular answers leave you hungry for more? In our fifth episode, Michael helps you tame your onscreen keyboard when it hides your UI by sliding it out of the way using UIScrollView.


Hi everyone! It’s Michael from Realm. I’m here again to answer some more of your mobile development questions.

I thought we’d keep going with our theme of keyboards, and talk about another question we’ve seen a lot online: How can your user type in fields located on the bottom half of the screen, when the keyboard covers them up?

We’ll talk about a few reasons why you might want to do this, and a few different techniques for moving things off of the bottom half of the screen in iOS.

To demonstrate some of these principles, I put together a small sample app in which we can figure out how to move things out of the keyboard’s way. Imagine we have an Emmy nomination form, where we can input all sorts of details about nominees. If we were to click one of our text fields in the bottom half of the screen, you would find that the keyboard covers up the field. This is what we’re here today to try to solve.

In the Xcode storyboard file, I’ve already put the information into a ScrollView, since I know that we’re going to want to use that to be able to scroll things out of the way. In order to get this to start avoiding the keyboard, I’ll open our ViewController.

import UIKit

class EmmyNominationViewController: UIViewController {

    @IBOutlet weak var scrollView: UIScrollView?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        
        // Register to be notified if the keyboard is changing size
        NSNotificationCenter.defaultCenter().addObserver(self,
            selector: "keyboardWillShowOrHide:", name: UIKeyboardWillShowNotification, object: nil)
        NSNotificationCenter.defaultCenter().addObserver(self,
            selector: "keyboardWillShowOrHide:", name: UIKeyboardWillHideNotification, object: nil)
    }
    
    deinit {
        // Don't have to do this on iOS 9+, but it still works
        NSNotificationCenter.defaultCenter().removeObserver(self)
    }

    func keyboardWillShowOrHide(notification: NSNotification) {
        
        // Pull a bunch of info out of the notification
        if let scrollView = scrollView, userInfo = notification.userInfo, endValue = userInfo[UIKeyboardFrameEndUserInfoKey], durationValue = userInfo[UIKeyboardAnimationDurationUserInfoKey] {
            
            // Transform the keyboard's frame into our view's coordinate system
            let endRect = view.convertRect(endValue.CGRectValue, fromView: view.window)
            
            // Find out how much the keyboard overlaps the scroll view
            // We can do this because our scroll view's frame is already in our view's coordinate system
            let keyboardOverlap = scrollView.frame.maxY - endRect.origin.y
            
            // Set the scroll view's content inset to avoid the keyboard
            // Don't forget the scroll indicator too!
            scrollView.contentInset.bottom = keyboardOverlap
            scrollView.scrollIndicatorInsets.bottom = keyboardOverlap
            
            let duration = durationValue.doubleValue
            UIView.animateWithDuration(duration, delay: 0, options: .BeginFromCurrentState, animations: {
                self.view.layoutIfNeeded()
                }, completion: nil)
        }
    }

}

We can see in our EmmyNominationViewController that we have a reference to our scrollView, so we’ll need to talk to that in a little while. The critical magic of being able to avoid the keyboard is this method call, which we’ll call twice:

NSNotificationCenter.defaultCenter().addObserver(self,
    selector: "keyboardWillShowOrHide:", name: UIKeyboardWillShowNotification, object: nil)
NSNotificationCenter.defaultCenter().addObserver(self,
    selector: "keyboardWillShowOrHide:", name: UIKeyboardWillHideNotification, object: nil)

We’re asking the default notification center to notify us when the keyboard will show (WillShow) or hide (WillHide). These are notifications that the system posts right before the keyboard comes on to or goes off of the screen. We’ll send them to the same selector since we’ll have the same magic. But, these two notifications will let us handle any sort of keyboard size changing.

deinit {
    // Don't have to do this on iOS 9+, but it still works
    NSNotificationCenter.defaultCenter().removeObserver(self)
}

In deinit, we need to unregister ourselves. On iOS 9 and above, the system will actually do this for us. Even though that means we won’t have to worry about it, this method call will still work. However, if you are supporting below iOS 9, it’s good to leave this in here for now; it won’t mess anything up.

func keyboardWillShowOrHide(notification: NSNotification) {
    
    // Pull a bunch of info out of the notification
    if let scrollView = scrollView, userInfo = notification.userInfo, endValue = userInfo[UIKeyboardFrameEndUserInfoKey], durationValue = userInfo[UIKeyboardAnimationDurationUserInfoKey] {
        
        // Transform the keyboard's frame into our view's coordinate system
        let endRect = view.convertRect(endValue.CGRectValue, fromView: view.window)
        
        // Find out how much the keyboard overlaps the scroll view
        // We can do this because our scroll view's frame is already in our view's coordinate system
        let keyboardOverlap = scrollView.frame.maxY - endRect.origin.y
        
        // Set the scroll view's content inset to avoid the keyboard
        // Don't forget the scroll indicator too!
        scrollView.contentInset.bottom = keyboardOverlap
        scrollView.scrollIndicatorInsets.bottom = keyboardOverlap
        
        let duration = durationValue.doubleValue
        UIView.animateWithDuration(duration, delay: 0, options: .BeginFromCurrentState, animations: {
            self.view.layoutIfNeeded()
            }, completion: nil)
    }
}

The real magic all happens here in our function. We take in this NSNotification, which means I have this big, compound if let where I’m pulling a bunch of stuff out of the notification. I’m getting its userInfo, since that could be optional, and I’m also pulling UIKeyboardFrameEndUserInfoKey and UIKeyboardAnimationDurationUserInfoKey (both optional).

Take note of the endValue as well. When transforming an endValue into a CGRect, the endValue indicates the frame that the keyboard will end up at. If it’s coming in, it’s a frame that’s partly on the screen, and if it’s about to hide, then it’s a frame that’s all the way off the screen. That’s going to tell us where we need to animate to in order to avoid it.

We will then call the ConvertRect method to make sure that we can handle things appropriately if our view isn’t the full window, and to make sure that we’re using the right coordinates.

let keyboardOverlap = scrollView.frame.maxY - endRect.origin.y

We also have to do a little bit of math to detect how much the keyboard overlaps. We’ll need to know if it’s overlapping a lot, or just a little.

Then, all we have to do is simply take the ScrollView and set both its contentInset and, importantly, its scrollIndicatorInsets. A lot of guides I’ve seen tell you to change the contentInset, but then forget about the scrollIndicatorInsets, so your indicator on the side looks a little wonky. Basically, all we’re doing is telling the bottom that there will be a little bit of an inset in order to make up for this keyboard overlap.

Finally, at the bottom, you’ll notice that we have done a little UIView animation so that everything looks beautiful. In our app, we can pull up and scroll around in our content without the keyboard being in the way. And, if we click on the “Studio” field, it actually scrolls a little bit for us to focus on the field, which is some nice out-of-the-box magic. That way, whenever the user taps it, it makes sure that that part is visible.

Extra Credit

There are two little pieces of extra credit that you can do, if you really want your app to feel magical.

Correct Animation Curves

First, we will get the information for the correct animation curve out of the notification. It’s another key in the dictionary.

func keyboardWillShowOrHide(notification: NSNotification) {
    
    // Pull a bunch of info out of the notification
    if let scrollView = scrollView, userInfo = notification.userInfo, endValue = userInfo[UIKeyboardFrameEndUserInfoKey], durationValue = userInfo[UIKeyboardAnimationDurationUserInfoKey], curveValue = userInfo[UIKeyboardAnimationCurveUserInfoKey] {
        
        // Transform the keyboard's frame into our view's coordinate system
        let endRect = view.convertRect(endValue.CGRectValue, fromView: view.window)
        
        // Find out how much the keyboard overlaps the scroll view
        // We can do this because our scroll view's frame is already in our view's coordinate system
        let keyboardOverlap = scrollView.frame.maxY - endRect.origin.y
        
        // Set the scroll view's content inset to avoid the keyboard
        // Don't forget the scroll indicator too!
        scrollView.contentInset.bottom = keyboardOverlap
        scrollView.scrollIndicatorInsets.bottom = keyboardOverlap
        
        let duration = durationValue.doubleValue
        let options = UIViewAnimationOptions(rawValue: UInt(curveValue.integerValue << 16))
        UIView.animateWithDuration(duration, delay: 0, options: options, animations: {
            self.view.layoutIfNeeded()
            }, completion: nil)
    }
}

You’ll see here that it’s called the AnimationCurve key. We’ll get that out, but due to a weird bug in UIKit, that enum value is not actually available by a public API. Instead, we’ll do this line of crazy Swift magic, and convert that value back into a valid UIViewAnimationOptions value:

let options = UIViewAnimationOptions(rawValue: UInt(curveValue.integerValue << 16))

Then, all we have to do is to pass that in as the options in our UIView.animate method. For our animation, we use the same curve as the keyboard animation, and it will all appear to work as one fluid motion rather than pieces moving a bit differently.

Interactive ScrollView

The final piece of extra credit we can do is to tell the ScrollView that its keyboardDismissMode is Interactive:

// The keyboard can be dismissed by dragging down (like in Messages)
scrollview?.keyboardDismissMode = .Interactive

In addition to Interactive, there is also OnDrag or None. None is the default, where dragging on the ScrollView doesn’t do anything. Interactive, is really interesting because it makes the keyboard work like it does in “Messages”, where you can drag down and up interactively to dismiss the keyboard. I can use my finger to push it off or pull it back on. That’s something that users will see in your app, and they’ll be impressed by how cool you are!

These techniques are somewhat specific to this form style that I’ve created here. Other things you might want to do are to use those same notifications in a similar method style with that calculation of the keyboard height, and maybe move things around in a different way. For example, I’ve done login forms where you just have two boxes. In those cases, it’s pretty easy to just move them, and you wouldn’t need the whole ScrollView.

Also, keyboards are becoming more complex. With the incorporation of third-party keyboards, the user could have a third-party keyboard, or the Apple built-in keyboard, or a hardware keyboard, so you really don’t want to make any assumptions about the size or location of the keyboard. As such, you want to do a really good job of trying to pull those values out of that notification, so that you’re always getting the exact value for the keyboard that’s about to appear, rather than trying to guess. A lot of older code still contains hard-coded values, and that’s not going to work for a special keyboard that the user might have.

Now that you’re armed with even more keyboard knowledge in iOS, go out and make some great apps! See you next time on TMI!

Additional Resources


This post is licensed under a Creative Commons BY-SA 3.0 license.


Michael Helmbrecht

Michael Helmbrecht

Michael designs and builds things: apps, websites, jigsaw puzzles. He's strongest where disciplines meet, and is excited to bring odd ideas to the table. But mostly he's happy to exchange knowledge and ideas with people. Find him at your local meetup or ice cream shop, and trade puns.