46

Is there a way to change the speed of the animation when scrolling a UITableView using setContentOffset:animated:? I want to scroll it to the top, but slowly. When I try the following, it causes the bottom few cells to disappear before the animation starts (specifically, the ones that won't be visible when the scroll is done):

[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:3.0];
[self.tableView setContentOffset:CGPointMake(0, 0)];
[UIView commitAnimations];

Any other way around this problem? There is a private method _setContentOffsetAnimationDuration that works, but I don't want to be rejected from the app store.

Jordan Kay
  • 689
  • 1
  • 6
  • 12
  • Please see my answer below. You can in fact access _setContentOffsetAnimationDuration through legitimate means. Thanks for noting it's existence in your original question. ;-) – eGanges Mar 26 '18 at 21:41

16 Answers16

93
[UIView animateWithDuration:2.0 animations:^{
    scrollView.contentOffset = CGPointMake(x, y);
}];

It works.

TK189
  • 1,490
  • 1
  • 13
  • 17
  • 44
    Well, it does not work if your scrollview is a uitableview. creation and reuse of cells fails. – hfossli Oct 18 '12 at 17:18
  • 1
    Works smooth and nicely for my UIscrollview – Amandir Oct 25 '12 at 11:16
  • 7
    Well, it works, but not in the context of the question. My cells are disappearing too! – yuf Dec 09 '12 at 21:25
  • 1
    This is incorrect. Causes bad behavior if scrolling more than one page due to improper cell reuse. – Moe Salih Aug 12 '13 at 23:45
  • @hfossli Were you able to find a way to achieve the animated contentOffset where the cell reuse is not failing? – DancOfDeth Sep 05 '13 at 14:44
  • 1
    @DancOfDeth Yep. You will have to do the scrolling yourself. Set up a CADisplayLink to fire every 1/60 seconds and update the contentOffset. I also recommend AHFunction for nice animationcurves. – hfossli Sep 05 '13 at 14:48
  • 1
    So you're aware, doing manual animation will break `scrollViewDidEndScrollingAnimation` – SimplGy Mar 09 '15 at 15:56
  • `scrollViewDidScroll` is only called once during the animation using this method (with the final value) – agandi Aug 02 '16 at 08:49
  • To maintain proper cell reuse, take a look at my answer: https://stackoverflow.com/a/44544330/2328732 – m_katsifarakis Jun 14 '17 at 12:07
18

Setting the content offset directly did not work for me. However, wrapping setContentOffset(offset, animated: false) inside an animation block did the trick.

UIView.animate(withDuration: 0.5, animations: {
                self.tableView.setContentOffset(
               CGPoint(x: 0, y: yOffset), animated: false)
            })
astro4
  • 317
  • 2
  • 5
  • 1
    Thanks you, this works great. It does not present any of the problems described in comments on other answers to this question. – Austin Wood Feb 16 '18 at 18:53
  • Works in Swift 4.0 – mcphersonjr Apr 12 '18 at 15:00
  • 1
    I have working scrollViewDidScroll method with other view depending of scroll offset. And using just animate, it triggers only once and I don't get smooth animation of dependent view. It just jumps to the final position immediately((( – bodich Jan 06 '20 at 20:30
  • It has the same problem as this answer https://stackoverflow.com/a/9106903/1050261 - correct reusing of cells fails, they may disappear before animation starts. – Igor Kulagin Jul 31 '20 at 15:38
  • Doesn't change the speed Xcode 13.4.1 – Oleh H Sep 20 '22 at 11:39
14

I've taken nacho4d's answer and implemented the code, so I thought it would be helpful for other people coming to this question to see working code:

I added member variables to my class:

CGPoint startOffset;
CGPoint destinationOffset;
NSDate *startTime;

NSTimer *timer;

and properties:

@property (nonatomic, retain) NSDate *startTime;
@property (nonatomic, retain) NSTimer *timer;

and a timer callback:

- (void) animateScroll:(NSTimer *)timerParam
{
    const NSTimeInterval duration = 0.2;

    NSTimeInterval timeRunning = -[startTime timeIntervalSinceNow];

    if (timeRunning >= duration)
    {
        [self setContentOffset:destinationOffset animated:NO];
        [timer invalidate];
        timer = nil;
        return;
    }
    CGPoint offset = [self contentOffset];

    offset.x = startOffset.x +
        (destinationOffset.x - startOffset.x) * timeRunning / duration;

    [self setContentOffset:offset animated:NO];
}

then:

- (void) doAnimatedScrollTo:(CGPoint)offset
{
    self.startTime = [NSDate date];
    startOffset = self.contentOffset;
    destinationOffset = offset;

    if (!timer)
    {
        self.timer = [NSTimer scheduledTimerWithTimeInterval:0.01
                                                      target:self
                                                    selector:@selector(animateScroll:)
                                                    userInfo:nil
                                                     repeats:YES];
    }
}

you'd also need timer cleanup in the dealloc method. Since the timer will retain a reference to the target (self) and self has a reference to the timer, some cleanup code to cancel/destroy the timer in viewWillDisappear is likely to be a good idea too.

Any comments on the above or suggestions for improvement would be most welcome, but it is working very well with me, and solves other issues I was having with setContentOffset:animated:.

JosephH
  • 37,173
  • 19
  • 130
  • 154
  • This code needs a small modification according to what the original poster wanted. He wanted a slow scroll up and down. So all those .x values should be .y. That got it working for me. – AaronG Feb 24 '11 at 03:32
  • Ah yes, well spotted, thanks - I'm actually using the above code in a paging UIScrollView and hadn't actually noticed the question was about UITableView :-) – JosephH Feb 24 '11 at 10:59
  • 3
    Don't use NSTimer for animations, use CADisplayLink. For example: http://www.bigspaceship.com/ios-animation-intervals/ – José Manuel Sánchez Jan 23 '15 at 12:20
  • 1
    for CADisplayLink, you may refer, stackoverflow answer link https://stackoverflow.com/a/45641913/2641380 – SHS Jun 06 '18 at 10:52
