Reputation: 237
I have seen a lot of posts where this is solved horizontally, however, I am having trouble implementing a solution for a vertical collection view.
My collection view's cells fill the entire width but not the entire height of the collection view, so normal paging does not work. I am trying to snap the cells center to the screens center when scrolling using a custom UICollectionViewFlowLayout
.
(Similar to an Instagram feed but no "free" scrolling and the posts get centered vertically)
class FeedLayout: UICollectionViewFlowLayout {
private var previousOffset: CGFloat = 0
private var currentPage: Int = 0
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
guard let collectionView = collectionView else {
return super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)
}
let itemsCount = collectionView.numberOfItems(inSection: 0)
if previousOffset > collectionView.contentOffset.y {
currentPage = max(currentPage - 1, 0)
} else if previousOffset < collectionView.contentOffset.y {
currentPage = min(currentPage + 1, itemsCount - 1)
}
let updatedOffset = ((collectionView.frame.height * 0.75) + minimumLineSpacing) * CGFloat(currentPage)
previousOffset = updatedOffset
return CGPoint(x: proposedContentOffset.x, y: updatedOffset)
}
}
Upvotes: 1
Views: 546
Reputation: 1414
I wrote an open-source extension that does this a few days ago.
I would adjust it to center the cell (instead of scroll cell-by-cell with new cells on the top) with this changes:
public extension UICollectionViewFlowLayout {
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
guard let collectionView = self.collectionView else {
let latestOffset = super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)
return latestOffset
}
// page height used for estimating and calculating paging
let pageHeight = self.itemSize.height + self.minimumLineSpacing
// determine total pages
// collectionView adds an extra self.minimumLineSpacing to the total contentSize.height so this must be removed to get an even division of pages
let totalPages = (collectionView.contentSize.height - self.minimumLineSpacing) / pageHeight
// determine current page index
let visibleRect = CGRect(origin: collectionView.contentOffset, size: collectionView.bounds.size)
let visiblePoint = self.itemSize.height * 2 > collectionView.visibleSize.height ? CGPoint(x: visibleRect.midX, y: visibleRect.midY) : CGPoint(x: visibleRect.midX, y: visibleRect.midY - (self.itemSize.height / 3))
let visibleIndexPath = collectionView.indexPathForItem(at: visiblePoint)?.row ?? 0
let currentIndex = CGFloat(visibleIndexPath)
// make an estimation of the current page position
let approximatePage = collectionView.contentOffset.y / pageHeight
// determine the current page based on velocity
let currentPage = velocity.y == 0 ? round(approximatePage) : (velocity.y < 0.0 ? floor(approximatePage) : ceil(approximatePage))
// create custom flickVelocity
let flickVelocity = velocity.y * 0.5
// check how many pages the user flicked, if <= 1 then flickedPages should return 0
let flickedPages = (abs(round(flickVelocity)) <= 1) ? 0 : round(flickVelocity)
// determine the new vertical offset
// scroll to top of next/previos cell
// let newVerticalOffset = ((currentPage + flickedPages) * pageHeight) - collectionView.contentInset.top
// scroll to center of next/previous cell
let newVerticalOffset = ((currentPage + flickedPages) * pageHeight) + ((collectionView.visibleSize.height - pageHeight) / 2) - collectionView.contentInset.top
// determine up or down swipe
let swipeDirection: CGFloat = flickVelocity > 0 ? 1 : -1
// determine if we are at the end of beginning of list
let beyond = newVerticalOffset + pageHeight >= collectionView.contentSize.height || collectionView.contentOffset.y < 0 ? true : false
// determine if the flick was too small to switch pages
let stay = abs(newVerticalOffset - collectionView.contentOffset.y) < (self.itemSize.height * 0.4) ? true : false
// determine if there are multiple pages available to swipe based on current page
var multipleAvailable = false
if flickVelocity > 0 {
multipleAvailable = currentIndex + swipeDirection < totalPages - 1 ? true : false
} else {
multipleAvailable = currentIndex + swipeDirection > 0 ? true : false
}
// give haptic feedback based on how many cells are scrolled
if beyond == false && stay == false {
if abs(flickedPages) > 1 && multipleAvailable {
TapticGenerator.notification(.success)
} else {
TapticGenerator.impact(.medium)
}
}
return CGPoint(x: proposedContentOffset.x, y: newVerticalOffset - collectionView.safeAreaInsets.top)
}
}
Upvotes: 1