Synchronizing rotation animation between the keyboard and the attached view

Hi, today I would like to show you how to make a UIView to stay attached to the top of the iPhone’s keyboard while the keyboard is animated. But not only when the keyboard is animated while it is being presented or dismissed but also when it is animated when the iPhone is rotated and the interface orientation is changed.

Although many answers could be found explaining how to make a UIView to stay attached to the keyboard while the keyboard is being animated when it is presented or dismissed. I have not found any solutions that also make that UIView to stay attached to the keyboard when device is rotated and rotation of the interface orientation is animated.

Note: The solution explained in this post does not work in iOS 8. I’ve wrote a second part for this post that explains how to solve this problem in iOS 8.

tl;dr

If you just want the code that fixes this problem, here is the link to the gist I’ve created with a working solution. The code has minimal set of methods your UIViewController should implement in order to create right animation effects when the keyboard is shown, hidden, rotated and even when the QuickType bar is minimized or expanded. But if you want to understand what is happening there, I suggest you continue reading.

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 as well as keyboard animation properties such as duration and curve, allowing to perfectly synchronize keyboard appearance animation with the slide-up animation of the attached UIView. Thus, creating the effect in which the keyboard pushes the view up or pulls it down.

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];
}

But, If you are somebody who pays attention to the small details, like me, 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

Following video is a slow motion example for non-synchronized animation between the keyboard and the view that does not stay attached to the keyboard while rotation is animated.

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

To solve this problem we need to make sure not to set up any animations whenever 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 following UIViewController methods:

When the device is rotated and rotation of the interface orientation is animated the order of the events happens in the following 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;
    }
}

Following video is a slow motion example for the result.

References:


comments powered by Disqus