13

In the video Advances in Collection View Layout - WWDC 2019, Apple introduces a new 'orthogonal scrolling behavior' feature. I have a view controller almost identical to OrthogonalScrollingViewController in their example code. In particular my collection view is laid out vertically, and each section can scroll horizontally (I use section.orthogonalScrollingBehavior = .groupPaging).

I want to have all my sections scroll horizontally in unison. Previously, I listened for scrollViewDidScroll on each horizontal collection view, then manually set the content offset of the others. However, with the new orthogonalScrollingBehavior implementation, scrollViewDidScroll never gets called on the delegate when I scroll horizontally. How can I detect horizontal scrolling events with the new API?

If there's another way to make the sections scroll together horizontally, I'm also open to other suggestions.

Chris Chute
  • 3,229
  • 27
  • 18
  • Sounds like an enhancement request to Apple is in order. – matt Dec 28 '19 at 20:31
  • 2
    OK, I just filed a feature request with Apple. I'll leave this question up for now and hopefully someone else has an temporary solution. – Chris Chute Dec 28 '19 at 21:28
  • Hi Chris, did you by any chance find a solution to this problem? I sort of have the same problem –– I want all rows in collection view scroll simultaneously orthogonally –– and I am deploying for iOS 11 and later so I cannot even use the compositional layout stuff... Kinda trapped in here :( – Houston Duane Jun 21 '21 at 07:45
  • 1
    @HoustonDuane if you're not using compositional layout, you can just listen for scrollViewDidScroll on each horizontal collection view, and set the content offset of the other horizontal scroll views to match. – Chris Chute Jun 24 '21 at 01:45
  • @ChrisChute Million thanks for the answer, but as we go deeper on this implementation there is much more to think about: – Houston Duane Jul 02 '21 at 03:25
  • @ChrisChute 1. I embed the horizontal scroll views in table view cells, so when I swipe up, I will bring up some new table view cells – new horizontal scroll views – and I need to make sure these newborns are set the content offset correctly; – Houston Duane Jul 02 '21 at 03:25
  • @ChrisChute 2. when there are tens of horizontal scroll views on screen, will there be potential frame lost while I'm scrolling due to performance? – Houston Duane Jul 02 '21 at 03:25
  • @ChrisChute 3. what should I do to make sure all horizontal views are laid out in the same round of runloop because notifications are often async? If the notifications are handled in different layout cycles, will there be noticeable x difference? – Houston Duane Jul 02 '21 at 03:27
  • If you have a relatively small (<100) number of cells, I would recommend keeping all the cells in memory rather than reusing cells. If they're all in an array, on each scrollViewDidScroll invocation, you can just iterate over all the other cells and set the contentOffset. I've used this technique before and performance hasn't been an issue. – Chris Chute Jul 11 '21 at 19:17

6 Answers6

4

You can use this callback:

let section = NSCollectionLayoutSection(group: group)

section.visibleItemsInvalidationHandler = { [weak self] (visibleItems, offset, env) in
}
Ilya
  • 141
  • 2
  • 14
  • I don't quite see how this would help you scrolling different orthogonal sections? Maybe I'm missing something – Chris Jul 30 '20 at 15:33
  • @Chris You could possibly check visibleItems.last?.indexPath or visibleItems.first?.indexPath to figure out which collection view section or item is visible. – iAmcR Jun 09 '21 at 15:48
  • 1
    This isn't *quite* the correct solution as if you want to do stuff based on whether or not a finger is still dragging you can't, but this is very very close. Probably the best we're going to get until later this year (i hope) – nickneedsaname Apr 09 '22 at 00:20
  • @nickneedsaname Here's the solution to your problem - https://stackoverflow.com/a/72139292/8704900 – Faizyy May 06 '22 at 09:38
2

As mentioned you can use visibleItemsInvalidationHandler which provides the location of the scroll offset.

You can detect if a page changed by getting the modulus of the page width. You need to additionally supply a tolerance to ignore halfway scroll changes.

Im using this:

class CollectionView: UICollectionViewController {
    
    
    private var currentPage: Int = 0 {
        didSet {
            if oldValue != currentPage {
                print("The page changed to \(currentPage)")
            }
        }
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        // Configure layout...
        let itemSize = NSCollectionLayoutSize...
        let item = NSCollectionLayoutItem...
        let groupSize = NSCollectionLayoutSize...
        let group = NSCollectionLayoutGroup.horizontal...
        let section = NSCollectionLayoutSection(group: group)
        section.orthogonalScrollingBehavior = .groupPaging

        
        // Use visibleItemsInvalidationHandler to make calculations
        section.visibleItemsInvalidationHandler = {  [weak self] items, location, environment in
            guard let self = self else { return }
            let width = self.collectionView.bounds.width
            let scrollOffset = location.x
            let modulo = scrollOffset.truncatingRemainder(dividingBy: width)
            let tolerance = width/5
            if modulo < tolerance {
                self.currentPage = Int(scrollOffset/width)
            }
        }
        
        self.collectionView.collectionViewLayout = UICollectionViewCompositionalLayout(section: section)
    }
    
}
MH175
  • 2,234
  • 1
  • 19
  • 35
1

Here's a hacky solution. Once you render your orthogonal section, you can access it via the subviews on your collectionView. You can then check if the subview is subclass of UIScrollView and replace the delegate.

collectionView.subviews.forEach { (subview) in
    if let v = subview as? UIScrollView {
        customDelegate.originalDelegate = v.delegate!
        v.delegate = customDelegate
    }
}

One tricky bit is that you want to capture its original delegate. The reason for this is because I notice that you must call originalDelegate.scrollViewDidScroll(scrollView) otherwise the section doesn't render out completely.

In other word something like:

class CustomDelegate: NSObject, UIScrollViewDelegate {
    var originalDelegate: UIScrollViewDelegate!
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        originalDelegate.scrollViewDidScroll?(scrollView)
    }
}
noobular
  • 3,257
  • 2
  • 24
  • 19
1

You can do this:

section.visibleItemsInvalidationHandler = { [weak self] visibleItems, point, environment in
            let indexPath = visibleItems.last!.indexPath
            self?.pageControl.currentPage = indexPath.row
}
sejmy
  • 11
  • 1
1

I have found one convenient way to handle this issue, you can avoid setting orthogonal scrolling and use configuration instead this way:

let config = UICollectionViewCompositionalLayoutConfiguration()
config.scrollDirection = .horizontal
let layout = UICollectionViewCompositionalLayout(sectionProvider:sectionProvider,configuration: config)

This will call all scroll delegates for collectionview. Hope this will be helpful for someone.

Jeremy Caney
  • 7,102
  • 69
  • 48
  • 77
priyanka.saroha
  • 201
  • 1
  • 8
1

The collectionView delegate willDisplay method will tell you when a cell is added to the collectionView (e.g. is displayed on screen, as they are removed when they go offscreen).

That should let you know that panning has effectively occurred (and in most cases the important part is not the pan gesture or animation but how it affects the displayed content).

In that delegate method, collectionView.visibleCells can be used to determine what cells are displayed and from that one can derive the position.

clearlight
  • 12,255
  • 11
  • 57
  • 75