Skip to content

Instantly share code, notes, and snippets.

@ohlulu
Last active July 10, 2020 02:39
Show Gist options
  • Save ohlulu/0fdc2643b4bdcae5eafc5b0e596570b9 to your computer and use it in GitHub Desktop.
Save ohlulu/0fdc2643b4bdcae5eafc5b0e596570b9 to your computer and use it in GitHub Desktop.
Infinite-LoopView
import UIKit
final class InfiniteLoopView<T, Cell>: UIView,
UICollectionViewDelegate,
UICollectionViewDataSource,
UICollectionViewDelegateFlowLayout
where Cell: UICollectionViewCell {
private lazy var collectionView: UICollectionView = {
let collectionViewLayout = UICollectionViewFlowLayout()
collectionViewLayout.scrollDirection = .horizontal
collectionViewLayout.minimumLineSpacing = 0
let view = UICollectionView(frame: .zero, collectionViewLayout: collectionViewLayout)
view.backgroundColor = .clear
view.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
view.register(Cell.self,
forCellWithReuseIdentifier: String(describing: Cell.self))
view.delegate = self
view.dataSource = self
view.showsVerticalScrollIndicator = false
view.showsHorizontalScrollIndicator = false
view.isPagingEnabled = true
return view
}()
// MARK: data source
/// 輪播畫面
var inputDatas = [T]() { didSet { dataSourceDidChange() } }
/// 輪播秒數
var changeSec: Int? = nil { didSet { restartTimer() } }
/// 設置外部 page viewControl (numberOfPages, currentPage)
var pageIndexHandler: ((Int, Int) -> Void)?
/// 設置 cell 和 資料型別
var configure: (T, Cell) -> Void
/// 點擊 cell 觸發事件
var selectHandler: ((Int) -> Void)?
private var currentPage = Int()
private var collectionInputDatas = [T]()
private var timer: Timer?
private let itemSize: CGSize
// Life cycle
init(itemSize: CGSize, configure: @escaping (T, Cell) -> Void) {
self.itemSize = itemSize
self.configure = configure
super.init(frame: .zero)
setupUI()
}
required init?(coder aDecoder: NSCoder) { fatalError() }
deinit { cleanTimer() }
// MARK: Helper Method
private func dataSourceDidChange() {
guard
let firstItem = inputDatas.first,
let lastItem = inputDatas.last
else {
return
}
collectionInputDatas = [lastItem] + inputDatas + [firstItem]
UIView.animate(withDuration: 0, animations: {
self.collectionView.reloadData()
}) { _ in
self.collectionView.setContentOffset(CGPoint(x: UIScreen.main.bounds.width, y: 0), animated: false)
self.pageIndexHandler?(self.inputDatas.count, 0)
self.collectionView.isScrollEnabled = self.inputDatas.count != 1
}
}
private func startTimer() {
guard let changeSec = changeSec else { return }
timer = Timer.scheduledTimer(
timeInterval: TimeInterval(changeSec),
target: self,
selector: #selector(scrollToNext),
userInfo: nil,
repeats: true
)
}
private func cleanTimer() {
timer?.invalidate()
timer = nil
}
private func restartTimer() {
cleanTimer()
startTimer()
}
@objc private func scrollToNext() {
if !(collectionView.isScrollEnabled) {
return
}
let x = CGFloat(Int(UIScreen.main.bounds.width) * (currentPage + 1))
collectionView.setContentOffset(CGPoint(x: x, y: 0), animated: true)
}
@objc private func scroll(to index: Int) {
if index == 0 { // 滑到 index 0 時, 把 index 改成 last
let x = CGFloat(Int(UIScreen.main.bounds.width) * (collectionInputDatas.count - 2))
collectionView.setContentOffset(CGPoint(x: x, y: 0), animated: false)
} else if index == collectionInputDatas.count - 1 { // 滑到 last index 時, 把 index 改成 1
let x = UIScreen.main.bounds.width
collectionView.setContentOffset(CGPoint(x: x, y: 0), animated: false)
}
let setIndxe = Int(collectionView.contentOffset.x / UIScreen.main.bounds.width)
pageIndexHandler?(inputDatas.count, setIndxe - 1)
currentPage = setIndxe
}
// MARK: - UICollectionViewDelegate
func scrollViewDidScroll(_ scrollView: UIScrollView) {
if scrollView.frame.size.width <= 0 {
return
}
// 原始資料
let pageFloat = (scrollView.contentOffset.x / scrollView.frame.size.width)
// 無條件捨去
let pageInt = Int(pageFloat)
// 無條件進位
let pageCeil = Int(ceil(pageFloat))
if pageInt == 0 {
scroll(to: pageCeil)
} else {
scroll(to: pageInt)
}
}
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
cleanTimer()
}
func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
startTimer()
}
func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
var index = indexPath.row - 1
index = max(-1, index)
index = min(index, inputDatas.count - 1)
selectHandler?(index)
}
// MARK: - UICollectionViewDataSource
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return collectionInputDatas.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: String(describing: Cell.self), for: indexPath) as? Cell
else {
return UICollectionViewCell()
}
let item = collectionInputDatas[indexPath.row]
configure(item, cell)
return cell
}
// MARK: - UICollectionViewDelegateFlowLayout
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
return itemSize
}
func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
return UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
}
// MARK: - Setup UI
private func setupUI() {
backgroundColor = .clear
addSubview(collectionView)
NSLayoutConstraint.activate([
collectionView.leadingAnchor.constraint(equalTo: leadingAnchor),
collectionView.topAnchor.constraint(equalTo: topAnchor),
collectionView.trailingAnchor.constraint(equalTo: trailingAnchor),
collectionView.bottomAnchor.constraint(equalTo: bottomAnchor),
collectionView.heightAnchor.constraint(equalToConstant: itemSize.height),
collectionView.widthAnchor.constraint(equalToConstant: itemSize.width)
])
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment