@@ -121,6 +121,60 @@ final class DiffableDataSource: UICollectionViewDiffableDataSource<AnyHashable,
121
121
withVisibleItems visibleItemIdentifiers: Set < UniqueIdentifier > ,
122
122
animated: Bool ,
123
123
completion: SnapshotCompletion ?
124
+ ) {
125
+ // Before applying a snapshot, first check if there's at least one cell that has children.
126
+ // If so, nesting is desired and we must use section snapshots, since UIKit only supports
127
+ // this behavior through the use of NSDiffableDataSourceSectionSnapshots.
128
+ // Otherwise, use a standard snapshot.
129
+ let childrenExist = destination. sections. first? . contains ( where: \. children. isNotEmpty) ?? false
130
+
131
+ if childrenExist {
132
+ _applySectionSnapshot ( from: source,
133
+ to: destination,
134
+ withVisibleItems: visibleItemIdentifiers,
135
+ animated: animated,
136
+ completion: completion)
137
+ }
138
+ else {
139
+ _applyStandardSnapshot ( from: source,
140
+ to: destination,
141
+ withVisibleItems: visibleItemIdentifiers,
142
+ animated: animated,
143
+ completion: completion)
144
+ }
145
+ }
146
+
147
+ nonisolated private func _applySectionSnapshot(
148
+ from source: CollectionViewModel ,
149
+ to destination: CollectionViewModel ,
150
+ withVisibleItems visibleItemIdentifiers: Set < UniqueIdentifier > ,
151
+ animated: Bool ,
152
+ completion: SnapshotCompletion ?
153
+ ) {
154
+ // For each section, build the destination section snapshot.
155
+ destination. sections. forEach {
156
+ let destinationSectionSnapshot = DiffableSectionSnapshot ( viewModel: $0)
157
+
158
+ // Apply the section snapshot.
159
+ //
160
+ // Swift 6 complains about 'call to main actor-isolated instance method' here.
161
+ // However, call this method from a background thread is valid according to the docs.
162
+ self . apply ( destinationSectionSnapshot, to: $0. id, animatingDifferences: animated) { [ weak self] in
163
+ self ? . _applySnapshotCompletion ( source: source,
164
+ destination: destination,
165
+ completion)
166
+ }
167
+ }
168
+
169
+
170
+ }
171
+
172
+ nonisolated private func _applyStandardSnapshot(
173
+ from source: CollectionViewModel ,
174
+ to destination: CollectionViewModel ,
175
+ withVisibleItems visibleItemIdentifiers: Set < UniqueIdentifier > ,
176
+ animated: Bool ,
177
+ completion: SnapshotCompletion ?
124
178
) {
125
179
// Build initial destination snapshot, then make adjustments below.
126
180
// This takes care of newly added items and newly added sections,
@@ -157,49 +211,48 @@ final class DiffableDataSource: UICollectionViewDiffableDataSource<AnyHashable,
157
211
// Swift 6 complains about 'call to main actor-isolated instance method' here.
158
212
// However, call this method from a background thread is valid according to the docs.
159
213
self . apply ( destinationSnapshot, animatingDifferences: animated) { [ weak self] in
160
- // UIKit guarantees `completion` is called on the main queue.
161
- dispatchPrecondition ( condition: . onQueue( . main) )
162
-
163
- guard let self else {
164
- MainActor . assumeIsolated {
165
- completion ? ( )
166
- }
167
- return
168
- }
169
-
170
- MainActor . assumeIsolated {
171
- // Once the snapshot with item reconfigures is applied,
172
- // we need to find and apply supplementary view reconfigures, if needed.
173
- //
174
- // This is necessary to update all headers, footers, and supplementary views.
175
- // Per notes above, supplementary views do not get reloaded / reconfigured
176
- // automatically by `DiffableDataSource` when they change.
177
- //
178
- // To trigger updates on supplementary views with the existing APIs,
179
- // the entire section must be reloaded. Yes, that sucks. We don't want to do that.
180
- // That causes all items in the section to be hard-reloaded, too.
181
- // Aside from the performance impact, doing that results in an ugly UI "flash"
182
- // for all item cells in the collection. Gross.
183
- //
184
- // However, we can actually do much better than a hard reload!
185
- // Instead of reloading the entire section, we can find and compare
186
- // the supplementary views and manually reconfigure them if they changed.
187
- //
188
- // NOTE: this only matters if supplementary views are not static.
189
- // That is, if they reflect data in the data source.
190
- //
191
- // For example, a header with a fixed title (e.g. "My Items") will NOT need to be reloaded.
192
- // However, a header that displays changing data WILL need to be reloaded.
193
- // (e.g. "My 10 Items")
194
-
195
- // Check all the supplementary views and reconfigure them, if needed.
196
- self . _reconfigureSupplementaryViewsIfNeeded ( from: source, to: destination)
197
-
198
- // Finally, we're done and can call completion.
199
- completion ? ( )
214
+ self ? . _applySnapshotCompletion ( source: source,
215
+ destination: destination,
216
+ completion)
217
+ }
218
+ }
200
219
201
- self . logger? . log ( " DataSource diffing snapshot complete " )
202
- }
220
+ nonisolated private func _applySnapshotCompletion( source: CollectionViewModel , destination: CollectionViewModel , _ completion: SnapshotCompletion ? ) {
221
+ // UIKit guarantees `completion` is called on the main queue.
222
+ dispatchPrecondition ( condition: . onQueue( . main) )
223
+
224
+ MainActor . assumeIsolated {
225
+ // Once the snapshot with item reconfigures is applied,
226
+ // we need to find and apply supplementary view reconfigures, if needed.
227
+ //
228
+ // This is necessary to update all headers, footers, and supplementary views.
229
+ // Per notes above, supplementary views do not get reloaded / reconfigured
230
+ // automatically by `DiffableDataSource` when they change.
231
+ //
232
+ // To trigger updates on supplementary views with the existing APIs,
233
+ // the entire section must be reloaded. Yes, that sucks. We don't want to do that.
234
+ // That causes all items in the section to be hard-reloaded, too.
235
+ // Aside from the performance impact, doing that results in an ugly UI "flash"
236
+ // for all item cells in the collection. Gross.
237
+ //
238
+ // However, we can actually do much better than a hard reload!
239
+ // Instead of reloading the entire section, we can find and compare
240
+ // the supplementary views and manually reconfigure them if they changed.
241
+ //
242
+ // NOTE: this only matters if supplementary views are not static.
243
+ // That is, if they reflect data in the data source.
244
+ //
245
+ // For example, a header with a fixed title (e.g. "My Items") will NOT need to be reloaded.
246
+ // However, a header that displays changing data WILL need to be reloaded.
247
+ // (e.g. "My 10 Items")
248
+
249
+ // Check all the supplementary views and reconfigure them, if needed.
250
+ self . _reconfigureSupplementaryViewsIfNeeded ( from: source, to: destination)
251
+
252
+ // Finally, we're done and can call completion.
253
+ completion ? ( )
254
+
255
+ self . logger? . log ( " DataSource diffing snapshot complete " )
203
256
}
204
257
}
205
258
0 commit comments