3
votes

So I realize that having nested scroll views at all it sort of a red flag, but the current setup of everything actually works quite well besides one small problem. One scroll view manages scrolling through a collection, while another handles zooming and panning on the entire collection view. This all works, but the small problem comes from when zooming in and panning downward, the scrollview pans while the collectionview scrolls, causing the view to scroll twice as fast, and not feel connected to your finger.

What I ideally want to happen is vertical scrolling is managed by the outer scroll view when panning is possible, and then handled by the inner scroll view when the outer one can no longer pan. I got very close by writing something like this:

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    if (scrollView == _outerScrollView) {
        CGPoint offset = _outerScrollView.contentOffset;
        CGFloat height = _outerScrollView.frame.size.height;
        CGFloat contentHeight = _outerScrollView.contentSize.height;

        ScrollDirection scrollDirection;
        if (offset > _lastContentOffset){
            scrollDirection = ScrollDirectionUp;
        } else {
            scrollDirection = ScrollDirectionDown;
        }

        BOOL scrollIsAtTop =  offset.y <= 0;
        BOOL scrollIsAtBottom = offset.y + height >= contentHeight;

        //If there is a pan upward and we aren't at the top of the outer
        //scrollview cancel the gesture on the inner view
        //downward vice versa
        if (!((scrollIsAtTop && scrollDirection == ScrollDirectionUp) 
               || (scrollIsAtBottom && scrollDirection == ScrollDirectionDown))) {
            _innerCollectionView.panGestureRecognizer.enabled = NO;
            _innerCollectionView.panGestureRecognizer.enabled = YES;
        } 
    }
    _lastContentOffset = offset.y;
}

This ALMOST works, with one side effect of a big pan downward stops when it hits the bottom and requires the user to start a new gesture to continue scrolling with the inner collection. Ideally this transition would be smooth, but I'm having a hard time figuring out a way to do this. Again I realize scroll view inside scroll view is not ideal, but if I can fix this small problem everything will be good, rather than attempt to redesign the whole thing.

Any ideas on how I can handle the double scroll in a way that lets the pan gesture win, but cleanly transitions to the inner collection when the outer scroll view can no longer pan vertically?

1
do you have a video you can put together quickly? or maybe zip up the project?lead_the_zeppelin
Not easily, if I can't find a fix soon I can put together a demo project that reproduces it, but it would take me an hour or so, so I'll keep fighting and see if anybody has any good suggestions in the mean time.Kevin DiTraglia
Did you ever figure this out? I currently have the same issue.ArtSabintsev
@ArtSabintsev I did come up with a (kind of terrible) solution that worked in the end, I'll post and answer of what it looked like.Kevin DiTraglia
Thanks! I have the whole rubber-banding issue between a nested UICollectionView and its UIScrollView superview. I had done more or less the same thing you did, which gives me solace, that I was on the right track, and am not the only person in the world with this annoying issue :)ArtSabintsev

1 Answers

2
votes

So, since I never got any answers, this is the solution I've been going with. Essentially if the inner collection view isn't at the top or bottom, I reset the y offset change the outer scroll view has in scrollViewDidScroll. Code looks like this:

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
    if (scrollView == _outerScrollView) {
        if (![self innerScrollIsAtTop] && ![self innerScrollIsAtBottom] && !self.allowOuterScroll) {
            [scrollView setContentOffset:CGPointMake(scrollView.contentOffset.x, self.lastContentOffset)];
        }
        self.lastContentOffset = scrollView.contentOffset.y;
    }
}

using these 2 conveniences:

- (BOOL)innerScrollIsAtTop {
    return _innerCollectionView.contentOffset.y <= 0;
}

- (BOOL)innerScrollIsAtBottom {
    CGFloat zoom = self.zoomScale;
    CGFloat height = _innerCollectionView.frame.size.height;
    CGFloat contentHeight = _innerCollectionView.contentSize.height;
    CGPoint offset = _innerCollectionView.contentOffset;

    return offset.y + height / zoom >= contentHeight;
}

And you'll need 2 class variables, a float to hold the previous y content offset of the outer scroll, and a BOOL to hold whether you want to allow the outer scroll view to scroll, which you can set to YES while zooming or programatically scrolling. This solution fixes the double scroll, but does have a cumbersome hack within scrollviewDidScroll that may bite you later and you constantly need to work around, but for now this is the solution I've been using.