Last active
July 10, 2020 02:39
-
-
Save ohlulu/0fdc2643b4bdcae5eafc5b0e596570b9 to your computer and use it in GitHub Desktop.
Infinite-LoopView
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 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