iPad: Resize view to account for keyboard (updated)

UPDATE (3/9/2011):

There were a couple of glitches in the original code so I found a nicer way to handle the scrolling as updated below!

Classic problem

In an iOS application the keyboard slides up and hides the view/field that the user is editing.  There are a ton of solutions out there but there are problems with just about all of them when it comes to the iPad.  Even more of an issue is the stock “Splitview” applications that we can implement using other UI views like a UIScrollView.

Requirements

First, here is a list of functions that I wanted to implement:

  • Works with a UISplitViewController
  • Works with a UIScrollView
  • Ensures the field being edited is visible
  • Uses smooth animation, not jumpy
  • Looks like a iPad application should behave
The Apple Way

What better way to implement this than to use Apple’s documentation, right? Well, as described in this document Apple has a great approach.  However, there were several problems with this implementation:

  • It was written for an iPhone
  • It does not take into account the rotation of the device
  • It does not take into account the scroll view’s current position
  • It does not take into account the split view design

However, what I do like about is that it takes advantage of simple code with minimal state management and just needed some tweaks.

Let me break it down.

First, when getting the size of the keyboard the following lines of code do NOT take in to account the rotation of the device:

NSDictionary* info = [aNotification userInfo];
CGRect kbRect = [[info objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue];

The good news is there is an easy way to fix this by just translating the CGRect using the current view’s convertRect method:

kbRect = [self.view convertRect:kbRect toView:nil];

Now the rectangle will be oriented correctly no matter which way the device is turned.  Ok … problem one solved.  Next up, the code didn’t take into account the split view controller design pattern. First, when calculating the CGRect which determined what was the “visible” area of the screen it ignored the toolbar as well as fact that the “detail view” was offset by the “master view” when in landscape view.

Accounting for the toolbar was easy by modifying this code:

CGRect aRect = self.view.frame; aRect.size.height -= kbRect.size.height;

… by taking into account the size of the toolbar:

aRect.size.height -= self.toolbar.frame.size.height;

Now, in order to determine if the currently selected field is visible, it’s coordinates must be translated to the same system the detail view is in.  The stock code gets the active field’s coordinates as follows:

CGPoint fieldOrigin = activeField.frame.origin;

… then as before I need to translate to a comparable system:

fieldOrigin = [self.view convertPoint:fieldOrigin toView:self.view.superview];

… and take into account the current scroll position of the UIScrollView (thanks DK_Donuts) and I end up with:

CGPoint fieldOrigin = activeField.frame.origin;
fieldOrigin.y -= scrollView.contentOffset.y;
fieldOrigin = [self.view convertPoint:fieldOrigin toView:self.view.superview];

Now we use this magical method: scrollRectToVisible and the scroll view takes over for us:

[scrollView scrollRectToVisible:activeField.frame animated:YES];

As far as making the positioning smooth and behaving like an iPad app, I need to save the original offset …

originalOffset = scrollView.contentOffset;

…and then reset the position when the keyboard is hidden.

[scrollView setContentOffset:originalOffset animated:YES];

The animation smooths it all out. Finally … something that works the way I expected!

The Code

Below is the listing of the code as modified to work as I needed it.

1. Add some instance variables

CGPoint originalOffset;
UIView *activeField;

2. Create the keyboard notification handlers

- (void)keyboardWasShown:(NSNotification*)aNotification
{
    NSDictionary* info = [aNotification userInfo];
    CGRect kbRect = [[info objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue];
	kbRect = [self.view convertRect:kbRect toView:nil];
    UIEdgeInsets contentInsets = UIEdgeInsetsMake(0.0, 0.0, kbRect.size.height, 0.0);
    scrollView.contentInset = contentInsets;
    scrollView.scrollIndicatorInsets = contentInsets;

    CGRect aRect = self.view.frame;
    aRect.size.height -= kbRect.size.height;
    aRect.size.height -= self.toolbar.frame.size.height;
    CGPoint fieldOrigin = activeField.frame.origin;
    fieldOrigin.y -= scrollView.contentOffset.y;
    fieldOrigin = [self.view convertPoint:fieldOrigin toView:self.view.superview];
    originalOffset = scrollView.contentOffset;
    if (!CGRectContainsPoint(aRect, fieldOrigin) ) {
        [scrollView scrollRectToVisible:activeField.frame animated:YES];
    }
}

… and …

- (void)keyboardWillBeHidden:(NSNotification*)aNotification {
     UIEdgeInsets contentInsets = UIEdgeInsetsZero;
     scrollView.contentInset = contentInsets;
     scrollView.scrollIndicatorInsets = contentInsets;
     [scrollView setContentOffset:originalOffset animated:YES];
 }

3. Wire up the notifications in viewDidLoad

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

4. It is up to you to wire up the activeField ivar depending on your implementation.

Advertisements