Eloy B.
Eloy B.

Reputation: 605

UICollectionView CompositionalLayout not calling UIScrollDelegate

Here is my problem:

1. ScrollViewDelegate does not get called once UICollectionView implements compositionalLayout.

With flowLayout and UICollectionViewDataSource the scrollView delegates get called. Once I implement the diffable datasource and CompositionalLayout the scrollView delegates don't get called anymore.

2. collectionView.decelerationRate = .fast gets ignored when implementing CompositionalLayout

My understanding is that UICollectionViewDelegate should call UIScrollViewDelegate, I have looked wide and far but no luck. Can someone point me out what am I missing? Am I integrating compositionalLayout wrong?

here is my code:



import UIKit

class ViewController: UIViewController, UICollectionViewDelegate, UIScrollViewDelegate {
    
    enum Section {
        case weekdayHeader
    }
    
    @IBOutlet weak var collectionView: UICollectionView!
    
    let weekdays = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday","Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
    
    var weekdaysArr = [Weekdays]()
    
    var dataSource: UICollectionViewDiffableDataSource<Section, Weekdays>! = nil
    
    private let cellReuseIdentifier = "myCell"
    
    
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.collectionView.delegate = self
        self.collectionView.register(UINib(nibName: "MyCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: cellReuseIdentifier)
        self.collectionView.decelerationRate = .normal
        
        configureCollectionView()
        configureDataSource()
        configureWeekdays()
        updateCollectionView()
        
        // Do any additional setup after loading the view.
    }
    
    func configureCollectionView(){
        self.collectionView.delegate = self
        self.collectionView.collectionViewLayout = generateLayout()
    }
    
    func generateLayout() -> UICollectionViewLayout {
        
        let itemSize = NSCollectionLayoutSize(
            widthDimension: .estimated(10),
            heightDimension: .fractionalHeight(1))
        let weekdayItem = NSCollectionLayoutItem(layoutSize: itemSize)
        //        weekdayItem.contentInsets = NSDirectionalEdgeInsets(top: 5, leading: 50, bottom: 5, trailing: 50)
        
        let groupSize = NSCollectionLayoutSize(
            widthDimension: .fractionalWidth(0.3),
            heightDimension: .fractionalHeight(1.0))
        let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [weekdayItem])
        
        //        let spacing = CGFloat(10)
        //        group.interItemSpacing = .fixed(spacing)
        
        let section = NSCollectionLayoutSection(group: group)
        section.interGroupSpacing = CGFloat(20)
        section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 500)
        section.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary
        
        
        
        
        let layout = UICollectionViewCompositionalLayout(section: section)
        
        return layout
        
    }
    
    func configureDataSource() {
        dataSource = UICollectionViewDiffableDataSource
            <Section, Weekdays>(collectionView: collectionView)
            { (collectionView: UICollectionView, indexPath: IndexPath, weekday: Weekdays) -> UICollectionViewCell? in
                guard let cell = collectionView.dequeueReusableCell(withReuseIdentifier: self.cellReuseIdentifier, for: indexPath) as? MyCollectionViewCell else {
                    fatalError("Cannot create a new cell") }
                cell.titleLabel.text = weekday.name
                print("weekday.name = \(weekday.name)")
                return cell
        }
        //      let snapshot = snapshotForCurrentState()
        //      dataSource.apply(snapshot, animatingDifferences: false)
    }
    
    func configureWeekdays(){
        
        weekdaysArr.append(Weekdays(name: "Monday"))
        weekdaysArr.append(Weekdays(name: "Tuesday"))
        weekdaysArr.append(Weekdays(name: "Wednesday"))
        weekdaysArr.append(Weekdays(name: "Thursday"))
        weekdaysArr.append(Weekdays(name: "Friday"))
        weekdaysArr.append(Weekdays(name: "Saturday"))
        weekdaysArr.append(Weekdays(name: "Sunday"))
        
    }
    
    func updateCollectionView(){
        var snapshot = NSDiffableDataSourceSnapshot<Section, Weekdays>()
        snapshot.appendSections([.weekdayHeader])
        snapshot.appendItems(weekdaysArr, toSection: .weekdayHeader)
        dataSource.apply(snapshot, animatingDifferences: false)
    }
    
    
    
    //MARK: Scroll View Delegate
    
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        print("ScrollView decel rate: \(scrollView.decelerationRate)")
    }
    
    public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
        print("Begin")
    }
    
    func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
        print("End")
    }
    
    func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
        print("END")
    }
    
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
          
          print("Did select a cell here")
          
         }
    
    
}

struct Weekdays: Hashable {
    let identifier: UUID = UUID()
    let name: String
    
    func hash(into hasher: inout Hasher){
        return hasher.combine(identifier)
    }
    
    static func == (lhs: Weekdays, rhs: Weekdays) -> Bool {
        return lhs.identifier == rhs.identifier
    }
}


Thank you

Edit: Updated Code -

Edit: Comparing against Apples sample code from WWDC I found out this behaviour.

If I use the sample codes Grid layout

func gridLayout() -> UICollectionViewLayout {
        let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(0.2),
                                             heightDimension: .fractionalHeight(1.0))
        let item = NSCollectionLayoutItem(layoutSize: itemSize)

        let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0),
                                              heightDimension: .fractionalWidth(0.2))
        let group = NSCollectionLayoutGroup.horizontal(layoutSize: groupSize,
                                                         subitems: [item])

        let section = NSCollectionLayoutSection(group: group)

        let layout = UICollectionViewCompositionalLayout(section: section)
        return layout
    }

When having only 7 Items the Scroll Delegate methods never get called.

I can still scroll though and the expected behavior should be that the scroll delegate methods do get called as I have vertical bounce.

When having about 40 items though and the content is obviously beyond the screen size the scroll delegate does get called.

Now interestingly when loading my own layout:

func generateLayout() -> UICollectionViewLayout {
        
        let itemSize = NSCollectionLayoutSize(
            widthDimension: .estimated(10),
            heightDimension: .fractionalHeight(1))
        let weekdayItem = NSCollectionLayoutItem(layoutSize: itemSize)
        //        weekdayItem.contentInsets = NSDirectionalEdgeInsets(top: 5, leading: 50, bottom: 5, trailing: 50)
        
        let groupSize = NSCollectionLayoutSize(
            widthDimension: .fractionalWidth(0.3),
            heightDimension: .fractionalHeight(1.0))
        let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [weekdayItem])
        
        //        let spacing = CGFloat(10)
        //        group.interItemSpacing = .fixed(spacing)
        
        let section = NSCollectionLayoutSection(group: group)
        section.interGroupSpacing = CGFloat(20)
        section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 500)
        section.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary
        
        
        
        
        let layout = UICollectionViewCompositionalLayout(section: section)
        
        return layout
        
    }

The Scroll delegates never get called. Even with 40+ Items.

Any ideas as to how to force UICollectionView to communicate with its ScrollViewDelegates?

Upvotes: 22

Views: 10292

Answers (7)

Shivansh Sinha
Shivansh Sinha

Reputation: 11

I tried using

section.visibleItemsInvalidationHandler = { [weak self] visibleItems, point, environment in }

but the method is getting invoked multiple times on scroll. So the best solution that worked for me was to use timestamp and check the difference in timestamp and only if it is greater than some value say 1 or 2 then only invoke the action.

private var currentTimeStamp: TimeInterval = NSDate().timeIntervalSince1970 // private parameter to avoid multiple call on scroll, defined outside

section.visibleItemsInvalidationHandler = { [weak self] visibleItems, location, layoutEnvironment in
        guard let self = self else { return }
        let newTimeStamp = NSDate().timeIntervalSince1970
        let delta = newTimeStamp - self.currentTimeStamp
        if delta > 2 { 
            performAction()
        }
        self.currentTimeStamp = newTimeStamp
    }

Upvotes: 1

priyanka.saroha
priyanka.saroha

Reputation: 239

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.

Upvotes: 7

Arthrix
Arthrix

Reputation: 29

UICollectionViewDelegate will call the scrollDidView if you vertically scroll the collectionView. In my situation ,I trying to track from horizontal scroll, I'm using this following code to make things work.

func collectionView(_ collectionView: UICollectionView, didEndDisplayingCell cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
      let visibleRect = CGRect(origin: yourCollectionView.contentOffset, size: photoCollectionView.bounds.size)
      let visiblePoint = CGPoint(x: visibleRect.midX, y: visibleRect.midY)
      let visibleIndexPath = yourCollectionView.indexPathForItem(at: visiblePoint) // Current display cell in collectionView
}

It's not the answer for this question, but hope this help person who searching for tracking current display cell from horizontal scroll in collectionView compositional layout

Upvotes: 2

MasterGoGo
MasterGoGo

Reputation: 98

Depending on your use-case, you could also use collection view delegate methods

func collectionView(_ collectionView: UICollectionView, willDisplay cell: UICollectionViewCell, forItemAt indexPath: IndexPath)

func collectionView(_ collectionView: UICollectionView, didEndDisplayingCell cell: UICollectionViewCell, forItemAt indexPath: IndexPath)

In these delegate methods - you can react when a specific section is about to display a cell to end displaying a cell

Upvotes: -1

Bryan Gula
Bryan Gula

Reputation: 490

The answer is using the visibleItemsInvalidationHandler closure on the section (NSCollectionLayoutSection).

This handler receives updates whenever an event results in an animation. It operates similar to scrollViewDidScroll and on horizontal scrolling groups it will be passed all the items, the scroll view offset and the NSCollectionLayoutEnvironment.

https://developer.apple.com/documentation/uikit/nscollectionlayoutsection/3199096-visibleitemsinvalidationhandler

Example:

section.visibleItemsInvalidationHandler = { [weak self] visibleItems, point, environment in
    self?.currentIndex = visibleItems.last?.indexPath.row
}

Upvotes: 36

Leonif
Leonif

Reputation: 482

code snippet:

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

Upvotes: 11

Eloy B.
Eloy B.

Reputation: 605

Appears to be an unexpected behaviour of horizontal scrolling groups within UICollectionView compositional layout

In case someone else encounters the same issue. It seems to be an issue with horizontal scrolling compositional layout groups.

only vertical scrolling groups will trigger the UIScrollViewDelegates.

Unfortunately, it also means that it seems decelerationrate.fast cannot be applied to a horizontal scroll. It will just be ignored.

Steps to reproduce the issue and analysis

This behavior can be recreated by implementing UIScrollViewDelegate methods in the wwdc example code "Advancements in Collection View Layout" here: https://developer.apple.com/videos/play/wwdc2019/215/ In the class: OrthogonalScrollBehaviorViewController.swift we find horizontal and vertical scrolling groups.

Conclusion

UIScrollViewDelegate only interacts with vertical scrolling groups. Horizontal scrolling groups do not communicate with the collection view's scroll delegate.

If there is a need for scroll delegate methods in horizontal scrolling groups than a traditional approach with nested CollectionViews and FlowLayout / Custom layout is still needed.

Remarks

If someone can point out to me that I am missing something I'd be very grateful until then this will stand as the answer to my above-stated issue.

Thanks to all who have commented.

Upvotes: 12

Related Questions