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

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

tl;dr

If you just want the code that fixes this problem, below is the gist I’ve created. 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 problem

As I’ve noted in the beginning, 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 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 following (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 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. Although now we have these values from transition coordinator context, but that’s for later.

Then, I though that maybe guys working on the UIKit just moved the code that posts keyboard notifications when orientation changes, outside of an 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 though that maybe it is something related to the layout constraints. And although I knew that layout constraints is only a higher abstraction layer to view’s frames and layout, and that changing the code to use layout constraints probably won’t help. I’ve tried it anyway. And of course as I was guessing from the beginning, it also didn’t help. I continued to struggle with it couple more hours until I suddenly asked myself - “But 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 come to a simple question like that, asking yourself why a basic thing such as animations does not work, you should go to 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 after I had areAnimationsEnabled method name in my clipboard while looking for the right place to paste it in my code. Paste, build, run and Yes! Just what I thought! iOS 8, unlike any others, disables animations when posting keyboard notifications due to a change in an 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 a 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 posted as a result of the interface orientation rotation, views should be updated without animations because the notification is already posted from within an animation block. Well, 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 same animation characteristics that are 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 didn’t disabled them for no reason.

What about the QuickType bar animation?

iOS 8 added the QuickType feature which adds 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 of the animation block does the job. So, in this case, it behaves exactly 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 have used two different keyboard notifications: UIKeyboardWillShowNotification and UIKeyboardWillHideNotification. I found out that there is another keyboard notification called UIKeyboardWillChangeFrameNotification which is called before each one of the previous two. Therefore, instead observing two notifications we can observe only one.

Here is the link to gist with the final solution.

  See Github Gist


comments powered by Disqus