Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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!

like image 989
abyx Avatar asked Oct 11 '13 05:10

abyx


5 Answers

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.

like image 103
Arek Holko Avatar answered Nov 03 '22 08:11

Arek Holko


I had a similar issue -- when animating the frame and offset the contents would "jump" right before animating into position -- and the ENTIRE solution was just adding UIViewAnimationOptionBeginFromCurrentState to the animation options. Voila!

like image 43
DanM Avatar answered Nov 03 '22 07:11

DanM


Try to remove the [self.view layoutIfNeeded] line and see if the problem persists or if other problems appear and if so if they look related in any way.

Also, it's always a good idea to reset the position of your views right before the animation. So try to set the normal offset just before the animation line (and maybe even call the layoutIfNeeded method there) Sort of placing everything in order right before you start the animation.

like image 29
Rad'Val Avatar answered Nov 03 '22 08:11

Rad'Val


Does the setNeedsUpdateConstraints really needed? isn't auto layout doing it automatically?

If not- I would suggest you doing all the resizing animation alone without using auto layout. it looks like the problem is when you try doing both animations together (but it's not on the same time)

like image 1
shem Avatar answered Nov 03 '22 09:11

shem


I'm not sure it's exactly the behaviour you want, but maybe it can give you a nudge in the right direction : Github project

What I did was to set up two constraints, one for the text field (to the bottom guide) and the other for the scroll view (to the textfield).

Then when calling the animation I animate the "center" property of both elements, not the contentOffset, and I treat the animation value and the constraint value separately.

Final result is here:

Youtube video

like image 1
vinaut Avatar answered Nov 03 '22 08:11

vinaut