Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 550
Eloy B. Avatar asked Jan 10 '20 01:01

Eloy B.


2 Answers

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.

like image 193
Eloy B. Avatar answered Sep 27 '22 22:09

Eloy B.


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.

like image 43
priyanka.saroha Avatar answered Sep 27 '22 21:09

priyanka.saroha