Synchronizing rotation animation between the keyboard and the attached view

posted on September 21, 2013

In this post, I would like to show how to make a UIView stay attached to the top of the iPhone’s keyboard while the keyboard is animated. This solution works when the keyboard is animated while being presented or dismissed and when the iPhone is rotated and the interface orientation is changed.

There could be found many answers explaining how to make a UIView stay attached to the keyboard while the keyboard is animated when presented or dismissed. However, I didn't find any examples explaining how to make UIView stay attached to the keyboard when the device is rotated, and the interface orientation is animated.

Note: The solution explained in this post does not work in iOS 8. I've written the second part of this post that explains how to solve this problem in iOS 8.

tl;dr

If you want the code that fixes this problem, here is the link to the gist I've created with a working solution. The code has a minimal set of methods your UIViewController needs to create the proper animation effects when the keyboard is shown, hidden, rotated, and even when the QuickType bar is minimized or expanded. But if you want to learn what is happening behind the scenes, continue reading.

Basic Solution

The basic principle for solving the first problem requires observing keyboard notifications such as UIKeyboardWillShowNotification and UIKeyboardWillHideNotification and updating the appropriate views when these notifications are received. The keyboard notification object supplies the final keyboard position and its dimensions and keyboard animation properties such as duration and curve, allowing to perfectly synchronize the keyboard's appearance animation with the slide-up animation of the attached UIView.

The following example demonstrates the basic principle explained above:

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

    [[NSNotificationCenter defaultCenter]
            addObserver:self selector:@selector(keyboardWillShow:)
            name:UIKeyboardWillShowNotification object:nil];
    [[NSNotificationCenter defaultCenter]
            addObserver:self selector:@selector(keyboardWillHide:)
            name:UIKeyboardWillHideNotification object:nil];
}

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

    [[NSNotificationCenter defaultCenter]
            removeObserver:self name:UIKeyboardWillShowNotification object:nil];
    [[NSNotificationCenter defaultCenter]
            removeObserver:self name:UIKeyboardWillHideNotification object:nil];
}

- (void)keyboardWillShow:(NSNotification*)notification {
    [self adjustViewForKeyboardNotification:notification];
}

- (void)keyboardWillHide:(NSNotification*)notification {
    [self adjustViewForKeyboardNotification:notification];
}

