-
Notifications
You must be signed in to change notification settings - Fork 97
/
UICollectionViewUpdater.swift
183 lines (150 loc) · 7.27 KB
/
UICollectionViewUpdater.swift
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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
import UIKit
/// An updater for managing diffing updates to render data to the `UICollectionView`.
open class UICollectionViewUpdater<Adapter: UICollectionViewAdapter>: Updater {
/// A Bool value indicating whether that enable diffing animation. Default is true.
open var isAnimationEnabled = true
/// A Bool value indicating whether that enable diffing animation while target is
/// scrolling. Default is false.
open var isAnimationEnabledWhileScrolling = false
/// A Bool value indicating whether that to always render visible components
/// after diffing updated. Default is true.
open var alwaysRenderVisibleComponents = true
/// A Bool value indicating whether that to reset content offset after
/// updated if not scrolling. Default is true.
open var keepsContentOffset = true
/// Max number of changes that can be animated for diffing updates. Default is 300.
open var animatableChangeCount = 300
/// A completion handler to be called after each updates.
open var completion: (() -> Void)?
/// Create a new updater.
public init() {}
/// Set the `delegate` and `dataSource` of given collection view, then reload data and invalidate layout.
///
/// - Parameters:
/// - target: A target to be prepared.
/// - adapter: An adapter to be set to `delegate` and `dataSource`.
open func prepare(target: UICollectionView, adapter: Adapter) {
target.delegate = adapter
target.dataSource = adapter
target.reloadData()
target.collectionViewLayout.invalidateLayout()
}
/// Perform updates to render given data to the target.
/// The completion is expected to be called after all updates
/// and the its animations.
///
/// - Parameters:
/// - target: A target instance to be updated to render given data.
/// - adapter: An adapter holding currently rendered data.
/// - data: A collection of sections to be rendered next.
open func performUpdates(target: UICollectionView, adapter: Adapter, data: [Section]) {
guard case .some = target.window else {
adapter.data = data
target.reloadData()
completion?()
return
}
let stagedChangeset = StagedDataChangeset(source: adapter.data, target: data)
guard !stagedChangeset.isEmpty else {
adapter.data = data
renderVisibleComponentsIfNeeded(in: target, adapter: adapter)
completion?()
return
}
let totalChangeCount = stagedChangeset.reduce(0) { total, changeset in
total + changeset.changeCount
}
guard animatableChangeCount >= totalChangeCount else {
adapter.data = data
target.reloadData()
completion?()
return
}
CATransaction.begin()
CATransaction.setCompletionBlock(completion)
performDifferentialUpdates(target: target, adapter: adapter, stagedChangeset: stagedChangeset)
CATransaction.commit()
}
/// Perform diffing updates to render given data to the target.
///
/// - Parameters:
/// - target: A target instance to be updated to render given data.
/// - adapter: An adapter holding currently rendered data.
/// - stagedChangeset: A staged set of changes of current data and next data.
open func performDifferentialUpdates(target: UICollectionView, adapter: Adapter, stagedChangeset: StagedDataChangeset) {
let contentOffsetBeforeUpdates = target.contentOffset
func performBatchUpdates() {
for changeset in stagedChangeset {
target.performBatchUpdates({
adapter.data = changeset.data
if !changeset.sectionDeleted.isEmpty {
target.deleteSections(IndexSet(changeset.sectionDeleted))
}
if !changeset.sectionInserted.isEmpty {
target.insertSections(IndexSet(changeset.sectionInserted))
}
if !changeset.sectionUpdated.isEmpty {
target.reloadSections(IndexSet(changeset.sectionUpdated))
}
for (sourceIndex, targetIndex) in changeset.sectionMoved {
target.moveSection(sourceIndex, toSection: targetIndex)
}
if !changeset.elementDeleted.isEmpty {
target.deleteItems(at: changeset.elementDeleted.map { IndexPath(item: $0.element, section: $0.section) })
}
if !changeset.elementInserted.isEmpty {
target.insertItems(at: changeset.elementInserted.map { IndexPath(item: $0.element, section: $0.section) })
}
if !changeset.elementUpdated.isEmpty {
target.reloadItems(at: changeset.elementUpdated.map { IndexPath(item: $0.element, section: $0.section) })
}
for (sourcePath, targetPath) in changeset.elementMoved {
target.moveItem(at: IndexPath(item: sourcePath.element, section: sourcePath.section), to: IndexPath(item: targetPath.element, section: targetPath.section))
}
})
}
}
if isAnimationEnabled && (isAnimationEnabledWhileScrolling || !target._isScrolling) {
performBatchUpdates()
}
else {
UIView.performWithoutAnimation(performBatchUpdates)
}
if keepsContentOffset {
target._setAdjustedContentOffsetIfNeeded(contentOffsetBeforeUpdates)
}
renderVisibleComponentsIfNeeded(in: target, adapter: adapter)
}
/// Renders components displayed in visible area again.
///
/// - Parameters:
/// - target: A target instance to render components.
/// - adapter: An adapter holding currently rendered data.
open func renderVisibleComponents(in target: UICollectionView, adapter: Adapter) {
UIView.performWithoutAnimation {
target.performBatchUpdates({
for kind in adapter.registeredSupplementaryViewKinds(for: target) {
for indexPath in target.indexPathsForVisibleSupplementaryElements(ofKind: kind) {
guard let node = adapter.supplementaryViewNode(forElementKind: kind, collectionView: target, at: indexPath) else {
continue
}
let view = target.supplementaryView(forElementKind: kind, at: indexPath) as? ComponentRenderable
view?.render(component: node.component)
}
}
for indexPath in target.indexPathsForVisibleItems {
let cellNode = adapter.cellNode(at: indexPath)
let cell = target.cellForItem(at: indexPath) as? ComponentRenderable
cell?.render(component: cellNode.component)
}
})
}
}
}
private extension UICollectionViewUpdater {
func renderVisibleComponentsIfNeeded(in target: UICollectionView, adapter: Adapter) {
if alwaysRenderVisibleComponents {
renderVisibleComponents(in: target, adapter: adapter)
}
}
}