34

I have two nested UIScrollViews, both scrolling in the vertical direction. I need the outer scrollview to scroll to it's max range first before allowing the inner scrollview to scroll. The inner scrollview should not be scrollable until the outer scrollview has reached it's max range. Here's an illustration: Nested Scrollviews Diagram

In the left diagram, a vertical drag inside of Scrollview B should move Scrollview A and Scrollview B should not be scrollable (but it still needs to be able to receive touches/taps). Once Scrollview A reaches it's max range (when Scrollview B gets to the top of the screen), then Scrollview B should scroll. This needs to work in one continuous motion.

I've attempted to toggle ScrollView B's scrollEnabled from ScrollView A's scrollViewDidScroll: delegate method, but this doesn't appear to be a viable solution because it doesn't work in one continuous motion (eg: The user needs to release and touch again after Scrollview B reaches the top of the screen).

What's the best way to implement this such that is works in one continuous motion?

user2393462435
  • 2,652
  • 5
  • 37
  • 45
  • You could place a transparent, sibling view on top of the scrollViews and use it to intercept gestures. Then, you forward them to one scrollView or the other depending on how far they've respectively scrolled. Or -- a completely different idea: I bet there's some way to do it with clever autolayout constraints... – Anna Dickinson Dec 12 '14 at 22:34
  • The tricky part about the overlay strategy is that Scrollview B still needs to accept touches (eg: to tap a button). So just forwarding all touches depending on the scrollview position wouldn't work. – user2393462435 Dec 12 '14 at 22:44
  • Another idea: you can stop a scrollView from scrolling by setting its contentOffset to a constant value in layoutSubviews. You could use that to stop B from scrolling until it's at the correct position with respect to A. That wouldn't have the delegate problem -- you don't need to wait for scrolling to end before setting the contentOffset. – Anna Dickinson Dec 13 '14 at 07:10
  • 1
    do you finally got the solution of this problem – Ankit Sachan Jan 09 '15 at 10:45
  • 2
    is any solution found?? : – Just a coder Feb 02 '16 at 23:17
  • 2
    @user2393462435 Did you get any solution for this problem ? – Kamala Dash Apr 19 '16 at 10:40

5 Answers5

5

I solved the problem in the following way. I am not really happy with it since it looks to me much too complicated, but it works (please note that the code below is a simplified, untested version of my code, which is due to a different UI more complicated):

I have 3 properties that control scrolling:

@property (nonatomic, assign) CGFloat currentPanY;
@property (nonatomic, assign) BOOL    scrollA;
@property (nonatomic, assign) BOOL    scrollB;

2-step scrolling:

Disable scrolling for B, and enable scrolling for A.
This allows to scroll A .

When A reaches its max position, disable scrolling for A, and enable scrolling for B:

-(void)scrollViewDidScroll: (UIScrollView *)scrollView {
    if (scrollView.contentOffset.y >= self.maxScrollUpOffset) {
        [scrollView setContentOffset:CGPointMake(0, self.maxScrollUpOffset) animated:NO];        
        self.scrollviewA.scrollEnabled = NO;
        self.scrollviewB.scrollEnabled = YES;
        self.scrollB = YES;
    }
}

This gives the following effect:
When A is scrolled upwards it will stop scrolling when its max size is reached. However B will not start scrolling, since the pan gesture recognizer of A does not forward its actions to the pan gesture recognizer of B. Thus one has to lift the finger and to start a 2nd scrolling. Then, B will scroll. This gives the 2-step scrolling.

Continuous scrolling:

For continuous scrolling, B must scroll while the finger that started scrolling of A continues moving upwards. To detect this, I added a further pan gesture recognizer tho A, and enabled it to detect gestures simultaneously with the built-in gesture recognizers of A and B:

 - (BOOL)gestureRecognizer:(UIPanGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UISwipeGestureRecognizer *)otherGestureRecognizer {
    return YES;
}

In the action of this additional pan gesture recognizer, I compute the distance the finger has moved upwards after the scrolling limit of A has been reached. By this distance, B is then scrolled programmatically:

- (void)panGestureRecognizerAction:(UIPanGestureRecognizer *)recognizer {
    if (recognizer.state != UIGestureRecognizerStateChanged) {
        self.currentPanY = 0;
        self.scrollB = NO;
        self.scrollA = NO;
    } else {
        CGPoint currentTranslation = [recognizer translationInView:self.scrollviewA];
        CGFloat currentYup = currentTranslation.y;

        if (self.scrollA || self.scrollB) {
            if (self.currentPanY == 0) {
                self.currentPanY = currentYup;
            }

            CGFloat additionalYup = self.currentPanY - currentYup;
            if (self.scrollA) {
                CGFloat offset = self.scrollviewA.scrollUpOffset + additionalYup;
                if (offset >= 0) {
                    self.scrollviewA.contentOffset = CGPointMake(0, offset);
                } else {
                    self.scrollviewA.contentOffset = CGPointZero;
                }
            } else if (self.scrollB){
                self.scrollviewB.contentOffset = CGPointMake(0, additionalYup);
            }
        }
    }
}  

