Actually, the solution isn't very hard. Thanks to this article that shows the right way to implement this feature, I finally implemented it.
The main thing to understand is that the paging, built in a collection view, won't work in this case, because the size of cells is less than the size of the screen. So, we need to implement our custom paging technology.
First, inside of a custom helper struct
, we define a property that will hold the index of the cell before dragging :
var indexOfCellBeforeDragging: Int = 0
Then, we create a method for calculating the section inset of cells:
//Setting the inset
func calculateSectionInset(forCollectionViewLayout collectionViewLayout: UICollectionViewFlowLayout, numberOfCells: Int) -> CGFloat {
let inset = (collectionViewLayout.collectionView!.frame.width) / CGFloat(numberOfCells)
return inset
}
Now we can use the method above, to set the size of items of the collection view.
//Setting the item size of the collection view
func configureCollectionViewLayoutItemSize(forCollectionViewLayout collectionViewLayout: UICollectionViewFlowLayout) {
let inset: CGFloat = calculateSectionInset(forCollectionViewLayout: collectionViewLayout, numberOfCells: 5)
collectionViewLayout.sectionInset = UIEdgeInsets(top: 0, left: inset/4, bottom: 0, right: inset/4)
collectionViewLayout.itemSize = CGSize(width: collectionViewLayout.collectionView!.frame.size.width - inset / 2, height: collectionViewLayout.collectionView!.frame.size.height)
collectionViewLayout.collectionView?.reloadData()
}
We need to get the index of the major cell (the cell that is in the biggest right now). To do so, we use the information about items width and collection view's contentOffset
on the x
axis:
//Getting the index of the major cell
func indexOfMajorCell(in collectionViewLayout: UICollectionViewFlowLayout) -> Int {
let itemWidth = collectionViewLayout.itemSize.width
let proportionalLayout = collectionViewLayout.collectionView!.contentOffset.x / itemWidth
return Int(round(proportionalLayout))
}
Next, we set the index of cell before starting dragging, using the method above:
//Setting the index of cell before starting dragging
mutating func setIndexOfCellBeforeStartingDragging(indexOfMajorCell: Int) {
indexOfCellBeforeDragging = indexOfMajorCell
}
Now we should handle the end of the dragging. Also, we take care of the possible snapping gesture on cells. Here we use some custom threshold for the swipe velocity.
//Handling dragging end of a scroll view
func handleDraggingWillEndForScrollView(_ scrollView: UIScrollView, inside collectionViewLayout: UICollectionViewFlowLayout, withVelocity velocity: CGPoint, usingIndexOfMajorCell indexOfMajorCell: Int) {
//Calculating where scroll view should snap
let indexOfMajorCell = indexOfMajorCell
let swipeVelocityThreshold: CGFloat = 0.5
let majorCellIsTheCellBeforeDragging = indexOfMajorCell == indexOfCellBeforeDragging
let hasEnoughVelocityToSlideToTheNextCell = indexOfCellBeforeDragging + 1 < 5 && velocity.x > swipeVelocityThreshold
let hasEnoughVelocityToSlideToThePreviousCell = ((indexOfCellBeforeDragging - 1) >= 0) && (velocity.x < -swipeVelocityThreshold)
let didUseSwipeToSkipCell = majorCellIsTheCellBeforeDragging && (hasEnoughVelocityToSlideToTheNextCell || hasEnoughVelocityToSlideToThePreviousCell)
if didUseSwipeToSkipCell {
let snapToIndex = indexOfCellBeforeDragging + (hasEnoughVelocityToSlideToTheNextCell ? 1 : -1)
let toValue = collectionViewLayout.itemSize.width * CGFloat(snapToIndex)
// Damping equal 1 => no oscillations => decay animation
UIView.animate(withDuration: 0.3, delay: 0, usingSpringWithDamping: 1, initialSpringVelocity: velocity.x, options: .allowUserInteraction, animations: {
scrollView.contentOffset = CGPoint(x: toValue, y: 0)
scrollView.layoutIfNeeded()
}, completion: nil)
} else {
let indexPath = IndexPath(row: indexOfMajorCell, section: 0)
collectionViewLayout.collectionView!.scrollToItem(at: indexPath, at: .centeredHorizontally, animated: true)
}
}
Now, in your view controller inside scrollViewWillBeginDragging(_:)
method we need to get the index of the major cell and set it as an index before starting dragging (commentsPagingHelper
is an instance of the helper struct):
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
let indexOfMajorCell = commentsPagingHelper.indexOfMajorCell(in: collectionViewLayout)
commentsPagingHelper.setIndexOfCellBeforeStartingDragging(indexOfMajorCell: indexOfMajorCell)
}
Finally, we handle the change of an index of the major cell inside scrollViewWillEndDragging(_:, withVelocity:, targetContentOffset:)
method:
func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
//Stop scrollView sliding:
targetContentOffset.pointee = scrollView.contentOffset
let indexOfMajorCell = commentsPagingHelper.indexOfMajorCell(in: collectionViewLayout)
commentsPagingHelper.handleDraggingWillEndForScrollView(scrollView, inside: collectionViewLayout, withVelocity: velocity, usingIndexOfMajorCell: indexOfMajorCell)
}
That's it. Hope, this will help someone with similar issue.