The proposed offset is where the collection view would stop without our intervention. We peek into this area by finding its centre as proposedContentOffsetCenterX and examine our currently visible cells to see which one’s centre is closer to the centre of that area.
class CenterItemPagingCollectionViewLayout: UICollectionViewFlowLayout {
private var mostRecentOffset: CGPoint = .zero {
didSet {
notifyPageChanged()
}
}
var pagingItemDidChanged: ((Int) -> Void)?
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint,
withScrollingVelocity velocity: CGPoint) -> CGPoint {
if velocity.x == 0 {
return mostRecentOffset
}
guard let cv = self.collectionView else {
mostRecentOffset = super.targetContentOffset(forProposedContentOffset: proposedContentOffset,
withScrollingVelocity: velocity)
return mostRecentOffset
}
let cvBounds = cv.bounds
let halfWidth = cvBounds.size.width * 0.5
guard let attributesForVisibleCells = self.layoutAttributesForElements(in: cvBounds) else {
mostRecentOffset = super.targetContentOffset(forProposedContentOffset: proposedContentOffset,
withScrollingVelocity: velocity)
return mostRecentOffset
}
var candidateAttributes: UICollectionViewLayoutAttributes?
for attributes in attributesForVisibleCells {
// == Skip comparison with non-cell items (headers and footers) == //
if attributes.representedElementCategory != .cell {
continue
}
if (attributes.center.x == 0) || (attributes.center.x > (cv.contentOffset.x + halfWidth) && velocity.x < 0) {
continue
}
candidateAttributes = attributes
}
// Beautification step , I don't know why it works!
if proposedContentOffset.x == -(cv.contentInset.left) {
mostRecentOffset = proposedContentOffset
return mostRecentOffset
}
guard let attrs = candidateAttributes else {
return mostRecentOffset
}
mostRecentOffset = CGPoint(x: floor(attrs.center.x - halfWidth), y: proposedContentOffset.y)
return mostRecentOffset
}
private func notifyPageChanged() {
guard let collectionView = collectionView else {
return
}
// take y as middle of collectionView, as mostRecentOffset might not be at any cell
let centerYOffset = CGPoint(x: mostRecentOffset.x,
y: collectionView.bounds.height / 2.0)
guard let indexPath = collectionView.indexPathForItem(at: centerYOffset) else {
// if offset.x <= 0, then it the first page because the first item has no previous item, hence indexPath returns nil
pagingItemDidChanged?(0)
return
}
// previous cell is always visible so the centered cell is the next indexPath
pagingItemDidChanged?(indexPath.item + 1)
}
}