Compositional Layout not invoking UIScrollViewDelegate on horizontal scrolling layouts

I have searced far and wide and finally compared Apples WWDC example code. It seems evident that when adding UIScrollViewDelegate methods to the "Orthogonal Section Behaviours" example code, the delegates only get fired when scrolling vertically.


Is this an expected behaviour and UIScrollViewDelegate is not supported horizontally by CompositionalLayout or is it something that will be addressed?


/*
See LICENSE folder for this sample’s licensing information.

Abstract:
Orthogonal scrolling section behaviors example
*/

import UIKit

class OrthogonalScrollBehaviorViewController: UIViewController {
    static let headerElementKind = "header-element-kind"

    enum SectionKind: Int, CaseIterable {
        case continuous, continuousGroupLeadingBoundary, paging, groupPaging, groupPagingCentered, none
        func orthogonalScrollingBehavior() -> UICollectionLayoutSectionOrthogonalScrollingBehavior {
            switch self {
            case .none:
                return UICollectionLayoutSectionOrthogonalScrollingBehavior.none
            case .continuous:
                return UICollectionLayoutSectionOrthogonalScrollingBehavior.continuous
            case .continuousGroupLeadingBoundary:
                return UICollectionLayoutSectionOrthogonalScrollingBehavior.continuousGroupLeadingBoundary
            case .paging:
                return UICollectionLayoutSectionOrthogonalScrollingBehavior.paging
            case .groupPaging:
                return UICollectionLayoutSectionOrthogonalScrollingBehavior.groupPaging
            case .groupPagingCentered:
                return UICollectionLayoutSectionOrthogonalScrollingBehavior.groupPagingCentered
            }
        }
    }
    var dataSource: UICollectionViewDiffableDataSource<Int, Int>! = nil
    var collectionView: UICollectionView! = nil

    override func viewDidLoad() {
        super.viewDidLoad()
        navigationItem.title = "Orthogonal Section Behaviors"
        configureHierarchy()
        configureDataSource()
    }
}

extension OrthogonalScrollBehaviorViewController {

    //   +-----------------------------------------------------+
    //   | +---------------------------------+  +-----------+  |
    //   | |                                 |  |           |  |
    //   | |                                 |  |           |  |
    //   | |                                 |  |     1     |  |
    //   | |                                 |  |           |  |
    //   | |                                 |  |           |  |
    //   | |                                 |  +-----------+  |
    //   | |               0                 |                 |
    //   | |                                 |  +-----------+  |
    //   | |                                 |  |           |  |
    //   | |                                 |  |           |  |
    //   | |                                 |  |     2     |  |
    //   | |                                 |  |           |  |
    //   | |                                 |  |           |  |
    //   | +---------------------------------+  +-----------+  |
    //   +-----------------------------------------------------+

    func createLayout() -> UICollectionViewLayout {

        let config = UICollectionViewCompositionalLayoutConfiguration()
        config.interSectionSpacing = 20

        let layout = UICollectionViewCompositionalLayout(sectionProvider: {
            (sectionIndex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
            guard let sectionKind = SectionKind(rawValue: sectionIndex) else { fatalError("unknown section kind") }

            let leadingItem = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(
                widthDimension: .fractionalWidth(0.7), heightDimension: .fractionalHeight(1.0)))
            leadingItem.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)

            let trailingItem = NSCollectionLayoutItem(layoutSize: NSCollectionLayoutSize(
                widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(0.3)))
            trailingItem.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)
            let trailingGroup = NSCollectionLayoutGroup.vertical(layoutSize: NSCollectionLayoutSize(
                widthDimension: .fractionalWidth(0.3), heightDimension: .fractionalHeight(1.0)),
                                                                 subitem: trailingItem,
                                                                 count: 2)

            let orthogonallyScrolls = sectionKind.orthogonalScrollingBehavior() != .none
            let containerGroupFractionalWidth = orthogonallyScrolls ? CGFloat(0.85) : CGFloat(1.0)
            let containerGroup = NSCollectionLayoutGroup.horizontal(
                layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(containerGroupFractionalWidth),
                                                  heightDimension: .fractionalHeight(0.4)),
                subitems: [leadingItem, trailingGroup])
            let section = NSCollectionLayoutSection(group: containerGroup)
            section.orthogonalScrollingBehavior = sectionKind.orthogonalScrollingBehavior()

            let sectionHeader = NSCollectionLayoutBoundarySupplementaryItem(
                layoutSize: NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                                  heightDimension: .estimated(44)),
                elementKind: OrthogonalScrollBehaviorViewController.headerElementKind,
                alignment: .top)
            section.boundarySupplementaryItems = [sectionHeader]
            return section

        }, configuration: config)
        return layout
    }
}

