Search code examples
iosobjective-cuiscrollviewcore-animationautolayout

UIScrollView animation of height and contentOffset "jumps" content from bottom


Trying to do something similar to the Messages.app's behavior, I have a UIScrollView and beneath it a text field, and trying to animate it so that when the keyboard appears everything is moved up above the keyboard using a constraint that moves the field up (and the UIScrollView's height changes as well due to autolayout) and also setting the contentOffset to scroll to the bottom at the same time.

The code accomplishes the wanted end-result, but during the animation right when the keyboard animation begins the scroll view becomes blank and then the content scrolls up from the bottom, instead of scrolling from the position it was in when the animation started.

The animation is this:

- (void)updateKeyboardConstraint:(CGFloat)height animationDuration:(NSTimeInterval)duration {
    self.keyboardHeight.constant = -height;
    [self.view setNeedsUpdateConstraints];

    [UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionBeginFromCurrentState animations:^{
        [self.view layoutIfNeeded];
        self.collectionView.contentOffset = 
            CGPointMake(0, self.collectionView.contentSize.height - self.collectionView.bounds.size.height);
    } completion:nil];
}

A video of the problem is available here.

Thanks!


Solution

  • It might be a bug in UIKit. It happens when there's a simultaneous change of size and contentOffset of UIScrollView. It'd be interesting to test if this behavior also happens without Auto Layout.

    I've found two workarounds to this problem.

    Using contentInset (the Messages approach)

    As it can be seen in the Messages app, UIScrollView's height doesn't change when a keyboard is shown - messages are visible under the keyboard. You can do it the same way. Remove constraint between UICollectionView and the view that contains UITextField and UIButton (I'll call it messageComposeView). Then add constraint between UICollectionView and Bottom Layout Guide. Keep the constraint between messageComposeView and the Bottom Layout Guide. Then use contentInset to keep the last element of the UICollectionView visually above the keyboard. I did it the following way:

    - (void)updateKeyboardConstraint:(CGFloat)height animationDuration:(NSTimeInterval)duration {
        self.bottomSpaceConstraint.constant = height;
    
        [UIView animateWithDuration:duration delay:0 options:UIViewAnimationOptionBeginFromCurrentState animations:^{
            CGPoint bottomOffset = CGPointMake(0, self.collectionView.contentSize.height - (self.collectionView.bounds.size.height - height));
            [self.collectionView setContentOffset:bottomOffset animated:YES];
    
            [self.collectionView setContentInset:UIEdgeInsetsMake(0, 0, height, 0)];
    
            [self.view layoutIfNeeded];
        } completion:nil];
    }
    

    Here self.bottomSpaceConstraint is a constraint between messageComposeView and Bottom Layout Guide. Here's the video showing how it works. UPDATE 1: Here's my project's source on GitHub. This project is a little simplified. I should've taken into consideration options passed in the notification in - (void)keyboardWillShow:(NSNotification *)notif.

    Performing changes in a queue

    Not an exact solution, but scrolling works fine if you move it to the completion block:

    } completion:^(BOOL finished) {
        [self.collectionView setContentOffset:CGPointMake(0, self.collectionView.contentSize.height - self.collectionView.bounds.size.height) animated:YES];
    }];
    

    It takes 0.25s for the keyboard to show, so the difference between the beginnings of the animations might be noticeable. Animations can also be done in the reversed order.

    UPDATE 2: I've also noticed that OP's code works fine with this change:

    CGPoint bottomOffset = CGPointMake(0, self.collectionView.contentSize.height - (self.collectionView.bounds.size.height - height));
    

    but only when contentSize's height is less than some fixed value (in my case around 800, but my layout may be a little different).

    In the end I think that the approach I presented in Using contentInset (the Messages approach) is better than resizing UICollectionView. When using contentInset we also get the visibility of the elements under the keyboard. It certainly fits the iOS 7 style better.