26

How would I make two scroll views follow each others scrolling?

For instance, I have a scroll view (A) on the left of a screen, whose contents can scroll up and down, but not left and right. Scroll view B matches the up and down of A, but can also scroll left and right. Scroll view A is always on the screen.

-----------------------------------------------------------
|             |                                           |
|             |                                           |
|             |                                           |
|     A       |                    B                      |
|             |                                           |
|    scrolls  |                                           |
|   up & down |              scrolls all directions       |
|             |                                           |
-----------------------------------------------------------

How would I make it so the the up and down scrolling (of either view) also makes the other view scroll in the same up-down direction? Or is there another method to do this?

cannyboy
  • 24,180
  • 40
  • 146
  • 252

5 Answers5

47

Set the delegate of scroll view A to be your view controller... then have...

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
  CGPoint offset = scrollViewB.contentOffset;
  offset.y = scrollViewA.contentOffset.y;
  [scrollViewB setContentOffset:offset];
}

If you want both to follow each other, then set delegate for both of them and use...

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
  if([scrollView isEqual:scrollViewA]) {
    CGPoint offset = scrollViewB.contentOffset;
    offset.y = scrollViewA.contentOffset.y;
    [scrollViewB setContentOffset:offset];
  } else {
    CGPoint offset = scrollViewA.contentOffset;
    offset.y = scrollViewB.contentOffset.y;
    [scrollViewA setContentOffset:offset];
  }
}

The above can be refactored to have a method which takes in two scrollviews and matches one to the other.

- (void)matchScrollView:(UIScrollView *)first toScrollView:(UIScrollView *)second {
  CGPoint offset = first.contentOffset;
  offset.y = second.contentOffset.y;
  [first setContentOffset:offset];
}

- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
  if([scrollView isEqual:scrollViewA]) {
    [self matchScrollView:scrollViewB toScrollView:scrollViewA];  
  } else {
    [self matchScrollView:scrollViewA toScrollView:scrollViewB];  
  }
}

Swift 3 Version:

    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        if scrollView == scrollViewA {
            self.synchronizeScrollView(scrollViewB, toScrollView: scrollViewA)
        }
        else if scrollView == scrollViewB {
            self.synchronizeScrollView(scrollViewA, toScrollView: scrollViewB)
        }
    }

    func synchronizeScrollView(_ scrollViewToScroll: UIScrollView, toScrollView scrolledView: UIScrollView) {
        var offset = scrollViewToScroll.contentOffset
        offset.y = scrolledView.contentOffset.y

        scrollViewToScroll.setContentOffset(offset, animated: false)
    }
dvp.petrov
  • 1,110
  • 13
  • 20
Simon Lee
  • 22,304
  • 4
  • 41
  • 45
17

I tried the Simon Lee's answer on iOS 11. It worked but not very well. The two scroll views was synchronized, but using his method, the scroll views would lost the inertia effect(when it continue to scroll after you release your finger) and the bouncing effect. I think it was due to the fact that setting the contentOffset through setContentOffset(offset, animated: false) method causes cyclic calls of the scrollViewDidScroll(_ scrollView: UIScrollView) delegate's method(see this question)

Here is the solution that worked for me on iOS 11:

 // implement UIScrollViewDelegate method
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        if scrollView == self.scrollViewA {
            self.syncScrollView(self.scrollViewB, toScrollView: self.scrollViewA)
        }
        else if scrollView == self.scrollViewB {
            self.syncScrollView(self.scrollViewA, toScrollView: scrollViewB)
        }
    }

    func syncScrollView(_ scrollViewToScroll: UIScrollView, toScrollView scrolledView: UIScrollView) {
        var scrollBounds = scrollViewToScroll.bounds
        scrollBounds.origin.y = scrolledView.contentOffset.y
        scrollViewToScroll.bounds = scrollBounds
    }