10

There is no a direct way of doing this, nor doing the way you wrote it. The only way I can accomplish this is by making the movement/animation by my own.

For example move 1px every 1/10 second should simulate a very slow scroll animation. (Since its a linear animation maths are very easy!)

If you want to get more realistic or fancy and simulate easy-in easy-off effect then you need some maths to calculate a bezier path so you can know the exact position at every 1/10 second, for example

At least the first approach shouldn't be that difficult. Just use or -performSelector:withObject:afterDelay or NSTimerswith

-[UIScrollView setContentOffset:(CGPoint*)];`

Hope it helps

Maulik
  • 19,348
  • 14
  • 82
  • 137
nacho4d
  • 43,720
  • 45
  • 157
  • 240
6

UIView calculates final view and then animates it. That's why cells that invisible on finish of animation invisible on start too. For prevent this needed add layoutIfNeeded in animation block:

[UIView animateWithDuration:2.0 animations:^{
    [self.tableView setContentOffset:CGPointMake(0, 0)];
    [self.tableView layoutIfNeeded]
}];

Swift version:

UIView.animate(withDuration: 2) {
    self.tableView.contentOffset.y = 10
    self.tableView.layoutIfNeeded()
}
Igor
  • 12,165
  • 4
  • 57
  • 73
5

I'm curious as to whether you found a solution to your problem. My first idea was to use an animateWithDuration:animations: call with a block setting the contentOffset:

[UIView animateWithDuration:2.0 animations:^{
    scrollView.contentOffset = CGPointMake(x, y);
}];

Side effects

Although this works for simple examples, it also has very unwanted side effects. Contrary to the setContentOffset:animated: everything you do in delegate methods also gets animated, like the scrollViewDidScroll: delegate method.

I'm scrolling through a tiled scrollview with reusable tiles. This gets checked in the scrollViewDidScroll:. When they do get reused, they get a new position in the scroll view, but that gets animated, so there are tiles animating all the way through the view. Looks cool, yet utterly useless. Another unwanted side effect is that possible hit testing of the tiles and my scroll view's bounds is instantly rendered useless because the contentOffset is already at a new position as soon as the animation block executes. This makes stuff appear and disappear while they're still visible, as to where they used to be toggled just outside of the scroll view's bounds.

With setContentOffset:animated: this is all not the case. Looks like UIScrollView is not using the same technique internally.

Is there anyone with another suggestion for changing the speed/duration of the UIScrollView setContentOffset:animated: execution?

epologee
  • 11,229
  • 11
  • 68
  • 104
  • nacho4d's answer suggesting using NSTimer to do the "animation" manually worked fine for me, did you try it? – JosephH Feb 11 '11 at 18:32
  • Well, it does not work if your scrollview is a uitableview. creation and reuse of cells fails. – hfossli Oct 18 '12 at 17:19
4

You can set the duration as follows:

scrollView.setValue(5.0, forKeyPath: "contentOffsetAnimationDuration") scrollView.setContentOffset(CGPoint(x: 100, y: 0), animated: true)

This will also allow you to get all of your regular delegate callbacks.

eGanges
  • 411
  • 3
  • 8
  • 1
    That's a private API that could lead to an App Store rejection – Felix Lapalme May 02 '18 at 17:33
  • 1
    just to clarify for newbies, setValue(_: forKey:) is NOT a private API. Whether or not contentOffsetAnimationDuration is supposed to be "unknown" and therefore private, is up for debate. Hopefully not, since A) this is the cleanest way to do this within the rules and leave the call backs intact; and B) I'm about to submit this to the store. I'll let us all know if bombs back. – eGanges May 25 '18 at 16:07
  • 1
    The "contentOffsetAnimationDuration" ivar isn't exposed publicly and is therefore private. There's nothing to debate about that. The question is if Apple will catch it in their automated private API check system. But more importantly, this is not a very safe way of doing things. If Apple changes the name of that property on their side (and they're absolutely free to do that since it's a private property) then your app will crash on that line. – Felix Lapalme May 25 '18 at 19:51
  • @eGanges what happened on submission? – Rakesha Shastri Sep 19 '18 at 07:40
  • oh boy... that's not gonna be approved. BUT IT WORKS SO WELL :( – Bruno Muniz May 06 '19 at 17:32
  • 1
    Sorry all, the feature set(s) wherein this enhancement (a scrolling text view) was a subset was never rolled forward to the production app, so I cannot provide more data on how well or not it would be received by Apple. :-( – eGanges May 07 '19 at 18:27
2

https://github.com/dominikhofmann/PRTween

subclass UITableview

#import "PRTween.h"


@interface JPTableView : UITableView{
  PRTweenOperation *activeTweenOperation;
}



- (void) doAnimatedScrollTo:(CGPoint)destinationOffset
{
    CGPoint offset = [self contentOffset];

    activeTweenOperation = [PRTweenCGPointLerp lerp:self property:@"contentOffset" from:offset to:destinationOffset duration:1.5];


}
johndpope
  • 5,035
  • 2
  • 41
  • 43
1

IF all your trying to do is scroll your scrollview I think you should use scroll rect to visible. I just tried out this code

 [UIView animateWithDuration:.7
                      delay:0
                    options:UIViewAnimationOptionCurveEaseOut
                 animations:^{  
                     CGRect scrollToFrame = CGRectMake(0, slide.frame.origin.y, slide.frame.size.width, slide.frame.size.height + kPaddingFromTop*2);
                     CGRect visibleFrame = CGRectMake(0, scrollView.contentOffset.y,
                                                      scrollView.frame.size.width, scrollView.frame.size.height);
                     if(!CGRectContainsRect(visibleFrame, slide.frame))
                         [self.scrollView scrollRectToVisible:scrollToFrame animated:FALSE];}];

and it scrolls the scrollview to the location i need for whatever duration i am setting it for. The key is setting animate to false. When it was set to true, the animation speed was the default value set by the method

Esko918
  • 1,397
  • 1
  • 12
  • 25
0

For people who also have issues with disappearing items while scrolling a UITableView or a UICollectionView you can expand the view itself so that we hold more visible items. This solution is not recommended for situations where you need to scroll a great distance or in situations where the user can cancel the animation. In the app I'm currently working on I only needed to let the view scroll a fixed 100px.

NSInteger scrollTo = 100;

CGRect frame = self.collectionView.frame;
frame.size.height += scrollTo;
[self.collectionView setFrame:frame];

[UIView animateWithDuration:0.8 delay:0.0 options:(UIViewAnimationOptionCurveEaseIn) animations:^{
    [self.collectionView setContentOffset:CGPointMake(0, scrollTo)];
} completion:^(BOOL finished) {
    [UIView animateWithDuration:0.8 delay:0.0 options:(UIViewAnimationOptionCurveEaseIn) animations:^{
        [self.collectionView setContentOffset:CGPointMake(0, 0)];

    } completion:^(BOOL finished) {
        CGRect frame = self.collectionView.frame;
        frame.size.height -= scrollTo;
        [self.collectionView setFrame:frame];
    }];
}];
Ben Groot
  • 5,040
  • 3
  • 40
  • 47
0

I use transitionWithView:duration:options:animations:completion:

        [UIView transitionWithView:scrollView duration:3 options:(UIViewAnimationOptionCurveLinear) animations:^{
        transitionWithView:scrollView.contentOffset = CGPointMake(contentOffsetWidth, 0);
    } completion:nil];

UIViewAnimationOptionCurveLinear is an option make animation to occur evenly over.

While I found that in an animation duration, the delegate method scrollViewDidScroll did not called until animation finished.

MichaelMao
  • 528
  • 6
  • 15
-1

You can simply use block based animation to animate the speed of scrollview. First calculate the offset point to which you want to scroll and then simply pass that offset value as here.....

    [UIView animateWithDuration:1.2
                      delay:0.02 
                      options:UIViewAnimationCurveLinear  
                      animations:^{
     [colorPaletteScrollView setContentOffset: offset ];
 }
 completion:^(BOOL finished)
 { NSLog(@"animate");
 } ];

here colorPaletteScrollView is my custom scrollview and offset is the value passed .

this code works perfectly fine for me.

Shubham
  • 785
  • 2
  • 9
  • 14
  • 1
    Well, it does not work if your scrollview is a uitableview. creation and reuse of cells fails. – hfossli Oct 18 '12 at 17:20
-2

Is there a reason you're using setContentOffset and not scrollRectToVisible:animated:?

- (void)scrollRectToVisible:(CGRect)rect animated:(BOOL)animated

I would recommend doing it like this:

[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:3.0];
[self.tableView scrollRectToVisible:CGRectMake(0, 0, 320, 0) animated:NO];
[UIView commitAnimations];

Unless that doesnt work. I still think you should try it.

Jumhyn
  • 6,687
  • 9
  • 48
  • 76
-2

Actually TK189's answer is partially correct.

To achieve a custom duration animated contentOffset change, with proper cell reuse by UITableView and UICollectionView components, you just have to add a layoutIfNeeded call inside the animations block:

[UIView animateWithDuration:2.0 animations:^{
    tableView.contentOffset = CGPointMake(x, y);
    [tableView layoutIfNeeded];
}];
m_katsifarakis
  • 1,777
  • 1
  • 21
  • 27
-3

On Xcode 7.1 - Swift 2.0 :

func textFieldShouldEndEditing(textField: UITextField) -> Bool {

    dispatch_async(dispatch_get_main_queue()) {
        UIView.animateWithDuration(0, animations: { self.scrollView!.setContentOffset(CGPointZero,animated: true) })
    }
    return true
}

OR

func textFieldShouldReturn(textField: UITextField) -> Bool {

    if(textField.returnKeyType == UIReturnKeyType.Next) {
        password.becomeFirstResponder()
    }

    dispatch_async(dispatch_get_main_queue()) {
        UIView.animateWithDuration(0, animations: { self.scrollView!.setContentOffset(CGPointZero,animated: true) })
    }

    textField.resignFirstResponder()
    return true
}

Note: self.scrollView!.setContentOffset(CGPointZero,animated: true) can have different positions depending on the requirement

Example:

let scrollPoint:CGPoint = CGPointMake(0,textField.frame.origin.y/2);
scrollView!.setContentOffset(scrollPoint, animated: true);
Pang
  • 9,564
  • 146
  • 81
  • 122
Alvin George
  • 14,148
  • 92
  • 64
-3

I wanted to change the contentOffSet of tableview when textfield begins to edit.

Swift 3.0

func textFieldDidBeginEditing(_ textField: UITextField) {

DispatchQueue.main.async { 
    UIView.animate(withDuration: 0, animations: {

     self.sampleTableView.contentOffset = CGPoint(x: 0, y: 0 - (self.sampleTableView.contentInset.top - 200 ))
    }) 
}    
}
Alvin George
  • 14,148
  • 92
  • 64