- (void)adjustViewForKeyboardNotification:(NSNotification *)notification {
    NSDictionary *notificationInfo = [notification userInfo];

    // Get the end frame of the keyboard in screen coordinates.
    CGRect finalKeyboardFrame = [[notificationInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
    // Get the animation curve and duration
    UIViewAnimationCurve animationCurve = (UIViewAnimationCurve) [[notificationInfo objectForKey:UIKeyboardAnimationCurveUserInfoKey] integerValue];
    NSTimeInterval animationDuration = [[notificationInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue];

    // Convert the finalKeyboardFrame to view coordinates to take into account any rotation
    // factors applied to the window’s contents as a result of interface orientation changes.
    finalKeyboardFrame = [self.view convertRect:finalKeyboardFrame fromView:self.view.window];

    // Calculate new position of the commentBar
    CGRect commentBarFrame = self.commentBar.frame;
    commentBarFrame.origin.y = finalKeyboardFrame.origin.y - commentBarFrame.size.height;

    // Update tableView height.
    CGRect tableViewFrame = self.tableView.frame;
    tableViewFrame.size.height = commentBarFrame.origin.y;

    // Animate view size synchronously with the appearance of the keyboard. 
    [UIView beginAnimations:nil context:nil];
    [UIView setAnimationDuration:animationDuration];
    [UIView setAnimationCurve:animationCurve];
    [UIView setAnimationBeginsFromCurrentState:YES];

    self.commentBar.frame = commentBarFrame;
    self.tableView.frame = tableViewFrame;

    [UIView commitAnimations];
}

Advanced Solution

However, If you are a hawk-eyed person who pays attention to the small details, then you must have noticed that even in popular applications such as Facebook Messenger and WhatsApp, the view with the text field attached to the top of the keyboard does not stay attached while the rotation of interface orientation is animated.

WhatsApp rotation
Toolbar with textfield detached from keyboard while animating rotation in WhatsApp
Facebook Messenger rotation
Toolbar with textfield detached from keyboard while animating rotation in Facebook Messenger

The following video is a slow-motion example of a non-synchronized animation between the keyboard and the view that does not stay attached to the keyboard while orientation is animated.

This detachment between the keyboard and the view happens because the keyboard is instantly dismissed and presented when the rotation animation starts. Subsequently, appropriate keyboard notifications are sent, and their notification observers set up two animations. But in fact, there is no need to set up these animations because UIKit already sets an animation to rotate the interface orientation.

To solve this problem, we need to prevent setting up any animations whenever the keyboard is dismissed or presented due to rotation of the interface orientation. Instead, we only need to update the position of appropriate views. For this purpose, we can utilize the following UIViewController methods:

When the device is rotated and the interface orientation is animated, the following events happen in order:

  1. willRotateToInterfaceOrientation:duration: method is called
  2. UIKeyboardWillHideNotification notification is sent
  3. willAnimateRotationToInterfaceOrientation:duration: method is called
  4. UIKeyboardWillShowNotification notification is sent
  5. didRotateFromInterfaceOrientation: method is called

So basically, we want to disable any custom animations between the first and the last method calls. And the simplest way to achieve that is by using a simple boolean flag.

Following example demonstrates the solution explained above:

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

    [[NSNotificationCenter defaultCenter]
            addObserver:self selector:@selector(keyboardWillShow:)
            name:UIKeyboardWillShowNotification object:nil];
    [[NSNotificationCenter defaultCenter]
            addObserver:self selector:@selector(keyboardWillHide:)
            name:UIKeyboardWillHideNotification object:nil];
}

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

    [[NSNotificationCenter defaultCenter]
            removeObserver:self name:UIKeyboardWillShowNotification object:nil];
    [[NSNotificationCenter defaultCenter]
            removeObserver:self name:UIKeyboardWillHideNotification object:nil];
}

// #1
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
    [super willRotateToInterfaceOrientation:toInterfaceOrientation duration:duration];
    self.animatingRotation = YES;
}
// #3
- (void)willAnimateRotationToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration {
    [super willAnimateRotationToInterfaceOrientation:toInterfaceOrientation duration:duration];
}
// #5
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation {
    [super didRotateFromInterfaceOrientation:fromInterfaceOrientation];
    self.animatingRotation = NO;
}
// #4
- (void)keyboardWillShow:(NSNotification*)notification {
    [self adjustViewForKeyboardNotification:notification];
}
// #2
- (void)keyboardWillHide:(NSNotification*)notification {
    [self adjustViewForKeyboardNotification:notification];
}

- (void)adjustViewForKeyboardNotification:(NSNotification *)notification {
    NSDictionary *notificationInfo = [notification userInfo];

    // Get the end frame of the keyboard in screen coordinates.
    CGRect finalKeyboardFrame = [[notificationInfo objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];

    // Convert the finalKeyboardFrame to view coordinates to take into account any rotation
    // factors applied to the window’s contents as a result of interface orientation changes.
    finalKeyboardFrame = [self.view convertRect:finalKeyboardFrame fromView:self.view.window];

    // Calculate new position of the commentBar
    CGRect commentBarFrame = self.commentBar.frame;
    commentBarFrame.origin.y = finalKeyboardFrame.origin.y - commentBarFrame.size.height;

    // Update tableView height.
    CGRect tableViewFrame = self.tableView.frame;
    tableViewFrame.size.height = commentBarFrame.origin.y;

    if (!self.animatingRotation) {
        // Get the animation curve and duration
        UIViewAnimationCurve animationCurve = (UIViewAnimationCurve) [[notificationInfo objectForKey:UIKeyboardAnimationCurveUserInfoKey] integerValue];
        NSTimeInterval animationDuration = [[notificationInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue];

        // Animate view size synchronously with the appearance of the keyboard. 
        [UIView beginAnimations:nil context:nil];
        [UIView setAnimationDuration:animationDuration];
        [UIView setAnimationCurve:animationCurve];
        [UIView setAnimationBeginsFromCurrentState:YES];

        self.commentBar.frame = commentBarFrame;
        self.tableView.frame = tableViewFrame;

        [UIView commitAnimations];
    } else {
        self.commentBar.frame = commentBarFrame;
        self.tableView.frame = tableViewFrame;
    }
}

The following video is a slow-motion example of the result.

References: