'How can I detect orthogonal scroll events when using `UICollectionViewCompositionalLayout`?

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.



Solution 1:[1]

You can use this callback:

let section = NSCollectionLayoutSection(group: group)

section.visibleItemsInvalidationHandler = { [weak self] (visibleItems, offset, env) in
}

Solution 2:[2]

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)
    }
}

Solution 3:[3]

You can do this:

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

Solution 4:[4]

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.

Solution 5:[5]

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)
    }
    
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Ilya
Solution 2 noobular
Solution 3 sejmy
Solution 4 Jeremy Caney
Solution 5 MH175