Synchronizing rotation animation between the keyboard and the attached view - Part 2

posted on January 28, 2015

In the first part of this post, I have explained how to synchronize rotation animation in iOS between the virtual keyboard and the view floating above it. Many things have been changed since then, iOS 6, iOS 7, and now iOS 8, which is why I am writing the second part of that post. In short, Apple changed something in iOS 8, causing keyboard notifications observer methods to execute their code while animations are disabled. But only when notifications are posted due to an interface orientation change while the keyboard was visible.

tl;dr

If you want the code that fixes this problem, use the Gist below. The code has a minimal set of methods your UIViewController should implement 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 understand what is happening there, I suggest you continue reading.

Loading gist https://gist.github.com/smnh/e864896ba37bc4cfdce6

The problem

As I've previously noted, the problem is that in iOS 8, the views that should animate in coordination with the keyboard rotation never animate.

First, I thought that it was happening because I was using deprecated methods to update the views when the interface orientation was changed:

So, I've added the new viewWillTransitionToSize:withTransitionCoordinator: method with the same logic I used in these three deprecated methods:

- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
    [super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
    [coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
        self.animationDuration = [context transitionDuration];
        self.animationCurve = [context completionCurve];
        self.animatingRotation = YES;
    } completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
        self.animatingRotation = NO;
    }];
}

Ignore the self.animationDuration and self.animationCurve properties for now.

Of course, this method didn't help. Otherwise, I wouldn't write this post. But, the new order of the events appeared to be as follows (note: the deprecated methods were not called although they were defined):

  1. viewWillTransitionToSize:withTransitionCoordinator: method called
  2. transition coordinator's animateAlongsideTransition: block called
  3. UIKeyboardWillHideNotification notification posted
  4. UIKeyboardWillShowNotification notification posted
  5. transition coordinator completion: block called
  6. UIKeyboardWillHideNotification notification posted
  7. UIKeyboardWillShowNotification notification posted

Yep, UIKeyboardWillHideNotification and UIKeyboardWillShowNotification notifications were posted one after another in both cases. And as with previous iOS versions all keyboard notifications hold 0 values for UIKeyboardAnimationCurveUserInfoKey and UIKeyboardAnimationDurationUserInfoKey notification's userInfo keys. However, now we have these values from the transition coordinator context. We will return to this fact later.

Then, I thought that maybe guys working on the UIKit just moved the code that posts keyboard notifications when orientation changes from the animation block that animates the keyboard and the view controller. So, I've tried to put the code that updates the views inside an animation block. It didn't help either. Then, I thought that maybe it was related to the layout constraints. And although I knew that layout constraints are only a higher abstraction layer to view frames and layout, changing the code to use layout constraints probably won't help. I've tried it anyway. And as I was guessing, it didn't help. I continued to struggle with it couple more hours until I suddenly asked myself - "Why do my views aren't animating even when I have changed their frames inside an animation block in the first place?". That was the turning point question.

If you strip down the problem to a simple question like that, asking yourself why a basic thing such as animations does not work, you should look in a place that can hint you why. For iOS developers, this place is called "UIKit Framework Reference". And this is exactly what I did. Ten seconds later, I had the areAnimationsEnabled method name in my clipboard while looking for the right place to paste it into my code. Paste, build, run, and Voila! Just what I thought! iOS 8, unlike any others, disables animations when posting keyboard notifications due to a change in interface orientation — knowing that the solution was two steps away.

The solution

As you have already guessed, the solution is to enable the animations back. And if there is an areAnimationsEnabled method, then there must be a setAnimationsEnabled: method. This method is what will do the trick. But it's not the end of the story. In the first part of this post, I wrote that when keyboard notifications are posted due to the interface orientation rotation, views should be updated without animations because the notification is already posted from within an animation block. In iOS 8, this is not the case; we need to set up our own animations. But to synchronize animations, our views need to use the same animation characteristics used for the animation that rotates the keyboard and the view controller. Remember these self.animationDuration and self.animationCurve properties we've got from transition coordinator context? We will use them just for that:

if (!self.animatingRotation) {
    // Update the views as usual with animation using animationDuration and animationCurve received from keyboard notification.
    [self adjustViewsForKeyboardFrame:finalKeyboardFrame withAnimationDuration:animationDuration animationCurve:animationCurve];
} else {
    if ([UIView areAnimationsEnabled]) {
        // Animations enabled, we are on iOS 7 and lower
        [self adjustViewsForKeyboardFrame:finalKeyboardFrame];
    } else {
        // Animations disabled, we are on iOS 8
        [UIView setAnimationsEnabled:YES];
        [self adjustViewsForKeyboardFrame:finalKeyboardFrame withAnimationDuration:self.animationDuration animationCurve:self.animationCurve];
        [UIView setAnimationsEnabled:NO];
    }
}

I have disabled animations back to be polite. After all, guys at Apple disabled them for a reason.

What about the QuickType bar animation?

iOS 8 added the QuickType feature, adding a bar with word suggestions above the keyboard. Swiping this bar up and down animates its expansion and minimization. But if we will use our current solution, our view floating above that bar won't be animated when it is expanded or minimized. This happens because when the QuickType bar is expanded or minimized, the posted keyboard notifications hold 0 values for UIKeyboardAnimationCurveUserInfoKey and UIKeyboardAnimationDurationUserInfoKey notification's userInfo keys, just like it happens when the interface orientation is changed. On the other hand, putting the code that updates the floating view outside the animation block does the job. So, in this case, it behaves the same way as it behaves in iOS 7 when the interface orientation is changed. What a mess! Nevertheless, here is the code that fixes all issues:

if (!self.animatingRotation) {
    // Get the animation duration from keyboard notification info
    NSTimeInterval animationDuration = [[notificationInfo objectForKey:UIKeyboardAnimationDurationUserInfoKey] doubleValue];
    if (animationDuration == 0) {
        // On iOS8 if the animationDuration is 0 then it is the QuickType panel who triggered this keyboard notification so any view updates will be animated automatically
        [self adjustViewsForKeyboardFrame:finalKeyboardFrame];
    } else {
        UIViewAnimationCurve animationCurve = (UIViewAnimationCurve) [[notificationInfo objectForKey:UIKeyboardAnimationCurveUserInfoKey] integerValue];
        [self adjustViewsForKeyboardFrame:finalKeyboardFrame withAnimationDuration:animationDuration animationCurve:animationCurve];
    }
} else {
    if ([UIView areAnimationsEnabled]) {
        [self adjustViewsForKeyboardFrame:finalKeyboardFrame];
    } else {
        [UIView setAnimationsEnabled:YES];
        [self adjustViewsForKeyboardFrame:finalKeyboardFrame withAnimationDuration:self.animationDuration animationCurve:self.animationCurve];
        [UIView setAnimationsEnabled:NO];
    }
}

One more thing

In my original post, I used two different keyboard notifications: UIKeyboardWillShowNotification and UIKeyboardWillHideNotification. I found another keyboard notification called UIKeyboardWillChangeFrameNotification, which is called before each of the previous two. Therefore, instead of observing two notifications, we can observe only one.

Here is the link to the Gist with the final solution.