-
Notifications
You must be signed in to change notification settings - Fork 40
Expand file tree
/
Copy pathInfiniteCollectionView.swift
More file actions
145 lines (138 loc) · 7.27 KB
/
InfiniteCollectionView.swift
File metadata and controls
145 lines (138 loc) · 7.27 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
//
// InfiniteCollectionView.swift
// Pods
//
// Created by hryk224 on 2015/10/17.
//
//
import UIKit
@objc public protocol InfiniteCollectionViewDataSource: class {
@objc @available(*, deprecated, renamed: "number(ofItems:)")
optional func numberOfItems(collectionView: UICollectionView) -> Int
@objc @available(*, deprecated, renamed: "collectionView(_:dequeueForItemAt:cellForItemAt:)")
optional func cellForItemAtIndexPath(collectionView: UICollectionView, dequeueIndexPath: NSIndexPath, indexPath: NSIndexPath) -> UICollectionViewCell
func number(ofItems collectionView: UICollectionView) -> Int
func collectionView(_ collectionView: UICollectionView, dequeueForItemAt dequeueIndexPath: IndexPath, cellForItemAt usableIndexPath: IndexPath) -> UICollectionViewCell
}
@objc public protocol InfiniteCollectionViewDelegate: class {
@objc @available(*, deprecated, renamed: "infiniteCollectionView(_:didSelectItemAt:)")
optional func didSelectCellAtIndexPath(collectionView: UICollectionView, indexPath: NSIndexPath)
@objc @available(*, deprecated, renamed: "scrollView(_:pageIndex:)")
optional func didUpdatePageIndex(index: Int)
@objc optional func infiniteCollectionView(_ collectionView: UICollectionView, didSelectItemAt usableIndexPath: IndexPath)
@objc optional func scrollView(_ scrollView: UIScrollView, pageIndex: Int)
}
open class InfiniteCollectionView: UICollectionView {
open weak var infiniteDataSource: InfiniteCollectionViewDataSource?
open weak var infiniteDelegate: InfiniteCollectionViewDelegate?
@available(*, deprecated, message: "It becomes unnecessary because it uses UICollectionViewFlowLayout.")
open var cellWidth: CGFloat?
fileprivate let dummyCount: Int = 3
fileprivate let defaultIdentifier = "Cell"
fileprivate var indexOffset: Int = 0
fileprivate var pageIndex = 0
required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
configure()
}
override public init(frame: CGRect, collectionViewLayout layout: UICollectionViewLayout) {
super.init(frame: frame, collectionViewLayout: layout)
configure()
}
deinit {
NotificationCenter.default.removeObserver(self, name: UIDevice.orientationDidChangeNotification, object: nil)
}
@objc open func rotate(_ notification: Notification) {
setContentOffset(CGPoint(x: CGFloat(pageIndex + indexOffset) * itemWidth, y: contentOffset.y), animated: false)
}
open override func selectItem(at indexPath: IndexPath?, animated: Bool, scrollPosition: UICollectionView.ScrollPosition) {
guard let indexPath = indexPath else { return }
// Correct the input IndexPath
let correctedIndexPath = IndexPath(row: correctedIndex(indexPath.item + indexOffset), section: 0)
// Get the currently visible cell(s) - assumes a cell is visible
guard let visibleCell = self.visibleCells.first else {
return
}
// Index path of the cell - does not consider multiple cells on the screen at the same time
guard let visibleIndexPath = self.indexPath(for: visibleCell) else {
return
}
let testIndexPath = IndexPath(row: correctedIndex(visibleIndexPath.item), section: 0)
guard correctedIndexPath != testIndexPath else {
return // Do not re-select the same cell
}
// Call supercase to select the correct IndexPath
super.selectItem(at: correctedIndexPath, animated: animated, scrollPosition: scrollPosition)
}
}
// MARK: - private
private extension InfiniteCollectionView {
var itemWidth: CGFloat {
guard let layout = collectionViewLayout as? UICollectionViewFlowLayout else { return 0 }
return layout.itemSize.width + layout.minimumInteritemSpacing
}
var totalContentWidth: CGFloat {
let numberOfCells: CGFloat = CGFloat(infiniteDataSource?.number(ofItems: self) ?? 0)
return numberOfCells * itemWidth
}
func configure() {
delegate = self
dataSource = self
register(UICollectionViewCell.self, forCellWithReuseIdentifier: defaultIdentifier)
NotificationCenter.default.addObserver(self, selector: #selector(InfiniteCollectionView.rotate(_:)), name: UIDevice.orientationDidChangeNotification, object: nil)
}
func centerIfNeeded(_ scrollView: UIScrollView) {
let currentOffset = contentOffset
let centerX = (scrollView.contentSize.width - bounds.width) / 2
let distFromCenter = centerX - currentOffset.x
if abs(distFromCenter) > (totalContentWidth / 4) {
let cellcount = distFromCenter / itemWidth
let shiftCells = Int((cellcount > 0) ? floor(cellcount) : ceil(cellcount))
let offsetCorrection = (abs(cellcount).truncatingRemainder(dividingBy: 1)) * itemWidth
if centerX > contentOffset.x {
contentOffset = CGPoint(x: centerX - offsetCorrection, y: currentOffset.y)
} else {
contentOffset = CGPoint(x: centerX + offsetCorrection, y: currentOffset.y)
}
let offset = correctedIndex(shiftCells)
indexOffset += offset
reloadData()
}
let centerPoint = CGPoint(x: scrollView.frame.size.width / 2 + scrollView.contentOffset.x, y: scrollView.frame.size.height / 2 + scrollView.contentOffset.y)
guard let indexPath = indexPathForItem(at: centerPoint) else { return }
pageIndex = correctedIndex(indexPath.item - indexOffset)
infiniteDelegate?.scrollView?(scrollView, pageIndex: pageIndex)
}
func correctedIndex(_ indexToCorrect: Int) -> Int {
guard let numberOfItems = infiniteDataSource?.number(ofItems: self) else { return 0 }
if numberOfItems > indexToCorrect && indexToCorrect >= 0 {
return indexToCorrect
}
let countInIndex = Float(indexToCorrect) / Float(numberOfItems)
let flooredValue = Int(floor(countInIndex))
let offset = numberOfItems * flooredValue
return indexToCorrect - offset
}
}
// MARK: - UICollectionViewDataSource
extension InfiniteCollectionView: UICollectionViewDataSource {
public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
let numberOfItems = infiniteDataSource?.number(ofItems: collectionView) ?? 0
return dummyCount * numberOfItems
}
public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
guard let cell = infiniteDataSource?.collectionView(collectionView, dequeueForItemAt: indexPath, cellForItemAt: IndexPath(item: correctedIndex(indexPath.item - indexOffset), section: 0)) else {
return collectionView.dequeueReusableCell(withReuseIdentifier: defaultIdentifier, for: indexPath)
}
return cell
}
}
// MARK: - UICollectionViewDelegate
extension InfiniteCollectionView: UICollectionViewDelegate {
public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
infiniteDelegate?.infiniteCollectionView?(collectionView, didSelectItemAt: IndexPath(item: correctedIndex(indexPath.item - indexOffset), section: 0))
}
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
centerIfNeeded(scrollView)
}
}