5

Is there a way to change contentOffset animation speed without creating your own animation that sets the contentOffset?

The reason why I can't create my own animation for contentOffset change is that this will not call -scrollViewDidScroll: in regular intervals during animation.

openfrog
  • 40,201
  • 65
  • 225
  • 373
  • 1
    Assuming calling `[scrollView setContentOffset:foo animated:YES]` from within an animation block doesn't work (and I suspect it doesn't), you can probably access the animation duration by messing with "private" APIs - e.g. [this answer](http://stackoverflow.com/a/11271360/349112) gives a way to change the deceleration rate. Here be dragons... – tc. Mar 03 '13 at 19:12
  • hi, please see my revised answer below. – danh Mar 04 '13 at 06:02

4 Answers4

12

Unfortunately there is no clean and easy way to do that. Here is a slightly brutal, but working approach:

1) Add CADisplayLink as a property:

@property (nonatomic, strong) CADisplayLink *displayLink;

2) Animate content offset:

CGFloat duration = 2.0;

// Create CADisplay link and add it to the run loop
self.displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(_displayLinkTick)];
[self.displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

[UIView animateWithDuration:duration animations:^{
    self.scrollView.contentOffset = newContentOffset;       
} completion:^(BOOL finished) {
    // Cleanup the display link
    [self.displayLink removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
    self.displayLink = nil;
}];

3) Finally observe the changes on presentationLayer like:

- (void)_displayLinkTick {
    CALayer *presentationLayer = (CALayer *)self.scrollView.layer.presentationLayer;
    CGPoint contentOffset = presentationLayer.bounds.origin;
    [self _handleContentOffsetChangeWithOffset:contentOffset];
}

- (void)_handleContentOffsetChangeWithOffset:(CGPoint)offset {
    // handle offset change
}
kkodev
  • 2,557
  • 23
  • 23
  • I think step 3 is the important one to have in mind: the current state of the UI is given by the presentationLayer. – Nikolay Spassov Oct 22 '14 at 09:34
  • 1
    @kamilkocemba This is an amazing tip in general! It's actually a really efficient way to use a single animation to drive the positions of other elements. Thanks! – Arjun Mehta Nov 03 '14 at 16:56
  • Its works. But any button on UIScrollView does not receive touch event. – Asfanur May 08 '15 at 08:41
  • But… what do you do in `_handleContentOffsetChangeWithOffset`? – buildsucceeded Sep 27 '16 at 15:30
  • @buildsucceeded, The same code you have in your `scrollViewDidScroll:` method. – Iulian Onofrei Jun 23 '17 at 08:09
  • Interesting use of the `presentationLayer`. – Iulian Onofrei Jun 23 '17 at 08:21
  • This works great with `UIViewControllerAnimatedTransitioning` to animate a view controller that's using autolayout (the two don't play nicely together). Animate a dummy UIView's frame, and track the presentation layer to update the view controller's frame. – arsenius Mar 13 '18 at 07:21
  • https://codegists.com/snippet/swift/moscrollviewswiftswift_tkirby_swift Swift class with func setContentOffset(contentOffset: CGPoint, with timingFunction: CAMediaTimingFunction, duration: CFTimeInterval) I added it as an answer but some busybody mod deleted it. – Kirby Todd Jun 27 '18 at 18:05
2

To get periodic information about the scroll state, you could run the animation in steps. The delegate will get called once (scrollViewDidScroll:) for each step

- (void)scrollTo:(CGPoint)offset completion:(void (^)(BOOL))completion {

    // this presumes an outlet called scrollView.   You could generalize by passing
    // the scroll view, or even more generally, place this in a UIScrollView category
    CGPoint contentOffset = self.scrollView.contentOffset;

    // scrollViewDidScroll delegate will get called 'steps' times
    NSInteger steps = 10;
    CGPoint offsetStep = CGPointMake((offset.x-contentOffset.x)/steps, (offset.y-contentOffset.y)/steps);
    NSMutableArray *offsets = [NSMutableArray array];

    for (int i=0; i<steps; i++) {
        CGFloat stepX = offsetStep.x * (i+1);
        CGFloat stepY = offsetStep.y * (i+1);
        NSValue *nextStep = [NSValue valueWithCGPoint:CGPointMake(contentOffset.x+stepX, contentOffset.y+stepY)];
        [offsets addObject:nextStep];
    }
    [self scrollBySteps:offsets completion:completion];
}

// run several scroll animations back-to-back
- (void)scrollBySteps:(NSMutableArray *)offsets completion:(void (^)(BOOL))completion {

    if (!offsets.count) return completion(YES);

    CGPoint offset = [[offsets objectAtIndex:0] CGPointValue];
    [offsets removeObjectAtIndex:0];

    // total animation time == steps * duration.  naturally, you can fool with both
    // constants.  to keep the rate constant, set duration == steps * k, where k
    // is some constant time per step
    [UIView animateWithDuration:0.1 delay:0.0 options:UIViewAnimationOptionCurveLinear animations:^{
        self.scrollView.contentOffset = offset;
    } completion:^(BOOL finished) {
        [self scrollBySteps:offsets completion:completion];
    }];
}

Call it like this...

CGPoint bottomOffset = CGPointMake(0, self.scrollView.contentSize.height - self.scrollView.bounds.size.height);    
[self scrollTo:bottomOffset completion:^(BOOL finished) {}];

// BONUS completion handler!  you can omit if you don't need it
danh
  • 62,181
  • 10
  • 95
  • 136
1

Below code solved my issue

CGPoint leftOffset = CGPointMake(0, 0);
          [UIView animateWithDuration:.5
                                delay:0
                              options:UIViewAnimationOptionCurveLinear
                           animations:^{
                               [self.topBarScrollView setContentOffset:leftOffset animated:NO];
                           } completion:nil];
arunjos007
  • 4,105
  • 1
  • 28
  • 43
-3

Here's a simple solution that works.

    [UIView beginAnimations:nil context:nil];
    [UIView setAnimationDuration:0.5];
    [self setContentOffset:<offset> animated:YES];
    [UIView commitAnimations];

Hope this helps.

Brian

Brian Boyle
  • 2,849
  • 5
  • 27
  • 35