In this post I'll try to present a generic way to handle the UI modifications that should be preformed when the keyboard's appearing on the iPhone.
Everyone who writes apps for iPhone knows that there are some limits that comes with this device.
A major one is the screen size.
Let say you have this super killer app that wrote:

The app looks great and everything is ready to be shipped!
Lets try to login and see what's happend...

Yeah, this is a known issue in iPhone. When the keyboard is poped up there is no automatic mechanism that scroll the view so the user will still be able to push the button or see the text he's entering.
Even more annoying - unlike iPad,iPhone don't have "hide keyboard" button on the keyboard! So once you tap on a text field and the keyboard is hiding your "confirm" button, there is no way the get rid of the keyboard.
The recommended pattern by Apple is to put your entire ViewController's view inside another UIScrollView. Then you intercept the event when the keyboard is about to pop up (using your UITextField delegate methods) to scroll the content view by calculating the necessary offset.
This is a tidies task because you can have multiple ViewController in you app that contains text fields, and you need to calculate the offset for each one of them every time!
Because of this annoying issue I wanted to find a tool that will take care of that for me.
I noticed that there are two elements that are common among all of the cases that I could think of:
With this information we can think about a generic solution.
I wanted to create a sub-class of UIViewController that will take care of the keyboard issue automatically. All you need to provide it is the "Threshold View" that you want to always be visible, and it will make it happend.
So this is what i had in mind:
//
// KeyboardAwareViewController.h
// KeyboardAwareViewControllerExample
//
// Created by Avraham Shukron on 6/21/11.
// Copyright 2011 AppStudio and associates. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface KeyboardAwareViewController : UIViewController
{
UIScrollView *enclosingScrollView;
UIView *thresholdView;
UIToolbar *keyboardToolbar;
}
@property (nonatomic , retain) IBOutlet UIToolbar *keyboardToolbar;
@property (nonatomic ,retain) IBOutlet *thresholdView;
@property (nonatomic , assign) BOOL shouldUseDefaultToolBar;
-(IBAction) hideKeyboard;
@end
The Interface itself is very simple, and the logic will be on the implementation side of course.
The UIToolBar is there to provide the user the ability to hide the keyboard whenever he wants to.
We also want to give the developer the ability to decide if to show the toolbar or to provide his own mechanism.
The property shouldDisplayToolBar will determine is the our default toolbar will be shown or not.
Now we need a way to intercept the events of "Keyboard goes up" and "Keyboard goes down". But the challenge is to it without even knowing who is the UITextField that poped it, because we want a generic solution.
Luckily, there is a way to get notified when the keyboard is going up and down.
The way to do it is through the NSNotificationCenter, by registering to the UIKeyboardDidShowNotification and UIKeyboardWillHideNotification events.
It is always a good practice to use dedicated methods to register and un-register for notifications.
So the first thing to do will be to register our KeyboardAwareViewController to those notifications:
-(void) signInForNotifications
{
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone)
{
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(setupToolbar:) name:UIKeyboardWillShowNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(scrollForKeyboardLayout:) name:UIKeyboardDidShowNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(returnToNormalLayout:) name:UIKeyboardWillHideNotification object:nil];
}
}
-(void) resignFromNotifications
{
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardDidShowNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillHideNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillShowNotification object:nil];
}
Notice that we want to sign in for those notification, only if the code will run on iPhone.
On iPad there is no problem to hide the keyboard so we don't want to mess with the View Hierarchy for no reason.
Also notice the methods that we specified as handlers to those notifications:
The registration method will be called at viewDidLoad, and the resigning will be called at dealloc.
- (void)viewDidLoad
{
[super viewDidLoad];
[self signInForNotifications];
}
- (void)dealloc
{
self.firstResponder = nil;
[self resignFromNotifications];
[thresholdView release];
[enclosingScrollView release];
[keyboardToolbar release];
[super dealloc];
}
Now that we covered the area of the notifications, we can actually act on the events of the keyboard popping up and going down, in order to scroll the important portion of the screen to the visible area.
In order to do that we need to make sure that our ViewController's view is enclosed inside a UIScrollView. This is why we have the instance variable enclosingScrollView for.
Lets see how we should put the main view inside the scroll view:
-(void) viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPhone)
{
if (! self.enclosingScrollView.superview)
{
CGRect frame = self.view.frame;
self.enclosingScrollView = [[[UIScrollView alloc] initWithFrame:frame] autorelease];
self.enclosingScrollView.backgroundColor = [UIColor redColor];
self.enclosingScrollView.bounces = NO;
[self.enclosingScrollView setContentSize:self.view.frame.size];
frame.origin = CGPointZero;
self.view.frame = frame;
self.originalViewFrame = self.view.frame;
[self.view.superview addSubview:self.enclosingScrollView];
[self.enclosingScrollView addSubview:self.view];
}
}
}
Again, If we're on iPad there is no need for that so it's good to check.
What we doing in the code above is essentially taking the place of the main view with the scroll view, and then putting the main view inside the scroll view.
We can do it only at viewDidAppear because until then our view is not inside the window (although it's already created).
The toolbar was a little bit tricky. Why? Because if you want to add a toolbar to the keyboard you have to know who is the UITextField that poped that keyboard.
In order to add a toolbar you need to set the toolbar as the "inputAccessoryView" of the UITextField that is the first responder.
So the first problem is to know who is the first responder. You would think that getting the first responder is an easy task, but actually it is not, because Apple decided to hide it from programmer.
So i wrote my custom extension method that extend UIView:
-(UIResponder *) findFirstResponder
{
if (self.isFirstResponder)
{
return self;
}
for (UIView *subView in self.subviews)
{
UIResponder *maybe = [subView findFirstResponder];
if (maybe) return maybe;
}
return nil;
}
The extension method above let me to find the first responder if there is any. All we need to do is to call this method on the main Window and it will recursively look for that first responder.
Another thing is that you cannot assign the toolbar to the text field after the keyboard appeared. This is why we also intercepted the notification UIKeyboardWillShowNotification. When we get this notification we do the following:
-(void) setupToolbar : (NSNotification *) notification
{
// Finding the first responder
self.firstResponder = [self.view.window findFirstResponder];
if (self.firstResponder.inputAccessoryView)
{
// If it already has an inputAccessoryView we'll just set our Toolbar reference to that.
if ([self.firstResponder.inputAccessoryView isKindOfClass:[UIToolbar class]])
{
self.keyboardToolbar = (UIToolbar *)self.firstResponder.inputAccessoryView;
}
}
else
{
// The first responder don't have inputAccessoryView
if ([self.firstResponder respondsToSelector:@selector(setInputAccessoryView:)])
{
if (self.shouldUseDefaultToolBar || (self.keyboardToolbar == nil))
{
self.keyboardToolbar = [self defaultToolbar];
}
//If we can set the inputAccessoryView, That's what we'll do.
[(UITextField *)self.firstResponder setInputAccessoryView:self.keyboardToolbar];
[self.firstResponder reloadInputViews];
}
}
}
The default toolbar is created in this way:
#define TOOLBAR_WIDTH 320
#define TOOLBAR_HEIGHT 44
-(UIToolbar *) defaultToolbar
{
UIToolbar *toReturn = [[UIToolbar alloc] initWithFrame:CGRectMake(0, 0 - TOOLBAR_HEIGHT, TOOLBAR_WIDTH, TOOLBAR_HEIGHT)];
UIBarButtonItem *hideKeyboard = [[UIBarButtonItem alloc] initWithTitle:@"Cancel" style:UIBarButtonItemStyleDone target:self action:@selector(hideKeyboard)];
[toReturn setItems:[NSArray arrayWithObject:hideKeyboard]];
[hideKeyboard release];
return [toReturn autorelease];
}
Now let's move to the heavy stuff - the math that involved. What we want to do is basically display the important area at the remaining portion of the screen when the keyboard is up, but also let the user scroll the view in the small area available.
The method below is the handler for the KeyboardDidShow notification. The code is heavily commented so there is no point to repeat on that:
-(void) scrollForKeyboardLayout : (NSNotification *) notification
{
// The notification is invoked multiple times for some reason.
// This is to make sure that all the calculations will be done only once.
if (! self.isInKeyboardLayout)
{
self.isInKeyboardLayout = YES;
NSValue *keyboardFrameValue = [notification.userInfo objectForKey:UIKeyboardFrameEndUserInfoKey];
CGRect keyboardFrame = [keyboardFrameValue CGRectValue];
CGRect frame = self.enclosingScrollView.frame;
// Keep the original frame of our view so it'll stay the same.
// Now we are resizing the scroll view to fit the remaining area of the screen
frame.size.height -= keyboardFrame.size.height;
// Setting the resized frame
self.enclosingScrollView.frame = frame;
// Restoring the original frame to the view.
// This is done because sometimes when you change the frame
// of the superview, the subview's frame, might change too.
self.view.frame = self.originalViewFrame;
// Threshold point is the lowest point that we want to be visible.
// In other words it's the lowest point of the threshold view
float thresholdPoint = self.thresholdView.frame.origin.y + self.thresholdView.frame.size.height;
// Padding is just for more pleasant result to the eye.
float padding = 10.0;
// Offset is the amount of scrolling that need to be made
// in order to get the threshold point inside the visible area.
float offset = (thresholdPoint - keyboardFrame.origin.y) + padding;
// When the threshold point is already in the visible area the offset will be < 0.
// In that case we don't want to make any scrolling.
if (offset < 0) offset = 0;
// Based on the offset, now we create the rect for the visible area that we want to scroll to.
CGSize size = CGSizeMake(320, keyboardFrame.origin.y);
CGPoint origin = self.view.frame.origin;
/* ATTENTION
* This is the most important line. Here we are using the
* offset to scroll to the right position.
*/
origin.y += offset;
CGRect visible;
visible.origin = origin;
visible.size = size;
// Finally, Scroll!
if (visible.origin.y != self.view.frame.origin.y)
{
[self.enclosingScrollView scrollRectToVisible:visible animated:YES];
}
// Mark as finished so we won't do it twice and mess everything up.
}
}
Now, if we'll run the application we can see how it responds to the keyboard being presented:

We can see that the UITextField's and the submit button are now visible, and the user can even scroll the view (you will have to take my word for that - no screenshot can prove it...).
We also have a toolbar with a button that says "Hide keyboard". Now we will implement that.
-(IBAction) hideKeyboard
{
UIResponder *first = [self.view.window findFirstResponder];
[first resignFirstResponder];
}
In order to make the keyboard go away, we have to find the first responder, which is the object that the keyboard belongs to.
After we find the first responder we calling resingFirstResponder on it.
This will cause the keyboard to disappear, which in turn will invoke the KeyboardWillHideNotification.
We handle this notification in the following way:
-(void) returnToNormalLayout : (NSNotification *) notification
{
// Check if we are in keyboard layout.
if (self.isInKeyboardLayout)
{
NSValue *keyboardFrameValue = [notification.userInfo objectForKey:UIKeyboardFrameEndUserInfoKey];
NSNumber *animationDuration = [notification.userInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey];
CGRect keyboardFrame = [keyboardFrameValue CGRectValue];
// Now we will restore the frame to the original state.
CGRect frame = self.enclosingScrollView.frame;
frame.size.height += keyboardFrame.size.height;
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:[animationDuration floatValue]];
self.enclosingScrollView.frame = frame;
self.view.frame = self.originalViewFrame;
CGRect visible;
visible.origin = CGPointZero;
visible.size = self.view.frame.size;
[UIView commitAnimations];
// Scrolling back to original state
[self.enclosingScrollView scrollRectToVisible:visible animated:YES];
// Marking as "not in keyboard layout".
self.isInKeyboardLayout = NO;
self.firstResponder = nil;
}
}
Now everything should work!
Hej,
Liked that generic approach.
Can you provide some example code using the ‘KeyboardAwareViewController’
“Now, if we’ll run the application we can see how it responds to the keyboard being presented:”
Kindly,
willy
@Willy
You should take a look at the example project available here:
https://subversion.assembla.com/svn/keyboard-aware-viewcontroller/ (this is the SVN address so you can just checkout the project and take a look)
@Avraham
Thank’s. A lot!
I really appreciate your generosity.
- willy
- IMPORTANT – You should not present a subclass of KeyboardAwareViewController modally. There are some things with the Window that are just messed up.
You do not need a UIScrollView, rather you can do this yourself with an ordinary UIView by simply animating the frame of the view. Why have the overhead of a UIScrollView simply for this functionality? I don’t get it.
@Jacob – It’s actually very simple: When the keyboard appears, I want more than just scrolling to the right position, but also enable the user to scroll at the remaining area to whatever he wants. I don’t want the content position to be fixed ,rather I want it scrollable.
For example, if you have several UITextFields on the screen, so when the user will touch one of them, the scroll view will scroll to show that text field. but the user might want to enter the text for all the text fields without dismissing the keyboard. using scroll view enables this behavior.