The keyboard is present in nearly every
application out there. Using the keyboard is the easiest way to provide users
with a way to input alphanumeric data into applications. Trivial as it might
look like in the beginning, a correct implementation of keyboard behavior can
be a costly endeavor. Multiple keyboard states make it hard to implement logic
that will behave correctly for all of them.
This post will cover my observations and
experiences with the keyboard system in iOS. I will try to describe some basic
concepts behind the keyboard notification system and take a closer look at the
order in which notifications are sent.
Keyboard System
There are two main patterns across the
Objective-C/Cocoa framework that give the user an idea of how the communication
process between different objects functions – the delegation pattern and the
notifications pattern.
The public keyboard API is built around the
latter. You just inform the NSNotificationCenterobject that you want to receive some specific notifications. These
notifications are later sent to you from somewhere else within the application
when a specific type of event occurs.
Given that notifications are a generic
pattern, there has to be a way to utilize them so they provide as much
information as we want them to. Moreover, this information can be represented
by a different number of objects which can, in turn, represent various types.
For this particular purpose there is auserInfo property in the NSNotification class.
This dictionary provides the elements listening for notifications with an
additional context behind the triggering of the notification.
For keyboard notifications, we use the [NSNotificationCenter defaultCenter] method to get an instance of NSNotificationCenter class, which will be used for the notification’s
registration process.
[[NSNotificationCenter
defaultCenter] addObserver:self selector:@selector(keyboardWillShow:)
name:UIKeyboardWillShowNotification object:nil];
And that’s all you need to start receiving
notifications about the appearance of the keyboard. Well, what next?
Keyboard’s Notification
To put things bluntly, the iOS keyboard can be
very annoying. Despite being really simple from the user’s perspective, in
multiple cases the logic of the application suffers from either the lack or the
misinterpretation of the system’s notifications. Below is a list of the
keyboard’s notifications present in iOS:
·
UIKeyboardWillShowNotification –
notification sent when the keyboard is about to show.
·
UIKeyboardDidShowNotification –
notification sent when the keyboard has just appeared.
·
UIKeyboardWillHideNotification –
notification sent when the keyboard is about to hide.
·
UIKeyboardDidHideNotification –
notification sent when the keyboard has just been hidden.
·
UIKeyboardWillChangeFrameNotification-
notification sent when the kebyoard’s frame is about to change.
·
UIKeyboardDidChangeFrameNotification –
notification sent when the keyboard’s frame has just changed.
If we take a look at the names of keyboard’s
notifications it is pretty easy to think that the typical (maybe the only
correct) ‘path’ for these notifications looks like this:
In fact, these two are the most common, but
when defining your application logic you can’t always expect to get all these
notifications in the presented order. It is entirely normal not to get some of
them in specific circumstances or to get them in a different order. Since we
want our applications to provide the best user experience possible, we should
know about every possible order that they may have to deal with.
UserInfo Dictionary
The UserInfo property of the NSNotification class is the only (public) way to get the keyboard frame
and some specific information about the keyboard’s animation. Every
notification gives the developer a snapshot of the keyboard’s present or future
state and allows us to update the state of the application. These are the
properties of a userInfo dictionary which are passed with every keyboard
notification:
·
UIKeyboardFrameBeginUserInfoKey –
frame of the keyboard at the beginning of the current keyboard state change.
·
UIKeyboardFrameEndUserInfoKey –
frame of the keyboard at the end of the current keyboard state change.
·
UIKeyboardAnimationDurationUserInfoKey –
duration of the animation used to animate the change of the keyboard state.
·
UIKeyboardAnimationCurveUserInfoKey –
animation curve used to animate the change of the keyboard’s state.
UIKeyboardFrameBeginUserInfoKey and UIKeyboardFrameEndUserInfoKey are two most important and probably most commonly used
elements of the userInfo dictionary. That being said, there is one important
thing to remember. Coordinates ‘hidden’ behind these keys don’t take rotation
factors applied to the window into account, so their values can seem be wrong.
It is really important to remember to use convertRect:fromWindow or convertRect:fromView to make sure we work on proper keyboard coordinates.
The two following properties are most commonly
used when we want to respond to a keyboard animation with our own animation. If
we have the values for both the length of the animation’s duration and the
curve used for the keyboard’s animation, we can use them to create our own
animation, which will sync nicely with the keyboard’s own animation.
Responding to Notifications
Let’s imagine we have a view which is an UITableView object and its frame is equal to the screen bounds. In
that particular case, the appearance of the keyboard will cause the bottom part
of theUITableView content to be obscured by the keyboard frame. Moreover,
the user won’t be able to scroll down to the bottom part of the content view
which is put in table view. This is not the kind of experience we want to
provide to our users.
We can handle this case in few different ways.
The majority (if not all) of them require using keyboard notifications. We just
listen for the appropriate notification and change some properties of our view
hierarchy, so that user gets to see the entire content of the table view.
Earlier in this post, we added ourselves as
the listeners for UIKeyboardWillShowNotificationusing the addObserver:selector:name:object method of the NSNotificationCenter class. Now, we can expand that code and add a method which
will be called after the notification is received:
-(void)keyboardWillShow:(NSNotification
*)notification {
CGRect
keyboardEndFrame = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
UIViewAnimationCurve curve =
[notification.userInfo[UIKeyboardAnimationCurveUserInfoKey] unsignedIntegerValue];
UIViewAnimationOptions options =
(curve << 16) | UIViewAnimationOptionBeginFromCurrentState;
NSTimeInterval duration =
[notification.userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
CGRect frame =
CGRectInset(self.tableView.frame, 0, CGRectGetHeight(keyboardEndFrame));
[UIView animateWithDuration:duration delay:0.0 options:options animations:^{
self.tableView.frame
= frame;
} completion:nil];
}
This code changes the frame of the table view
so that it occupies only that part of the screen which is not hidden behind the
keyboard frame. Furthermore, it changes the frame using an animation that
perfectly matches the keyboard animation. The animation's duration and its
curve are identical to the one used to show the keyboard, so the user shouldn't
even notice any changes to the table view frame. Just don't forget to return
the tableView frame to its initial state after the keyboard gets hidden!
Short Summary
After reading our introduction to the
notification system, you should know how to listen for or respond to keyboard
notifications, you’re well-versed in the keyboard's userInfo dictionary, and spelling the names of all of the keyboard
notifications is not a problem anymore.
Well, you may think that you now know
everything to properly work with a keyboard. In theory that might be true, but
in practice your application can sometimes exhibit some unexpected behaviors.
You still don't know the order in which notifications are sent and which of
them can be absent in specific circumstances. Try to think for a moment about
what will happen when there’s an external keyboard connected to the iPad? Not
that obvious, isn’t it?
In the following parts of this post, I’d like
to explore the notifications that occur when we change the state of the
keyboard, e.g. docking the keyboard, showing the keyboard when there’s an
external one connected to the device, etc. Additionally, I’ll try to take a
close look at the values included in theuserInfo dictionary. Finally, I’ll provide some code that will try
to encapsulate some system notifications and broadcast my own. The latter
notifications will have names equivalent to the system ones but will be sent in
different circumstances.
Keyboard Magic
In iOS 5, Apple added a new feature to the
system keyboard. Clicking and holding the keyboard button at the bottom right
corner of the keyboard brings up a popup which allows you to merge/unmerge and
dock/undock you keyboard. You can even move the undocked keyboard by holding
and panning the abovementioned keyboard button along the screen.
Dragging keyboard triggers UIKeyboardWillChangeFrameNotification andUIKeyboardDidChangeFrameNotification notifications. First of them is send when user starts
dragging and the second one after dragging is finished. Because of the fact
system doesn't know what will be value of the UIKeyboardFrameEndUserInfoKey key at the beginning of the dragging, value set for this
key is equal to CGRectZero in UIKeyboardWillChangeFrameNotification's dictionary. Moreover, value of the UIKeyboardFrameBeginUserInfoKey key is equal toCGRectZero in UIKeyboardDidChangeFrameNotification.
You don't get UIKeyboardWillShowNotification and UIKeyboardDidShowNotificationnotifications when a non-standard keyboard is being shown.
Hiding the keyboard doesn't triggerUIKeybardWillHideNotification and UIKeyboardDidHideNotification notifications. Still, UIKeyboardWillChangeFrameNotification andUIKeyboardDidChangeFrameNotification notifications work as expected and can be used to imitate
other ones (to detect when a non-standard keyboard is being shown or hidden).
How to imitate UIKeyboardWillShowNotification
and UIKeyboardDidShowNotification when showing unmerged/undocked keyboard?
1.
Check whether UIKeyboardDidChangeFrameNotification was
not preceded byUIKeyboardWillShowNotification.
2.
Check whether the rectangle from userInfo's UIKeyboardFrameEndUserInfoKey is
within screen bounds.
If both of these conditions are met, the
keyboard will be shown in the unmerged/undocked state. Imitating UIKeyboardWillHideNotification and UIKeyboardDidHideNotificationnotifications obviously works in almost the same way, albeit
with slightly different logic. You just need to switch “within” from condition
no. 2 to “not within” and you’re set. Here is the code:
- (void)keyboardWillShow:(NSNotification
*)notification {
self.standardKeyboard
= YES;
}
- (void)keyboardWillHide:(NSNotification
*)notification {
self.standardKeyboard
= YES;
}
- (void)keyboardDidHide:(NSNotification *)notification { self.standardKeyboard
= NO; } - (void)keyboardDidShow:(NSNotification
*)notification { self.standardKeyboard = NO; } - (void)keyboardWillChangeFrame:(NSNotification
*)notification { CGRect endFrame = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue]; UIWindow *window = [[[UIApplication sharedApplication] windows] firstObject];
endFrame = [window convertRect:endFrame fromWindow:nil];
if(CGRectContainsRect(window.frame,
endFrame) && !self.standardKeyboard) {
[[NSNotificationCenter
defaultCenter] postNotificationName:MCSKeyboardWillShowNotification object:nil
userInfo:notification.userInfo];
} else if (!self.standardKeyboard)
{
[[NSNotificationCenter
defaultCenter] postNotificationName:MCSKeyboardWillHideNotification object:nil
userInfo:notification.userInfo];
}
[[NSNotificationCenter defaultCenter] postNotificationName:MCSKeyboardWillChangeFrameNotification object:nil userInfo:notification.userInfo];
}
- (void)keyboardDidChangeFrame:(NSNotification
*)notification {
CGRect endFrame =
[notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
UIWindow *window = [[[UIApplication sharedApplication] windows] firstObject];
endFrame = [window convertRect:endFrame fromWindow:nil];
if
(CGRectContainsRect(window.frame, endFrame) && !self.standardKeyboard)
{
[[NSNotificationCenter
defaultCenter] postNotificationName:MCSKeyboardDidShowNotification object:nil
userInfo:notification.userInfo];
} else if (!self.standardKeyboard)
{
[[NSNotificationCenter
defaultCenter] postNotificationName:MCSKeyboardDidHideNotification object:nil
userInfo:notification.userInfo];
}
[[NSNotificationCenter defaultCenter]
postNotificationName:MCSKeyboardDidChangeFrameNotification object:nil
userInfo:notification.userInfo];
}
Toggling Dock and Merge State
Toggling the keyboard's dock and merge states
causes the notifications to be sent in slightly different order than when the
keyboard’s appearing or hiding:
If we dig deeper into userInfo properties of
keyboard notifications, we’ll come upon even more weirdness than we expected.
When unmerging or undocking the keyboard, the value forUIKeyboardFrameEndUserInfoKey in the userInfo dictionary is different inUIKeyboardDidHideNotification than in all the other notifications. The value of this key
taken from UIKeyboardDidChangeFrameNotification suggests that the keyboard will be placed either in the
screen or beyond it when other notifications suggest just the opposite. Also,UIKeyboardDidChangeFrameNotification is delivered with theUIKeyboardFrameBeginUserInfoKey key of the userInfo dictionary equal to CGRectZero.
Furthermore, docking or merging results in the
sending of theUIKeyboardDidChangeFrameNotification notification with the value for theUIKeyboardFrameBeginUserInfoKey key equal to CGRectZero.
Splitting/Merging When Keyboard Is Undocked
Assuming the keyboard is in an undocked state,
merging and splitting the keyboard makes the notification system send
notifications in the following order:
That's right - UIKeyboardDidChangeFrameNotification is sent twice although there are no differences in
keyboard frames passed in the userInfo dictionary. Also, do be careful with these frames - they
sometimes represent pretty useless values. UIKeyboardFrameEndUserInfoKey is equal to CGRectZero in UIKeyboardDidChangeFrameNotification andUIKeyboardFrameBeginUserInfoKey is equal to CGRectZero inUIKeyboardWillChangeFrameNotification. Why twoUIKeyboardDidChangeFrameNotification notifications? The only possible reason I found for that
is captured on the screenshot underneath.
It seems that during the merging/splitting
animation, there is change in the keyboard view/frame, and that’s probably the
reason behind having two UIKeyboardDidChangeFrameNotificationnotifications. Just don't forget these notifications don’t
always occur on a one-to-one basis!
Multiple Interface Orientations - Device Rotation
Device rotation is pretty weird when we look
at it from the keyboard notification perspective. It can be really frustrating
and hard to notice. Let's say your application supports multiple interface
orientations and users start to rotate the device when they keyboard is
displayed on the screen. What happens then? If we take a closer look at the
keyboard animation which appears when we rotate the screen, we’ll quickly see
that there’s nothing special to it.
But even a cursory glance at the notification
logs shows us that something weird is happening. It looks like the rotation of
the device makes the keyboard disappear and appear again. It’s almost
imperceptible from the user’s perspective, but this notification behavior can
have serious implications for the logic of the application. It looks like the
easiest way to detect these notifications is to implement two templates methods
of the UIViewController.
Assuming that they were implemented, this is
the true order for notifications/method calls for device rotation:
1.
willRotateToInterfaceOrientation:duration: -
template method
2.
UIKeyboardWillHideNotification
3.
UIKeyboardDidHideNotification
4.
UIKeyboardWillShowNotification
5.
didRotateToInterfaceOrinetation:duration: -
template method
6.
UIKeyboardDidShowNotification
A simple flag set in willRotateToInterfaceOrientation:duration: (and unset indidRotateToInterfaceOrientation:) lets us ignore three out of the four notifications which
appear during device rotation. The only one left is pretty easy to detect - if
the keyboard is displayed and you get UIKeyboardDidShowNotification, don't do anything.
-(void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
self.interfaceRotation = YES;
}
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
{
self.interfaceRotation
= NO;
}
- (void)keyboardWillShow:(NSNotification
*)notification {
if (self.interfaceRotation)
{
return;
}
[[NSNotificationCenter defaultCenter] postNotificationName:MCKeyboardWillShowNotification object:nil userInfo:notification.userInfo];
}
- (void)keyboardDidShow:(NSNotification *)notification {
if (self.interfaceRotation)
{
return;
}
[[NSNotificationCenter defaultCenter] postNotificationName:MCKeyboardDidShowNotification object:nil userInfo:notification.userInfo];
}
- (void)keyboardWillHide:(NSNotification
*)notification {
if (self.interfaceRotation)
{
return;
}
[[NSNotificationCenter defaultCenter] postNotificationName:MCKeyboardWillHideNotification object:nil userInfo:notification.userInfo];
}
- (void)keyboardDidHide:(NSNotification *)notification {
if (self.interfaceRotation)
{
return;
}
[[NSNotificationCenter defaultCenter] postNotificationName:MCKeyboardDidHideNotification object:nil userInfo:notification.userInfo];
}
External Keyboard
Additional changes take place when we connect
an external keyboard to the iPad. In this particular case, the notification
behavior depends on the inputAccessoryView property of the control which was the reason for
displaying the keyboard.
Let's say we have a UITextField or UITextView object and we set its inputViewAccessoryproperty. Assuming this object becomes a first responder, the
view assigned to inputAccessoryView will be displayed above the keyboard on the
screen. This enables programmers to provide a customized keyboard experience to
the end users of the application.
If inputAccessoryView is not present or its height is equal to 0 points,
no keyboard notifications are sent. My guess is that this is because in this
case, no visual changes take place in application. Otherwise, all notifications
behave as expected – which means they are being sent as in the majority of
cases when the keyboard is displayed or hidden in a normal (not undocked or
split) state.
What about the keyboard frame when inputAccessoryView is coupled with the currently displayed keyboard? Luckily
for us, frames passed through keyboard notifications seem to take the displayed
input view into account. That means keyboard frames passed in the objectInfo dictionary are unions of the keyboard's frame itself and
the frame of the input view. Additionally, when there is an external keyboard
hooked up to the device and only the accessory view is displayed, the
keyboard's frame is the union of the two abovementioned frames (although the
keyboard itself is not visible).
Keyboard Visibility
Due to the fact that there are so many states
in which hiding or displaying the keyboard can take place, retaining a value
that would inform us whether the keyboard is currently displayed on screen or
not is not that obvious (and by displayed I mean visible to the user).
The easiest solution assumes the creation of a
bool property which is later updated each time the system sends a UIKeyboardDidShowNotification or UIKeyboardDidHideNotificationnotification. Although really intuitive, this solution doesn't
work properly when the keyboard is either undocked or unmerged.
-(void)keyboardDidShow:(NSNotification *)notification {
self.keyboardVisible
= YES;
}
-(void)keyboardDidHide:(NSNotification *)notification {
self.keyboardVisible
= NO;
}
A better approach to this problem is to update
the bool property after we receive aUIKeyboardDidChangeFrameNotofication notification. Just removeUIKeyboardFrameEndUserInfoKey from the notification's userInfo property and check whether it is contained within screen
bounds.
-(void)keyboardDidChangeFrame:(NSNotification
*)notification{
CGRect endFrame =
[[notification.userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
UIWindow *window = [[[UIApplication sharedApplication] windows] firstObject];
endFrame = [window convertRect:endFrame fromWindow:window];
self.keyboardVisible
= CGRectContainsRect(window.frame, endFrame);
}
Notice that we’re using the CGRectContainsRect function instead of CGRectIntersectsRect.
This will ensure that the keyboardVisible property will be equal to YES only
when the entire keyboard is visible, and by “entire keyboard” I mean keyboard +
input accessory view. If you want this property to be equal to YES even
when only the input accessory view is visible (when an external keyboard is
connected to the device), use CGRectIntersectsRect instead.
Rultech Blog - wordpress
Rultech Blog - blogspotRultech Knowledge Base
Pro!deaClub Blog
Pro!deaClub - blogspot
This is really very helpful. :)
ReplyDelete