Reputation: 605
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
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
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
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
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
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.
Example:
section.visibleItemsInvalidationHandler = { [weak self] visibleItems, point, environment in
self?.currentIndex = visibleItems.last?.indexPath.row
}
Upvotes: 36
Reputation: 482
code snippet:
section.visibleItemsInvalidationHandler = { [weak self] visibleItems, point, environment in
self?.pager.currentPage = visibleItems.last!.indexPath.row
}
Upvotes: 11
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