Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Continuous vertical scrolling between UICollectionView nested in UIScrollView

While I know nested scrollViews aren't ideal, our designers provided me with this setup, so I'm doing my best to make it work. Let's begin!

View Hierarchy

  • UIView
    • UIScrollView (Vertical Scrolling Only)
      • UIImageView
      • UICollectionView #1 (Horizontal Scrolling Only)
      • UIImageView (different from previous UIImageView)
      • UICollectionView #2 (Vertical Scrolling Only)

Important Note

All my views are defined using programmatic Auto Layout. Each successive view in the UIScrollView's subview hierarchy has a y-coordinate dependency on the view that came before it.

The Problem

For the sake of simplicity, let's modify the nomenclature a bit:

  • _outerScrollView will refer to UIScrollView
  • _innerScrollView will refer to UICollectionView #2

I'd like for my _outerScrollView to route its touch event to the _innerScrollView upon reaching the bottom of its contentSize. I'd like the reverse to happen when I scroll back up.

At present, I have the following code:

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView
{
    CGFloat bottomEdge = [scrollView contentOffset].y + CGRectGetHeight(scrollView.frame);
    if (bottomEdge >= [_outerScrollView contentSize].height) {
        _outerScrollView.scrollEnabled = NO;
        _innerScrollView.scrollEnabled = YES;
    } else {
        _outerScrollView.scrollEnabled = YES;
        _innerScrollView.scrollEnabled = NO;
    }
}

where the initial conditions (before any scrolling occurs) is set to:

outerScrollView.scrollEnabled = YES;
innerScrollView.scrollEnabled = NO;

What happens?

Upon touching the view, the outerScrollView scrolls until its bottom edge, and then has a rubber band effect due to _outerScrollView.bounces = YES; If I touch the view again, the innerScrollView scroll until it hits its bottom edge. On the way back up, the same rubber banding effect occurs in the reverse order. What I want to happen is have a fluid motion between the two subviews.

Obviously, this is due to the scrollEnabled conditions that are set in the conditional in the code snippet. What I'm trying to figure out is how to route the speed/velocity of one scrollView to the next scrollView upon hitting an edge.

Any assistance in this issue would be greatly appreciated.

Other Notes

  1. This did not work for me: https://github.com/ole/OLEContainerScrollView
  2. I am considering putting everything in the UIScrollView hierarchy (except for UICollectionView #2) inside UICollectionView #2 supplementaryView. Not sure if that would work.
like image 342
ArtSabintsev Avatar asked Sep 11 '14 16:09

ArtSabintsev


2 Answers

Figured it out!

First:

_scrollView.pagingEnabled = YES;

Second:

- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
    if (scrollView == _scrollView || scrollView == _offersCollectionView) {

        CGFloat offersCollectionViewPosition = _offersCollectionView.contentOffset.y;
        CGFloat scrollViewBottomEdge = [scrollView contentOffset].y + CGRectGetHeight(scrollView.frame);

        if (scrollViewBottomEdge >= [_scrollView contentSize].height) {
            _scrollView.scrollEnabled = NO;
            _offersCollectionView.scrollEnabled = YES;
        } else if (offersCollectionViewPosition <= 0.0f && [_offersCollectionView isScrollEnabled]) {
            [_scrollView scrollRectToVisible:[_scrollView frame] animated:YES];
            _scrollView.scrollEnabled = YES;
            _offersCollectionView.scrollEnabled = NO;
        }
    }
}

Where:

  • _scrollView is the _outerScrollView
  • _offersCollectionView is the _innerScrollView (which was UICollectionView #2 in my original post).

Here's what happens now:

  • When I swipe up (so the view moves down), the offersCollectionView takes over the entire view, moving the other subViews out of the view.
  • If I swipe down (so the views up), the rest of the subviews come back into focus with the scrollView's bounce effect.
like image 55
ArtSabintsev Avatar answered Oct 20 '22 15:10

ArtSabintsev


Accepted answer didn't work for me. Here's what did:

Define a subclass of UIScrollView:

class CollaborativeScrollView: UIScrollView, UIGestureRecognizerDelegate {

    var lastContentOffset: CGPoint = CGPoint(x: 0, y: 0)

    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return otherGestureRecognizer.view is CollaborativeScrollView
    }
}

Since it's not possible to reroute touches to another view, the only way to ensure that the outer scrollview can continue scrolling once the inner one stops is if it had been receiving the touches the whole time. However, in order to prevent the outer one from moving while the inner one is, we have to lock it without setting its isScrollEnabled to false, otherwise it'll stop receiving the touches and won't be able to pick up where the inner one left off when we want to scroll past the inner one's top or bottom.

That's done by assigning a UIScrollViewDelegate to the scrollviews, and implementing scrollViewDidScroll(_:) as shown:

class YourViewController: UIViewController, UIScrollViewDelegate {

    private var mLockOuterScrollView = false

    @IBOutlet var mOuterScrollView: CollaborativeScrollView!
    @IBOutlet var mInnerScrollView: CollaborativeScrollView!

    enum Direction {
        case none, left, right, up, down
    }

    func viewDidLoad() {
        mOuterScrollView.delegate = self
        mInnerScrollView.delegate = self
        mInnerScrollView.bounces = false
    }

    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        guard scrollView is CollaborativeScrollView else {return}
        let csv = scrollView as! CollaborativeScrollView

        //determine direction of scrolling
        var directionTemp: Direction?
        if csv.lastContentOffset.y > csv.contentOffset.y {
            directionTemp = .up
        } else if csv.lastContentOffset.y < csv.contentOffset.y {
            directionTemp = .down
        }
        guard let direction = directionTemp else {return}

        //lock outer scrollview if necessary
        if csv === mInnerScrollView {
            let isAlreadyAllTheWayDown = (mInnerScrollView.contentOffset.y + mInnerScrollView.frame.size.height) == mInnerScrollView.contentSize.height
            let isAlreadyAllTheWayUp = mInnerScrollView.contentOffset.y == 0
            if (direction == .down && isAlreadyAllTheWayDown) || (direction == .up && isAlreadyAllTheWayUp) {
                mLockOuterScrollView = false
            } else {
                mLockOuterScrollView = true
            }
        } else if mLockOuterScrollView {
            mOuterScrollView.contentOffset = mOuterScrollView.lastContentOffset
        }

        csv.lastContentOffset = csv.contentOffset
    }
}

And that's it. This will stop your outer scrollview from scrolling when you begin scrolling the inner one, and get it to pick up again when the inner one is scrolled all the way to one of its ends.

like image 22
shoe Avatar answered Oct 20 '22 15:10

shoe