There is still a disadvantage:
If you start scrolling, lift the finger, and let the scrollView decelerate, it will behave like the 2-stage scrolling, since the additional pan gesture recognizer won’t recognize any pan gesture.

Reinhard Männer
  • 14,022
  • 5
  • 54
  • 116
  • Hi @ReinhardManner would you kindly be able to help me with my question please? [link](https://stackoverflow.com/questions/52665922/smooth-transition-between-2-scrollviews) I am trying to implement what you did but not able to achieve the continuous scrolling – j.iese Oct 06 '18 at 07:23
5

In my case I solved subclassing UIScrollView for the outer ScrollView.

class MYOuterScrollView: UIScrollView, UIGestureRecognizerDelegate
{
    override func gestureRecognizerShouldBegin(gestureRecognizer: UIGestureRecognizer) -> Bool
    {
        return true
    }

    func gestureRecognizer(gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWithGestureRecognizer otherGestureRecognizer: UIGestureRecognizer) -> Bool
    {
         return true
    }
}
Bruno Bieri
  • 9,724
  • 11
  • 63
  • 92
Davide Candita
  • 386
  • 2
  • 9
  • Hi @DavideCandita would you kindly be able to help me with my question please? [link](https://stackoverflow.com/questions/52665922/smooth-transition-between-2-scrollviews) I tried subclassing UIScrollView for my outer ScrollView but was unable to get it working. Would you be able to explain how I could apply this to my question in the link – j.iese Oct 06 '18 at 07:28
3

Your requirement that this should work in one continuous motion calls out the answer: you need to use just one UIScrollView, not two.

If you have just one scrollview you can then perform your magic by overriding the scrollview's layoutSubviews method and rejigging its content to perform your parallax effect based on the current value of contentOffset. Make sure that contentSize always reflect the full height (you can even update contentSize inside layoutSubviews if needs be).

For an architecture, take your existing diagram and just replace Scrollview B with View B.

Clafou
  • 15,250
  • 7
  • 58
  • 89
  • I wonder if this can be done with nested scroll views by turning off scrolling for all but the outermost scroll view, and having this outermost parent update the other scroll views' `frame`s in its `layoutSubviews` method. In particular, this would be appealing if it worked for nested `UICollectionView` instances. My guess is it wouldn't work, but it'd be nice. Could probably set it up with layout constraints too. – Benjohn Oct 26 '15 at 13:34
  • 1
    A really nice way to implement this would be with an `NestedScrollViewHostView` that would contain a `scollView` instance. It's `layoutSubviews` method would look up the responder chain to find top most `UIScrollView`, pick up its frame, convert this to local coordinates, and apply it to the contained `UIScrollView` child (which might be a collection or list view, of course). Adding the `scrollView` would turn off the child's scrolling. This would allow nesting a `UICollectionView` in a `UICollectionView`, and having the child only create cells as they become visible. – Benjohn Oct 26 '15 at 13:50
  • @Benjohn What if the scrollViewB is a child of horizontal scrollView placed upon the scrollViewA as in this question - http://stackoverflow.com/q/35745353/5615133 . I have an exact requirement as this. Can you please have a look over the above shared question? – G.Abhisek Mar 04 '16 at 07:13
  • I'm afraid I don't understand the requirement in the question that you linked to, sorry. I tried out the technique I described and it seemed like it would work. I didn't put it in to a product though, so I can't guarantee that there aren't fundamental issues with it. It is also moderately involved, so I'd give it a miss if you can. – Benjohn Mar 04 '16 at 14:25
1

The gesture recognizer for scroll view A would need to pass off to the gesture recognizer for scroll view B to have on continuous motion which I am pretty sure is not possible. Why not combine the content of the two scroll views instead and then you would have one continuous motion. This code will combine the content of scrollView A and B into just A.

UIScrollView* scrollViewA = ...;
UIScrollView* scrollViewB = ...;
NSArray* subviews = scrollViewB.subviews;
for (int i = 0; i < subviews.count; i++)
{
    UIView* subview = [subviews objectAtIndex:i];
    CGRect frame = subview.frame;
    frame.origin.y += scrollViewA.contentSize.height;
    subview.frame = frame;
    [scrollViewA addSubview:subview];
}
CGSize size = scrollViewA.contentSize;
size.height += scrollViewB.contentSize.height;
scrollViewA.contentSize = size;
Stephen Johnson
  • 5,293
  • 1
  • 23
  • 37
  • What if the scrollViewB is a child of horizontal scrollView placed upon the scrollViewA as in this question - http://stackoverflow.com/q/35745353/5615133 . I have an exact requirement as this. Can you please have a look over the above shared question? – G.Abhisek Mar 04 '16 at 07:16
-1

Same-Direction Scrolling

Same direction scrolling occurs when a UIScrollView that is a subview of a UIScrollView both scroll in the same direction. This is shown in the left image.

Follow this link if want to look in details https://developer.apple.com/library/ios/documentation/WindowsViews/Conceptual/UIScrollView_pg/NestedScrollViews/NestedScrollViews.html

enter image description here

Just set some attributes which are mentioned in above image. it works.

Sarat Patel
  • 856
  • 13
  • 32