I would like to replicate the paging in the multi-row App Store collection view:
So far I\'ve designed it as close as possible to the way it looks, including sh
There is no reason to subclass UICollectionViewFlowLayout
just for this behavior.
UICollectionView
is a subclass of UIScrollView
, so its delegate protocol UICollectionViewDelegate
is a subtype of UIScrollViewDelegate
. This means you can implement any of UIScrollViewDelegate
’s methods in your collection view’s delegate.
In your collection view’s delegate, implement scrollViewWillEndDragging(_:withVelocity:targetContentOffset:)
to round the target content offset to the top left corner of the nearest column of cells.
Here's an example implementation:
override func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) {
let layout = collectionViewLayout as! UICollectionViewFlowLayout
let bounds = scrollView.bounds
let xTarget = targetContentOffset.pointee.x
// This is the max contentOffset.x to allow. With this as contentOffset.x, the right edge of the last column of cells is at the right edge of the collection view's frame.
let xMax = scrollView.contentSize.width - scrollView.bounds.width
if abs(velocity.x) <= snapToMostVisibleColumnVelocityThreshold {
let xCenter = scrollView.bounds.midX
let poses = layout.layoutAttributesForElements(in: bounds) ?? []
// Find the column whose center is closest to the collection view's visible rect's center.
let x = poses.min(by: { abs($0.center.x - xCenter) < abs($1.center.x - xCenter) })?.frame.origin.x ?? 0
targetContentOffset.pointee.x = x
} else if velocity.x > 0 {
let poses = layout.layoutAttributesForElements(in: CGRect(x: xTarget, y: 0, width: bounds.size.width, height: bounds.size.height)) ?? []
// Find the leftmost column beyond the current position.
let xCurrent = scrollView.contentOffset.x
let x = poses.filter({ $0.frame.origin.x > xCurrent}).min(by: { $0.center.x < $1.center.x })?.frame.origin.x ?? xMax
targetContentOffset.pointee.x = min(x, xMax)
} else {
let poses = layout.layoutAttributesForElements(in: CGRect(x: xTarget - bounds.size.width, y: 0, width: bounds.size.width, height: bounds.size.height)) ?? []
// Find the rightmost column.
let x = poses.max(by: { $0.center.x < $1.center.x })?.frame.origin.x ?? 0
targetContentOffset.pointee.x = max(x, 0)
}
}
// Velocity is measured in points per millisecond.
private var snapToMostVisibleColumnVelocityThreshold: CGFloat { return 0.3 }
Result:
You can find the full source code for my test project here: https://github.com/mayoff/multiRowSnapper