30

I implemeted a custom refresh control (my own class, not a subclass), and for some reason since moving to iOS 8, setting the contentInset of the scroll view (specifically, UICollectionView) to start the refresh animation causes a weird jump/stutter. Here is my code:

- (void)containingScrollViewDidScroll:(UIScrollView *)scrollView
{
    CGFloat scrollPosition = scrollView.contentOffset.y + scrollView.contentInset.top;

    if( scrollPosition > 0 || self.isRefreshing )
    {
        return;
    }

    CGFloat percentWidth = fabs( scrollPosition ) / self.frame.size.height / 2;

    CGRect maskFrame = self.maskLayer.frame;

    maskFrame.size.width = self.imageLayer.frame.size.width * percentWidth;

    [CATransaction begin];
    [CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
    self.maskLayer.frame = maskFrame;
    [CATransaction commit];
}

- (void)containingScrollViewDidEndDragging:(UIScrollView *)scrollView
{
    if( ( self.maskLayer.frame.size.width >= self.imageLayer.frame.size.width ) && !self.isRefreshing )
    {
        self.isRefreshing = YES;
        [self setLoadingScrollViewInsets:scrollView];
        [self startAnimation];
        [self sendActionsForControlEvents:UIControlEventValueChanged];
    }
}

- (void)setLoadingScrollViewInsets:(UIScrollView *)scrollView
{
    UIEdgeInsets loadingInset = scrollView.contentInset;
    loadingInset.top += self.frame.size.height;

    UIViewAnimationOptions options = UIViewAnimationOptionAllowUserInteraction | UIViewAnimationOptionBeginFromCurrentState;

    [UIView animateWithDuration:0.2 delay:0 options:options animations:^
    {
        scrollView.contentInset = loadingInset;
    }
    completion:nil];
}

Basically once the user releases to refresh, I animate the contentInset to the height of the refresh control. I figure the animation would reduce stuttering/jumpiness, which it did in iOS 7. But in iOS 8, when the scrollView is released from dragging, instead of just animating to the contentInset, the scroll view content jumps down from the point of release really quickly, and then animates up smoothly. I'm not sure if this is a bug in iOS 8 or what. I've also tried adding:

scrollView.contentOffset = CGPointZero;

in the animation block, which didn't change anything.

Does anyone have any ideas? Any help would be highly appreciated. Thanks!

ryanthon
  • 2,524
  • 3
  • 17
  • 18

8 Answers8

39

I changed the method with my animation block to:

- (void)setLoadingScrollViewInsets:(UIScrollView *)scrollView
{
    UIEdgeInsets loadingInset = scrollView.contentInset;
    loadingInset.top += self.view.frame.size.height;

    CGPoint contentOffset = scrollView.contentOffset;

    [UIView animateWithDuration:0.2 animations:^
    {
        scrollView.contentInset = loadingInset;
        scrollView.contentOffset = contentOffset;
    }];
}
Dani
  • 1,228
  • 1
  • 16
  • 26
ryanthon
  • 2,524
  • 3
  • 17
  • 18
  • 3
    Worked for me! You rock. Thanks for putting this in here. – boztalay Oct 16 '14 at 00:46
  • 2
    You're correct, this does fix the issue. It looks like animating the contentInset alone causes the animation to interfere with the deceleration animation, which is why there appears a "jump". Including the contentOffset starts the animation from the current position. – stillmotion Dec 08 '14 at 07:19
  • Including the content offset in the animation seems quite difficult to guess. How did you figur it out? – Tudor Apr 30 '15 at 17:02
  • Note: The order in the animation block matters - `contentInset` must be `contentOffset`. – Code May 21 '16 at 16:09
  • 2
    Great answer! The cause of this problem is when the contentInset is set, contentOffset is also set to a value according to the new value of contentInset. So restore contentOffset to its old value after setting contentInset solves the problem. It seems the animation block is not needed in my app with iOS 9.0. – Zack Zhu Jul 19 '16 at 21:37
  • Look at Zack Zhu answer down bellow, it uses a double animation block... that worked for me – Peter Lapisu Mar 30 '17 at 14:41
  • 1
    OMG !!! Thank you, I've already thought that I will break my table, monitor and keyboard !!! – daxh Sep 18 '17 at 16:37
6
- (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate{
    CGPoint point = scrollView.contentOffset;

    static CGFloat refreshViewHeight = 200.0f;

    if (scrollView.contentInset.top == refreshViewHeight) {
        //openning
        if (point.y>-refreshViewHeight) {
            //will close
            //必须套两层animation才能避免闪动!
            [UIView animateWithDuration:0 animations:NULL completion:^(BOOL finished) {
                [UIView animateWithDuration:0.25 animations:^{
                    scrollView.contentOffset = CGPointMake(0.0f, 0.0f);
                    scrollView.contentInset = UIEdgeInsetsMake(0.0f, 0.0f, 0.0f, 0.0f);
                } completion:NULL];
            }];

        }
    }
    else{
        //closing
        static CGFloat openCriticalY = 40.0f;//会执行打开的临界值
        if(point.y<-openCriticalY){
            //will open
            [UIView animateWithDuration:0 animations:NULL completion:^(BOOL finished) {
                [UIView animateWithDuration:0.25 animations:^{
                    scrollView.contentInset = UIEdgeInsetsMake(refreshViewHeight, 0.0f, 0.0f, 0.0f);
                    scrollView.contentOffset = CGPointMake(0.0f, -refreshViewHeight);
                } completion:NULL];
            }];
        }
    }
}

you can try this,the key is using two animation.

user3044484
  • 463
  • 5
  • 7
  • In my app with iOS9.0, no animation block is needed to remove the jump, just resetting contentOffset after setting contentInset will do the trick. However, if you want to control the tableVIew's scroll speed when hiding the refresh view, double animation blocks will do the magic, one animation block is not enough. – Zack Zhu Jul 20 '16 at 19:47
  • Can you explain how the double animation blocks work? Thanks. – Zack Zhu Jul 20 '16 at 20:20
3

To remove the jump, ryanthon's answer will do the trick. In my app with iOS9.0, no animation block is even needed to remove the jump, just resetting contentOffset after setting contentInset will do the trick.

If you want to control the tableView's scroll speed when hiding the refresh view, double animation blocks trick in user3044484's answer will do the magic, one animation block is not enough.

if (self.tableView.contentInset.top == 0)
{
    UIEdgeInsets tableViewInset = self.tableView.contentInset;
    tableViewInset.top = -1.*kTableHeaderHeight;
    [UIView animateWithDuration: 0 animations: ^(void){} completion:^(BOOL finished) {
        [UIView animateWithDuration: 0.5 animations:^{
        //no need to set contentOffset, setting contentInset will change contentOffset accordingly.
            self.tableView.contentInset = tableViewInset;  
        }];
    }];
}
Zack Zhu
  • 378
  • 4
  • 8
2

I had the same problem, and moved my animation block to the...

- (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView

delegate method instead. Not perfect, but I can't pinpoint the problem yet. Looks like scrollView's -layoutSubviews method is being called more often under iOS8, causing jumpy animations.

Siôn
  • 174
  • 1
  • 9
  • I tried moving it into there, but it doesn't seem to give the desired effect, unfortunately... – ryanthon Oct 11 '14 at 23:16
  • 1
    This solution worked for me. To be precise, I moved my setting of `scrollView.contentInset` (within an `animateWithDuration:`) from `scrollViewDidEndDragging:willDecelerate:` to `scrollViewWillBeginDecelerating:` – codeperson Oct 21 '14 at 04:43
2

Two solutions:

  1. disable bounce when start loading and enable bounce when finished loading
  2. after setting content inset when start loading, please set content offset with a constant value
Linkou Bian
  • 305
  • 1
  • 4
  • 11
  • Toggling bounce does not work. But setting contentOffset to its value before setting contentInset works like a charm. No animation block is needed. The cause of this problem is when the contentInset is set, contentOffset is also set to a value according to the new value of contentInset. So restore contentOffset to its old value after setting contentInset solves the problem. – Zack Zhu Jul 19 '16 at 21:32
1

I have resolved by:-

  • Disable the Bounce when ContentInset is being updated to contentOffsetY .
  • Keep track of contentOffset and Update to tableView ContentInset.
 var contentOffsetY:CGFloat = 100
    func tracScrollContent(location: UIScrollView) {
        if(location.contentOffset.y  == -contentOffsetY ) {
            tblView.isScrollEnabled = true
            tblView.bounces = false
            tblView.contentInset = UIEdgeInsets(top: contentOffsetY, left: 0, bottom: 0, right: 0)
        }else if(location.contentOffset.y >= 0 ){
            tblView.bounces = true
            tblView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
        }
    }

Github Demo Swift-3

Shrawan
  • 7,128
  • 4
  • 29
  • 40
0

animating in - (void)scrollViewWillBeginDecelerating:(UIScrollView *)scrollView unfortunately does't work for me. The scrollview bounces anyway.

According to Bounds automatically changes on UIScrollView with content insets this seems like an iOS8-bug to me...

Community
  • 1
  • 1
niggeulimann
  • 688
  • 6
  • 15
0

Based on answer by @ryanthon. Seems that there's no need to animate changes, at least now in 2023). Proven in iOS 15.

let offset = scrollView.contentOffset.y
scrollView.contentInset.top = pullToRefreshInset
scrollView.contentOffset.y = offset
Varrry
  • 2,647
  • 1
  • 13
  • 27