So instead of setting contentOffset we are using bounds property to sync the other scrollView with the one that was scrolled by the user. This way the delegate method scrollViewDidScroll(_ scrollView: UIScrollView) is not called cyclically and the scrolling happens very smooth and with inertia and bouncing effects as with a single scroll view.

Dmitry Klochkov
  • 2,484
  • 24
  • 31
  • 1
    I just tried your solution but scrollBounds is always the same value. are you doing something different outside the code you provide? – Jonathanff Nov 05 '17 at 03:34
  • No. Related to the scrolling synchronization of two scroll views, it is all I have in my project and it is working fine. – Dmitry Klochkov Nov 05 '17 at 13:48
  • 1
    This is the correct answer for iOS 11+ . As Dmitry says previous solutions cyclically calls `scrollViewDidScroll` which results into weird behaviour. Setting view's bounds does not call scrollViewDidScroll. – Dick Thunder Jan 28 '19 at 14:18
  • 2
    scrollBounds.origin.x = scrolledView.contentOffset.x , if you use horizontal scrollView – Puji Wahono Jul 25 '19 at 05:01
7

Swift 5.4 // Xcode 13.1

What has worked flawlessly for me was the following:

  1. Create a custom subclass of UIScrollView
  2. Conform to UIGestureRecognizer delegate
  3. Override the gestureRecognizer(_:shouldRecognizeSimultaneouslyWith:) GestureRecognizerDelegate method
public func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return true
    }
  1. Both collection views need to have the same superview. Add both gesture recognizers to your superview
yourSuperview.addGestureRecognizer(scrollView1.panGestureRecognizer)
yourSuperview.addGestureRecognizer(scrollView2.panGestureRecognizer)

Hope this helps!

ChikabuZ
  • 10,031
  • 5
  • 63
  • 86
Bartosz Kunat
  • 1,485
  • 1
  • 23
  • 23
2

Guys i know the question is answered, but decided to share with you here my approach for solving a similar issue that i had, because i believe it is a pretty clean solution. Basically i had to make three collection views scroll together, and what i did, is i made a custom UICollectionView subclass called SharedOffsetCollectionView, that when you set this class to a collection view in storyboards or you instantiate it from code directly, you can have all instances scroll the same.

So with SharedOffsetCollectionView all collection view instances of this class in your app, will scroll the same always. In my opinion it is a clean solution because it requires adding zero logic in your view controllers, it is all contained in this external class, you just have to set the class of your collection view to be SharedOffsetCollectionView and you are done.

The same approach could easily be transferred to UITableViews and UIScrollViews

Hope that is helpful to some you. :)

My solution is written in:

Swift 5.2, XCode 11.4.1

Stoyan
  • 1,265
  • 11
  • 20
0

The answer above all did not quite work our for me, since I run into a cyclic call of scrollViewDidScroll. This happend, because setting the content offset of a scroll view also calls scrollViewDidScoll. I solved it by putting a lock between it, which is set based on if a scroll view is being dragged by the user or not, so the syncing won't happen by setting the content offset:

func scrollViewDidScroll(_ scrollView: UIScrollView) {
    self.synchronizeScrollView(offSet: scrollView.contentOffset.y,
                               scrollViewAIsScrolling: scrollView == scrollViewA,
                               isScroller: scrollView.isDragging)
}

private enum Scroller {
    case page, time
}

private var scroller: Scroller? = nil

func synchronizeScrollView(offSet: CGFloat, scrollViewAIsScrolling: Bool,
                           isScroller: scrollView.isDragging) {
    
    let scrollViewToScroll = scrollViewAIsScrolling ? scrollViewB : scrollViewA
    var offset = scrollViewToScroll.contentOffset
    offset.y = offSet
    scrollViewToScroll.setContentOffset(offset, animated: false)
}

This code can be refactored based on how many scroll views are used and based on who owns them. I won't recommend having one controller being the delegate of many scroll views. I would rather solve it with delegation.

Ali Pacman
  • 719
  • 7
  • 12