extension OrthogonalScrollBehaviorViewController {
    func configureHierarchy() {
        collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: createLayout())
        collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        collectionView.backgroundColor = .systemBackground
        collectionView.register(TextCell.self, forCellWithReuseIdentifier: TextCell.reuseIdentifier)
        collectionView.register(ListCell.self, forCellWithReuseIdentifier: ListCell.reuseIdentifier)
        collectionView.register(
            TitleSupplementaryView.self,
            forSupplementaryViewOfKind: OrthogonalScrollBehaviorViewController.headerElementKind,
            withReuseIdentifier: TitleSupplementaryView.reuseIdentifier)
        view.addSubview(collectionView)
        collectionView.delegate = self
    }
    func configureDataSource() {
        dataSource = UICollectionViewDiffableDataSource<Int, Int>(collectionView: collectionView) {
            (collectionView: UICollectionView, indexPath: IndexPath, identifier: Int) -> UICollectionViewCell? in

            // Get a cell of the desired kind.
            guard let cell = collectionView.dequeueReusableCell(
                withReuseIdentifier: TextCell.reuseIdentifier, for: indexPath) as? TextCell
                else { fatalError("Cannot create new cell") }

            // Populate the cell with our item description.
            cell.label.text = "\(indexPath.section), \(indexPath.item)"
            cell.contentView.backgroundColor = .cornflowerBlue
            cell.contentView.layer.borderColor = UIColor.black.cgColor
            cell.contentView.layer.borderWidth = 1
            cell.contentView.layer.cornerRadius = 8
            cell.label.textAlignment = .center
            cell.label.font = UIFont.preferredFont(forTextStyle: .title1)

            // Return the cell.
            return cell
        }
        dataSource.supplementaryViewProvider = {
            (collectionView: UICollectionView, kind: String, indexPath: IndexPath) -> UICollectionReusableView? in
            guard let sectionKind = SectionKind(rawValue: indexPath.section)
                else { fatalError("Unknown section kind") }

            // Get a supplementary view of the desired kind.
            if let header = collectionView.dequeueReusableSupplementaryView(
                ofKind: kind,
                withReuseIdentifier: TitleSupplementaryView.reuseIdentifier,
                for: indexPath) as? TitleSupplementaryView {

                // Populate the view with our section's description.
                header.label.text = "." + String(describing: sectionKind)

                // Return the view.
                return header
            } else {
                fatalError("Cannot create new header")
            }
        }

        // initial data
        var snapshot = NSDiffableDataSourceSnapshot<Int, Int>()
        var identifierOffset = 0
        let itemsPerSection = 18
        SectionKind.allCases.forEach {
            snapshot.appendSections([$0.rawValue])
            let maxIdentifier = identifierOffset + itemsPerSection
            snapshot.appendItems(Array(identifierOffset..<maxIdentifier))
            identifierOffset += itemsPerSection
        }
        dataSource.apply(snapshot, animatingDifferences: false)
    }
}

extension OrthogonalScrollBehaviorViewController: UICollectionViewDelegate {
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        collectionView.deselectItem(at: indexPath, animated: true)
    }
    
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
           print("ScrollView decel rate: \(scrollView.decelerationRate)")
           print("DidIt")
           print("DidIt")

           print("DidIt")

       }
       
       public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
           print("Begin")
       }
       
       func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
           print("End")
       }
       
        func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
           print("END")
       }
}
Post not yet marked as solved Up vote post of MCACH Down vote post of MCACH
4.7k views

Replies

I also hit this problem today, did you find it out. I found out one solution may be or may not be, but the section which is the building block for the layout has sort of method like




section.visibleItemsInvalidationHandler = ({ (visibleItems, point, env) in
                    // print(visibleItems.first)
})



which prints items indexPath and many things but dont know should it be considered or not I dont know, here is the sample code I have been working with.


func createLayout() -> UICollectionViewCompositionalLayout {
     
        let sectionProvider = { (sectionInex: Int, layoutEnvironment: NSCollectionLayoutEnvironment) -> NSCollectionLayoutSection? in
         
            let sectionDefinition = SectionDefinition(rawValue: sectionInex)!
         
            let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(sectionDefinition.itemWidth), heightDimension: .fractionalHeight(1.0))
         
            let items = NSCollectionLayoutItem(layoutSize: itemSize)
         
            items.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)
         
            let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(sectionDefinition.groupWidth), heightDimension: .absolute(sectionDefinition.groupHeight))
         
         
            let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize, subitem: items, count: sectionDefinition.count)
         
            let section = NSCollectionLayoutSection(group: group)
         
            if sectionDefinition == .fullWidth {
                section.visibleItemsInvalidationHandler = ({ (visibleItems, point, env) in
                    // print(visibleItems.first)
                })
            }
         
         
            section.orthogonalScrollingBehavior = .continuous
         
         
            return section
        }
     
        let config = UICollectionViewCompositionalLayoutConfiguration()
     
        config.interSectionSpacing = 20
     
        let layout = UICollectionViewCompositionalLayout(sectionProvider: sectionProvider, configuration: config)
        return layout
    }

And as you said if a collection view is vertical then the scroll view delegates method are fired but not in horizontal/orthogonal collection view. Have you found anything more about it?

Hey I am also facing the same issue after updating collection view layout to composition layout, UIScrollViewDelegate methods is not supporting horizontal scroll. anyone have a work around for this one please share it.

Hi everybody. I also faced this problem. Here's my crutch:

    private var isDragging = false
    private var previousPointX: CGFloat?
    private var dragTimer: Timer?

    private func horizontalScrollHandler(forSection section: NSCollectionLayoutSection, completion: (() -> Void)?) {
        section.visibleItemsInvalidationHandler = { [weak self] (_, point, _) in
            guard let self = self else { return }
            
            if let previousPointX = self.previousPointX, previousPointX != point.x {
                self.isDragging = true
                self.restartDragTimer(completion: completion)
            }
            self.previousPointX = point.x
        }
    }
    
    private func restartDragTimer(completion: (() -> Void)?) {
        self.dragTimer?.invalidate()
        self.dragTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: false) { [weak self] _ in
            self?.isDragging = false
            completion?()
        }
    }

and in the completion block, I make the changes I need.

It is a pity that for so many years Apple has not made attempts to improve (or I didn't find it). :D

If there are better solutions, please write them down.