14

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!

abyx
  • 69,862
  • 18
  • 95
  • 117
  • Are you giving to the animation duration the same duration as the keyboard animation? Most probably the animation went wrong because the calculation that you made need to be converted in different reference systems, points coordinates could be different in the view hierarchy. You can solve using scroll view insets as posted by Hoko. – Andrea Oct 13 '13 at 12:27
  • @Andrea - yup, I'm using the same duration. Also, regarding point coordinates: I believe the code should be ok in that regard since I'm changing an absolute value in a constraint (its constant) and not specifying a point on screen – abyx Oct 13 '13 at 12:30
  • Why is `self.keyboardHeight.constant = -height;` not inside the animation block? I assume `keyboardHeight` is your constraint... – Wain Oct 13 '13 at 14:38
  • @Wain that doesn't seem to make a difference – abyx Oct 14 '13 at 06:22
  • @DanM 's simple answer resolved the problem for me. Probably deserves some up-votes :) thanks! – Craig Dec 12 '14 at 16:10

6 Answers6

36

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.

Arek Holko
  • 8,966
  • 4
  • 28
  • 46
  • Thanks :) I had to tweak it a bit more, but it's much better now :) – abyx Oct 14 '13 at 22:35
  • Updating the animation in a timer may also work if changing frame and scrolling is absolutely necessary. Also there is another bug if you are expanding the height of a uiscrollview while the scroll content is at the bottom. – Marwan Roushdy Jan 14 '14 at 00:49
  • the solution here worked for me by setting the `contentInset` _after_ the `setContentOffset`...that prevented the jumpy behavior. – Timmerz Mar 09 '21 at 00:11
8

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!

DanM
  • 7,037
  • 11
  • 51
  • 86
  • I think this answer needs some up-votes. Worked for me, thanks!! – Craig Dec 12 '14 at 16:08
  • I have the same problem, but with a UIPickerView and your solution sadly does not work for me. – Flupp Sep 15 '17 at 13:07
  • This didn't work for me. As i grow my scrollview's content size, the scrollview rsets it's content offset to 0,0. Really annoying me right now – Travis Delly May 21 '18 at 22:01
2

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.

Rad'Val
  • 8,895
  • 9
  • 62
  • 92
  • Removing that causes other weird animation behavior. Resetting does not help unfortunately – abyx Oct 14 '13 at 06:23
1

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)

shem
  • 4,686
  • 2
  • 32
  • 43
  • Seems like it really isn't needed but removing it doesn't help. I'm sure it can be done by manipulating the frame manually, but was hoping to have a an easier autolayout solution :) – abyx Oct 12 '13 at 07:42
  • What happen if you doesn't do animation at all and just let auto layout resize screen? it just "jump"? – shem Oct 12 '13 at 12:29
1

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

vinaut
  • 2,416
  • 15
  • 13
  • Hi, your `UIScrollView` doesn't scroll its content to the bottom when the keyboard is shown (I didn't downvote you). – Arek Holko Oct 13 '13 at 14:46
  • I assumed (and one comment to another answer seems to confirm that) that what the OP wants is just to have the same effect as when the autolayout resizes the constraints automatically but animated. If I mistook the author's wishes I'll either change the answer or delete it. I definitely see no ground to downvote it. – vinaut Oct 13 '13 at 14:50
  • Maybe it wasn't the best explanation, but here it's `and also setting the contentOffset to scroll to the bottom at the same time.` – Arek Holko Oct 13 '13 at 14:52
0

I had the same problem, I was able to resolve it by registering NSNotifications for keyboard/hiding and showing. I am providing the code for same. Hope it will help you.

just declare BOOL isMovedUp in your .h class

In ViewDidLoad

// registering notifications for keyboard/hiding and showing
[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(keyboardWillShow)
                                             name:UIKeyboardWillShowNotification
                                           object:nil];

[[NSNotificationCenter defaultCenter] addObserver:self
                                         selector:@selector(keyboardWillHide)
                                             name:UIKeyboardWillHideNotification
                                           object:nil];    
}

#pragma mark-keyboard notifications
- (void)keyboardWillShow {
    // Animate the current view out of the way
    if (isMovedUp==YES){

    } else {

        [self setViewMovedUp:YES];
        isMovedUp=YES;
    }
}

- (void)keyboardWillHide {
    if (isMovedUp==YES) {
        [self setViewMovedUp:NO];
        isMovedUp=NO;
    }        
}

//method for view transformation

-(void)setViewMovedUp:(BOOL)movedUp {
    [UIView beginAnimations:nil context:NULL];
    [UIView setAnimationDuration:0.5]; // if you want to slide up the view

    CGRect rect = self.winePopUpView.frame;

    if (movedUp) {
        //isKeyBoardDown = NO;
        // 1. move the view's origin up so that the text field that 
        // will be hidden come above the keyboard 
        // 2. increase the size of the view so that the area 
        // behind the keyboard is covered up.
        rect.origin.y -= 100;
        //rect.size.height += 100;
    } else  {
        // revert back to the normal state.
        rect.origin.y += 100;
        //rect.size.height -= 100;
        //isKeyBoardDown = YES;
    }
    self.winePopUpView.frame = rect;
    [UIView commitAnimations];
}
Alex Cio
  • 6,014
  • 5
  • 44
  • 74
Sundeep Saluja
  • 1,089
  • 2
  • 14
  • 36