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

20 thoughts on “iPad: Resize view to account for keyboard (updated)

  1. Thank you,

    Your code is very clear, and each step is nicely explained. That makes a huge difference.

    One issue I found (working on it): If you do scroll away from the field, and then dismiss the keyboard, you will be scrolled back to whence you came.

  2. Here’s a fix for the scrolling issue:

    Add this protocol to your view controller:
    UIScrollViewDelegate

    Make your view controller the delegate of your scroll view.

    Add this method to your view controller:

    – (void)scrollViewDidScroll:(UIScrollView *)scrollView
    {
    originalOffset = scrollingHost.contentOffset;
    }

    I still have an issue with the removal of the keyboard being a little to harsh, not smoothly animated at all, but I can live with that for now.

    Once again than you for sharing your nice, well-commented code with me and everybody else.

  3. Thanks, Anders, for contributing. I have worked on it even more myself and have a couple tweaks to add in to the post above … as soon as I get the time 🙂

  4. I’ve been trying to use this for my iPad app and having limited success. My text fields are buried in a view hierarchy, so that may be my problem. It goes as such: [[self view] -> scrollView -> editBoxView->UITextField.

    In a nav controller if I go to my view with that hierarchy, the view doesn’t scroll. As soon as I go to the next field, the view will scroll. Also, if the keyboard isn’t up and I tap in a field, it doesn’t scroll.

    Sugguestions?

    • Mark … when I get back in my office I’ll take a look at my code. Seems to me I ran into something similar and made some small adjustments. I’ll get back to you.

    • Here is the latest code from my project … take a look through it and let me know if any of it improves your situation. Otherwise, you may have to share some code with me of yours.

      // Get the size of the keyboard
      NSDictionary* info = [aNotification userInfo];
      CGRect kbRect = [[info objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue];
      kbRect = [self.view convertRect:kbRect toView:nil];

      // Shrink the scroll view
      UIEdgeInsets contentInsets = UIEdgeInsetsMake(0.0, 0.0, kbRect.size.height, 0.0);
      scrollView.contentInset = contentInsets;
      scrollView.scrollIndicatorInsets = contentInsets;

      // Get the visible frame
      CGRect aRect = self.view.frame;
      aRect.size.height -= kbRect.size.height; // Adjust for the keyboard
      aRect.size.height -= self.toolbar.frame.size.height; // Adjust for the toolbar

      // Get the selected field’s frame
      UIView *activeField = … your view here…
      aRect.size.height -= activeField.frame.size.height; // Adjust for the size of the view
      CGPoint fieldOrigin = activeField.frame.origin;
      fieldOrigin = [activeField.superview convertPoint:fieldOrigin toView:self.view];
      fieldOrigin.y -= [scrollView convertPoint:scrollView.contentOffset toView:self.view].y; // Adjust for the scroll position

      originalOffset = scrollView.contentOffset;
      if (!CGRectContainsPoint(aRect, fieldOrigin) ) {
      [scrollView scrollRectToVisible:[activeField.superview convertRect:activeField.frame toView:scrollView] animated:YES];
      }

      • Thank you for posting this (11 June) code and earlier explanations: it is just what I need. But there is a problem when running on iPhone 4.2.3 and iPhone simulator 4.3, both in portrait mode (landscape not enabled and the iPad version does not have the need for this):

        I have a scrollView in a FlipSideViewController with 8 textFields in 4 rows of two. When the alpha keyboard appears when clicking in any field, the bottom two rows are covered by the keyboard. One can scroll up to view the covered fields.
        With the code above nothing happens: the final if test always fails.
        If I remove

        fieldOrigin.y -= [scrollView convertPoint:scrollView.contentOffset toView:self.view].y; // Adjust for the scroll position

        the whole thing works just as advertised: the first two rows never move, the bottom two rows are scrolled up so that the clicked field is at the top of the scrollView and visible. I notice that this line was not present in the original code.

        In addition, there appears to be no such thing as a toolbar in a FlipSideViewController so I commented that out too.

        Thank you too to Anders for his code from 11 May 2011

      • Two things to add:
        1) Further testing (!) in the above scenario shows that once the hidden field has scrolled to the top of the scrollView and is visible and I start to type, the field then scrolls again off the top of the scrollview so I have to scroll it back down to continue typing!
        Not yet sure what’s happening here. The method scrollViewDidScroll is invoked four times by the time the first character is typed – two before and two after.

        2) I find that with Anders’ code in place, with my scenario, if I click on any of the fields in the last two rows and then dismiss the keyboard, only the last two rows are displayed at the top of the scrollview – even if I clicked on a field in the last row. It does not return to showing 4 rows.
        If I scroll after clicking on any field in the last two rows and then dismiss the keyboard, no scrolling occurs and I am left seeing just those fields visible after I scrolled – which may be what Anders code is trying to achieve. But it has the (unwanted in my view) side effect of not returning to the original starting point if I did not scroll after clicking in a field.

      • Further to the phantom scrolling problem above, I changed the code at the end of the method to the following and it seems to work for me. I realise this may not be optimal but…

        if (!CGRectContainsPoint(aRect, fieldOrigin) ) {
        //[scrollView scrollRectToVisible:[activeField.superview convertRect:activeField.frame toView:scrollView] //animated:YES];
        fieldOrigin = activeField.frame.origin;
        fieldOrigin.x = 0.0;
        fieldOrigin.y -= 20.0;
        [scrollView setContentOffset:fieldOrigin animated:YES];
        }

  5. How do you set the activeField?

    I am setting it like

    activeField = [[UIView alloc] initWithFrame:textField.frame];

    biut then activeField.superview is null.

    • It looks like you are creating a brand new textField but not actually adding it to another view somewhere. If it isn’t on a view, it won’t have a superview and won’t show anywhere. In my code I am actually wired up to every text field’s “text did begin edit” notification and I set the active field each time a text field starts editing. Let me know if that helps!

  6. Jason, that is correct.
    I finally used your original code.

    I forgot to set the scroll view content size. If it is not set it won’t scroll.

    [self.myScrollView setContentSize:CGSizeMake(768,2000)];

    I still need to fix it for orientation changes.

    Thanks for your help ant tutorial.

  7. Hey Thank you – you saved my day.
    I forgot the [self.myScrollView setContentSize:CGSizeMake(768,2000)]; as well which took me an hour to nice – thanks again.

    Bjarke
    Denmarkl

    • The code I have is integrated with my projects and wouldn’t make sense to post here other than the examples above. When I get a chance, sometime, perhaps I’ll create a demo project and post that.

  8. Nice, thank you.

    One tweak: I changed the detection logic to make sure that the control’s entire frame is within view, not just its origin point. I had one textfield line up just peeking above the keyboard enough to see the cursor, but not the text 🙂

    CGRect controlRect = CGRectMake(fieldOrigin.x, fieldOrigin.y, activeField.frame.size.width, activeField.frame.size.height);
    if (!CGRectContainsRect(aRect, controlRect) ) {
    [controlsScrollView scrollRectToVisible:activeField.frame animated:YES];
    }

  9. Jason, came across your solution nice complete implementation with very good comments!

    I’m using UIDatePicker instead of the normal keyboard for editing a UITextField, and I don’t have a toolbar, but this still solved my problem nicely.

    Thank you!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s