Maintain UITableView scroll position with keyboard expansion

Want to maintain the scroll position of your UITableView when the Keyboard expands? Well look no further. This is an article that will discuss how to use the Keyboard events to retain the scroll position in your table.

For a working example you can refer to the Github Repository:
https://github.com/bmancini55/iOSExamples-BottomScrollPosition

Keyboard Closed Keyboard Open

Keyboard Event Background

The starting point for maintaining scroll position is the use of NotificationCenter to monitor for Keyboard events. Primarily we will be working with UIKeyboardWillChangeFrameNotification, which is triggered before changes are made to the Keyboard's frame. There are several other Notifications that can be triggered for Keyboard events:

UIKeyboardWillShowNotification  
UIKeyboardDidShowNotification  
UIKeyboardWillHideNotification  
UIKeyboardDidHideNotification  
UIKeyboardWillChangeFrameNotification  
UIKeyboardDidChangeFrameNotification  

Each of these is described in detail the UIWindow Class Reference if you get curious and want to do more hacking. But for this example, we will only need to use the UIKeyboardWillChangeFrameNotification event.

Each of these events uses the Keyboard Notification Info to pass event information to the event handler. These consists of the following keys:

UIKeyboardFrameBeginUserInfoKey  
UIKeyboardFrameEndUserInfoKey  
UIKeyboardAnimationDurationUserInfoKey  
UIKeyboardAnimationCurveUserInfoKey  

UIKeyboardFrameBeginUserInfoKey contains the starting frame for the Keyboard event.

UIKeyboardFrameEndUserInfoKey contains the ending frame for the keyboard event. Combined with the starting frame we can determine what the change to the keyboard is going to be.

Below is some sample output for these values.

Open Keyboard  
Begin: { Origin: {X: 0, Y: 568}, Size: {W: 320, H: 253}}  
End:   { Origin: {X: 0, Y: 315}, Size: {W: 320, H: 253}}  
Close Keyboard  
Begin: { Origin: {X: 0, Y: 315}, Size: {W: 320, H: 253}}  
End:   { Origin: {X: 0, Y: 568}, Size: {W: 320, H: 253}}  

You can see that the origin's Y values are changing. This is what we will use to determine the change in Keyboard location.

UIKeyboardAnimationDurationUserInfoKey and UIKeyboardAnimationCurveUserInfoKey contain animation information that can be used to construct our animation.

These keys are defined in more detail in the documentation if you want to read more.

Event Handlers

To start with you will need to attach and remove the UIKeyboardWillChangeFrameNotification in your TableViewController.

You can do this in the viewWillAppear and viewDidDisappear methods respectively.

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];

    // Add a notification for keyboard change events
    [[NSNotificationCenter defaultCenter] addObserver:self
                                             selector:@selector(keyboardWillChange:)
                                                 name:UIKeyboardWillChangeFrameNotification
                                               object:nil];
}

- (void)viewDidDisappear:(BOOL)animated
{
    [super viewDidDisappear:animated];

    // Remove notification for keyboard change events
    [[NSNotificationCenter defaultCenter] removeObserver:self name:UIKeyboardWillChangeFrameNotification object:nil];
}

These methods will use keyboardWillChange handler to process the information.

// This will be called each time there is a frame change for the keyboad
// We can use the begin/end values to determine how the keyboard is going to change
// and apply appropriate scrolling to our table
-(void)keyboardWillChange:(NSNotification *)notification
{
    // Retrieve the keyboard begin / end frame values
    CGRect beginFrame = [[notification.userInfo objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue];
    CGRect endFrame =  [[notification.userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
    CGFloat delta = (endFrame.origin.y - beginFrame.origin.y);
    NSLog(@"Keyboard YDelta %f -> B: %@, E: %@", delta, NSStringFromCGRect(beginFrame), NSStringFromCGRect(endFrame));

    // Lets only maintain the scroll position if we are already scrolled at the bottom
    // or if there is a change to the keyboard position
    if(self.tableView.scrolledToBottom && fabs(delta) > 0.0) {

        // Construct the animation details
        NSTimeInterval duration = [[notification.userInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue];
        UIViewAnimationCurve curve = [[notification.userInfo objectForKey:UIKeyboardAnimationCurveUserInfoKey] unsignedIntegerValue];
        UIViewAnimationOptions options = (curve << 16) | UIViewAnimationOptionBeginFromCurrentState;

        [UIView animateWithDuration:duration delay:0 options:options animations:^{

            // Make the tableview scroll opposite the change in keyboard offset.
            // This causes the scroll position to match the change in table size 1 for 1
            // since the animation is the same as the keyboard expansion
            self.tableView.contentOffset = CGPointMake(0, self.tableView.contentOffset.y - delta);

        } completion:nil];
    }
}

This method will do a few things. First it will retrieve the begin and end frame values for the event. From these CGRect values it can calculate the change in frame position for the Keyboard.

In the code shown it will check a Category called scrolledToBottom to determine if the the tableView is at the bottom position (these will be covered later). It will also check that the there is a change in the keyboard's frame. If these two conditions are met, we will continue.

The next part of the code will retrieve the animation information used by the keyboard event. We can mimic this animation to ensure our scrolling happens in the same manner as the keyboard movement.

In order to scroll, we create our animation block and change the contentOffset for the tableView. We adjust the content offset in the opposite direction as the keyboard movement. This allows the content stay in the same position. Voila!

Note: This example uses UITableViewController. This means that the frame of the UITableView will automatically adjust when the keyboard expansion occurs so that the content of the UITableView does not sit behind the keyboard.

If you are using a UIViewController, you will not have this automatically baked in and will need to adjust the UITableView's frames accordingly. The example below shows how you might do this:

// This will be called each time there is a frame change for the keyboard
// and will adjust the frame so that it does not sit behind the keyboard
-(void)keyboardWillChange:(NSNotification *)notification
{
    // Construct the animation details
    CGRect endFrame =  [[notification.userInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];      
    NSTimeInterval duration = [[notification.userInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue];
    UIViewAnimationCurve curve = [[notification.userInfo objectForKey:UIKeyboardAnimationCurveUserInfoKey] unsignedIntegerValue];
    UIViewAnimationOptions options = (curve << 16) | UIViewAnimationOptionBeginFromCurrentState;

    // Create new table Frame
    CGRect frame = CGRectInset(self.tableView.frame, 0, CGRectGetHeight(endFrame));

    // Adjust table frame with matching animation
    [UIView animateWithDuration:duration delay:0 options:options animations:^{
        self.tableView.frame = frame;        
    } completion:nil];  
}

Scroll Helpers

As mentioned before, this example uses scroll helpers defined in a Category for UITableView. These will assist and scrolling to the bottom of a table view and determining if it is already scrolled to the bottom:

// Returns true if the table is currently scrolled to the bottom
- (bool) scrolledToBottom
{
    return self.contentOffset.y >= (self.contentSize.height - self.bounds.size.height);
}


// Scrolls the UITableView to the bottom of the last row
- (void)scrollToBottom:(BOOL)animated
{
    NSInteger lastSection = [self.dataSource numberOfSectionsInTableView:self] - 1;
    NSInteger rowIndex = [self.dataSource tableView:self numberOfRowsInSection:lastSection] - 1;

    if(rowIndex >= 0) {
        NSIndexPath *indexPath = [NSIndexPath indexPathForRow:rowIndex inSection:lastSection];
        [self scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionBottom animated:animated];
    }
}
comments powered by Disqus