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
- RPDP and Community’s answers to “How to make a UITextField move up when keyboard is present”
- Keyboard Notification by Kristopher Johnson
- Little Bites of Cocoa: Keyboard Notifications
This post is licensed under a Creative Commons BY-SA 3.0 license.
Receive news and updates from Realm straight to your inbox