Skip to content

Instantly share code, notes, and snippets.

@rlaguilar
Created October 18, 2018 19:38
Show Gist options
  • Save rlaguilar/d44d507f368e97f470a179807a69e6ab to your computer and use it in GitHub Desktop.
Save rlaguilar/d44d507f368e97f470a179807a69e6ab to your computer and use it in GitHub Desktop.
import UIKit
final class PagingCollectionView: UICollectionView, UICollectionViewDelegate {
private weak var externalDelegate: UICollectionViewDelegate?
override var delegate: UICollectionViewDelegate? {
get { return externalDelegate }
set {
externalDelegate = newValue
let currentDelegate = super.delegate
super.delegate = nil
super.delegate = currentDelegate
}
}
var currentIndexPath: IndexPath? {
let currentCenter = CGPoint(x: contentOffset.x + bounds.width / 2, y: bounds.height / 2)
return indexPathForItem(at: currentCenter)
}
init(frame: CGRect) {
let layout = PagingCollectionViewLayout()
layout.minimumLineSpacing = 0
layout.scrollDirection = .horizontal
super.init(frame: frame, collectionViewLayout: layout)
decelerationRate = .fast
showsHorizontalScrollIndicator = false
backgroundColor = .clear
super.delegate = self
}
@available(*, unavailable)
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func responds(to aSelector: Selector!) -> Bool {
return super.responds(to: aSelector) || (externalDelegate?.responds(to: aSelector) ?? false)
}
override func forwardingTarget(for aSelector: Selector!) -> Any? {
return externalDelegate
}
func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
notifyPageChangeIfNeeded()
externalDelegate?.scrollViewDidEndDecelerating?(scrollView)
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
if !decelerate {
notifyPageChangeIfNeeded()
}
externalDelegate?.scrollViewDidEndDragging?(scrollView, willDecelerate: decelerate)
}
private func notifyPageChangeIfNeeded() {
if let pagingDelegate = externalDelegate as? PagingCollectionViewDelegate, let index = currentIndexPath {
pagingDelegate.pagingCollectionView(self, didMoveToPageAt: index)
}
}
}
protocol PagingCollectionViewDelegate: UICollectionViewDelegate {
func pagingCollectionView(_ collectionView: PagingCollectionView, didMoveToPageAt indexPath: IndexPath)
}
class PagingCollectionViewLayout: UICollectionViewFlowLayout {
private var itemsFrame: [CGRect] = []
override func prepare() {
super.prepare()
guard let cv = collectionView else { return }
let itemsCount = cv.dataSource?.collectionView(cv, numberOfItemsInSection: 0) ?? 0
let allIndexPaths = (0 ..< itemsCount).map { IndexPath(item: $0, section: 0) }
itemsFrame = allIndexPaths.compactMap { self.layoutAttributesForItem(at: $0)?.frame }
}
override func targetContentOffset(forProposedContentOffset proposedContentOffset: CGPoint, withScrollingVelocity velocity: CGPoint) -> CGPoint {
guard let collectionView = collectionView else {
return .zero
}
if let targetRect = self.targetRect(forVelocity: velocity, in: collectionView) {
let leadingMargin = (collectionView.bounds.width - targetRect.width) / 2
return CGPoint(x: targetRect.minX - leadingMargin, y: proposedContentOffset.y)
} else {
return super.targetContentOffset(forProposedContentOffset: proposedContentOffset, withScrollingVelocity: velocity)
}
}
private func targetRect(forVelocity velocity: CGPoint, in collectionView: UICollectionView) -> CGRect? {
let centerOffset = collectionView.contentOffset.x + collectionView.bounds.width / 2
if velocity.x < 0 {
return itemsFrame.last(where: { $0.midX <= centerOffset }) ?? itemsFrame.first
} else if 0 < velocity.x {
return itemsFrame.first(where: { centerOffset <= $0.midX }) ?? itemsFrame.last
} else {
let isNearToCenter = { (rect1: CGRect, rect2: CGRect) -> Bool in
abs(rect1.midX - centerOffset) < abs(rect2.midX - centerOffset)
}
return itemsFrame.min(by: isNearToCenter)
}
}
}
class PagingCollectionViewLayoutDelegate: NSObject, UICollectionViewDelegateFlowLayout {
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
fatalError("This function needs to be implemented in subclasses")
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
let numberOfItems = collectionView.dataSource?.collectionView(collectionView, numberOfItemsInSection: section) ?? 0
guard numberOfItems > 0 else { return .zero }
let firstItemSize = self.collectionView(collectionView, layout: collectionViewLayout, sizeForItemAt: IndexPath(item: 0, section: section))
let leftInset = (collectionView.bounds.width - firstItemSize.width) / 2
let lastItemSize = self.collectionView(collectionView, layout: collectionViewLayout, sizeForItemAt: IndexPath(item: numberOfItems - 1, section: section))
let rightInset = (collectionView.bounds.width - lastItemSize.width) / 2
return UIEdgeInsets(top: 0, left: leftInset, bottom: 0, right: rightInset)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment