@@ -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