Skip to content

Commit eb1868b

Browse files
committed
Split apply into 2 methods, for section & standard snapshot
1 parent e145eaf commit eb1868b

File tree

1 file changed

+95
-42
lines changed

1 file changed

+95
-42
lines changed

Sources/DiffableDataSource.swift

Lines changed: 95 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,60 @@ final class DiffableDataSource: UICollectionViewDiffableDataSource<AnyHashable,
121121
withVisibleItems visibleItemIdentifiers: Set<UniqueIdentifier>,
122122
animated: Bool,
123123
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?
124178
) {
125179
// Build initial destination snapshot, then make adjustments below.
126180
// This takes care of newly added items and newly added sections,
@@ -157,49 +211,48 @@ final class DiffableDataSource: UICollectionViewDiffableDataSource<AnyHashable,
157211
// Swift 6 complains about 'call to main actor-isolated instance method' here.
158212
// However, call this method from a background thread is valid according to the docs.
159213
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+
}
200219

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")
203256
}
204257
}
205258

0 commit comments

Comments
 (0)