1

Question

How can I get the full scrollable content width of a horizontal collection view section using Compositional Layout?

Details

I am implementing a UICollectionView with Compositional Layout so I can get the nice and easy section.orthogonalScrollingBehavior = .groupPaging behavior. However, I also need to know the percentage that the user has scrolled through the section so I can keep a custom PageControl in sync. With other collection views that use UICollectionViewFlowLayout this was a straightforward calculation:

func scrollViewDidScroll(_ scrollView: UIScrollView) {
    let scrollProgress = scrollView.contentOffset.x / scrollView.contentSize.width
    pageControl.currentPercentage = scrollProgress 

I've already learned that I need to use visibleItemsInvalidationHandler instead of this delegate method. My problem is that with Compositional Layout, the value of both layoutEnvironment.container.contentSize.width and collectionView.contentSize.width is no longer the full width of the scrollable content, it's now equal to the frame of the collectionView itself. The value of scrollOffset is in terms of the full scrollable content size, though.

section.visibleItemsInvalidationHandler = { [weak self] visibleItems, scrollOffset, layoutEnvironment in
    guard let self = self else { return }
    let contentWidth = layoutEnvironment.container.contentSize.width // not the full content width
    //let contentWidth = self.collectionView.contentSize.width // neither is this!
    let scrollProgress = scrollPosition / contentWidth
    self.pageControl.currentPercentage = scrollProgress
}

I've tried digging around in layoutEnvironment and found nothing equal to the full scrollable content size. I've also tried computing it based on the number of cells but I haven't been able to get it right because of needing to account for contentOffsets and inter-group spacing.

My full layout code and screenshots of the resulting UI is below, in case it helps. Maybe there's a way to change this layout to achieve the same result and get the right value in contentSize.

let layout: UICollectionViewLayout = {
    let itemWidth = itemSize.width - 2 * Const.margin
    let itemLayoutSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .fractionalHeight(1.0))
    let item = NSCollectionLayoutItem(layoutSize: itemLayoutSize)

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

    let section = NSCollectionLayoutSection(group: group)
    section.contentInsets = NSDirectionalEdgeInsets(top: 0, leading: Const.margin, bottom: 0, trailing: Const.margin)
    section.interGroupSpacing = Const.interItemSpacing
    section.orthogonalScrollingBehavior = .groupPaging
    section.visibleItemsInvalidationHandler = { /* ... */ }

    return UICollectionViewCompositionalLayout(section: section)
}()

Starting position:

starting position

Scrolled to next item:

scrolled to next item

Rich Ellis
  • 97
  • 8

1 Answers1

0

In case it helps anyone else: it's not the solution I was hoping for but I eventually figured out how to compute the content width based on the number of cells. For my layout, the only thing I had to consider was the group width (same as the item width) and the inter-group spacing:

func contentWidth(forCellCount cellCount: Int) -> CGFloat {
    (itemWidth + Const.interItemSpacing) * CGFloat(cellCount)
}

Note that I don't have access to the cellCount inside the visibleItemsInvalidationHandler so I had to (weakly) capture self in that closure and call a method to be able to get that value:

section.visibleItemsInvalidationHandler = { [weak self] _, scrollOffset, _ in
    self?.handleHorizontalScroll(scrollOffset: scrollOffset)
}
Rich Ellis
  • 97
  • 8