Created
October 18, 2018 19:38
-
-
Save rlaguilar/d44d507f368e97f470a179807a69e6ab to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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