Skip to content

Commit 88d10ba

Browse files
committed
Adopt Swift 6 + concurrency + remove background diffing
Closes #157. - Adopt Swift 6 - Support Concurrency - Remove background diffing Maintaining `CollectionViewDriverOptions.diffOnBackgroundQueue` is too large of an effort for very little benefit. It was always questionable, and against guidance from the UIKit team. See this blog post for more: https://www.jessesquires.com/blog/2024/12/19/diffable-data-source-main-actor-inconsistency
1 parent ec0a461 commit 88d10ba

13 files changed

+59
-121
lines changed

CHANGELOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ NEXT
77

88
- TBA
99

10+
0.2.0
11+
-----
12+
13+
This release closes the [0.2.0 milestone](https://github.com/jessesquires/ReactiveCollectionsKit/milestone/3?closed=1).
14+
15+
**Breaking Changes:**
16+
- Adopt Swift 6 and Swift Concurrency, remove `CollectionViewDriverOptions.diffOnBackgroundQueue`. ([@jessesquires](https://github.com/jessesquires), [#157](https://github.com/jessesquires/ReactiveCollectionsKit/issues/157), [#158](https://github.com/jessesquires/ReactiveCollectionsKit/pull/158)) **See linked issue and pull request for decision to remove `diffOnBackgroundQueue`.**
17+
1018
0.1.9
1119
-----
1220

Example/Sources/List/ListViewController.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ final class ListViewController: ExampleViewController, CellEventCoordinator {
2020
lazy var driver: CollectionViewDriver = {
2121
let driver = CollectionViewDriver(
2222
view: self.collectionView,
23-
options: .init(diffOnBackgroundQueue: true),
2423
emptyViewProvider: sharedEmptyViewProvider,
2524
cellEventCoordinator: self
2625
)

Package.swift

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,5 @@ let package = Package(
4141
]
4242
)
4343
],
44-
swiftLanguageModes: [.v5]
44+
swiftLanguageModes: [.v6]
4545
)
46-
47-
#warning("Remove after Swift 6 language mode")
48-
let swiftSettings = [
49-
SwiftSetting.enableExperimentalFeature("StrictConcurrency")
50-
]
51-
52-
for target in package.targets {
53-
var settings = target.swiftSettings ?? []
54-
settings.append(contentsOf: swiftSettings)
55-
target.swiftSettings = settings
56-
}

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ driver.update(viewModel: updated)
9191
## Requirements
9292

9393
- iOS 16.0+
94-
- Swift 5.10+
94+
- Swift 6.0+
9595
- Xcode 26.0+
9696
- [SwiftLint](https://github.com/realm/SwiftLint)
9797

ReactiveCollectionsKit.xcodeproj/project.pbxproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,7 @@
288288
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
289289
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
290290
SWIFT_STRICT_CONCURRENCY = complete;
291-
SWIFT_VERSION = 5.0;
291+
SWIFT_VERSION = 6.0;
292292
TARGETED_DEVICE_FAMILY = "1,2";
293293
VERSIONING_SYSTEM = "apple-generic";
294294
};
@@ -351,7 +351,7 @@
351351
SWIFT_COMPILATION_MODE = wholemodule;
352352
SWIFT_OPTIMIZATION_LEVEL = "-O";
353353
SWIFT_STRICT_CONCURRENCY = complete;
354-
SWIFT_VERSION = 5.0;
354+
SWIFT_VERSION = 6.0;
355355
TARGETED_DEVICE_FAMILY = "1,2";
356356
VALIDATE_PRODUCT = YES;
357357
VERSIONING_SYSTEM = "apple-generic";

Sources/CollectionViewDriver.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ public final class CollectionViewDriver: NSObject {
9292

9393
// workaround for swift initialization rules.
9494
// the "real" init is below.
95-
self._dataSource = DiffableDataSource(view: view, diffOnBackgroundQueue: false)
95+
self._dataSource = DiffableDataSource(view: view)
9696

9797
super.init()
9898

@@ -106,7 +106,6 @@ public final class CollectionViewDriver: NSObject {
106106
// `self` owns the `_dataSource`, so we know that `self` will always exist.
107107
self._dataSource = DiffableDataSource(
108108
view: view,
109-
diffOnBackgroundQueue: options.diffOnBackgroundQueue,
110109
cellProvider: { [unowned self] view, indexPath, itemIdentifier in
111110
self._cellProvider(
112111
collectionView: view,

Sources/CollectionViewDriverOptions.swift

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,6 @@ import Foundation
1515

1616
/// Defines various options to customize behavior of a ``CollectionViewDriver``.
1717
public struct CollectionViewDriverOptions: Hashable {
18-
/// Specifies whether or not to perform diffing on a background queue.
19-
/// Pass `true` to perform diffing in the background,
20-
/// pass `false` to perform diffing on the main thread.
21-
public let diffOnBackgroundQueue: Bool
22-
2318
/// Specifies whether or not the ``CollectionViewDriver`` should
2419
/// perform a hard `reloadData()` when replacing the ``CollectionViewModel`` with
2520
/// a new one, or if it should always perform a diff.
@@ -31,13 +26,10 @@ public struct CollectionViewDriverOptions: Hashable {
3126
/// Initializes a `CollectionViewDriverOptions` object.
3227
///
3328
/// - Parameters:
34-
/// - diffOnBackgroundQueue: Whether or not to perform diffing on a background queue. Default is `false`.
3529
/// - reloadDataOnReplacingViewModel: Whether or not to reload or diff during replacement. Default is `false`.
3630
public init(
37-
diffOnBackgroundQueue: Bool = false,
3831
reloadDataOnReplacingViewModel: Bool = false
3932
) {
40-
self.diffOnBackgroundQueue = diffOnBackgroundQueue
4133
self.reloadDataOnReplacingViewModel = reloadDataOnReplacingViewModel
4234
}
4335
}

Sources/DebugDescriptions.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,6 @@ private func debugDescriptionBuilder<Target: TextOutputStream>(
133133
debugDescriptionBuilder(
134134
elements: [
135135
(.type(CollectionViewDriverOptions.self), indent + 2),
136-
(.field(label: "diffOnBackgroundQueue", value: options.diffOnBackgroundQueue), indent + 4),
137136
(.field(label: "reloadDataOnReplacingViewModel", value: options.reloadDataOnReplacingViewModel), indent + 4),
138137
(.end, indent + 2)
139138
],
@@ -203,7 +202,6 @@ func driverOptionsDebugDescription(_ options: CollectionViewDriverOptions) -> St
203202
debugDescriptionBuilder(
204203
elements: [
205204
(.type(CollectionViewDriverOptions.self), 0),
206-
(.field(label: "diffOnBackgroundQueue", value: options.diffOnBackgroundQueue), 2),
207205
(.field(label: "reloadDataOnReplacingViewModel", value: options.reloadDataOnReplacingViewModel), 2),
208206
(.end, 0)
209207
],

Sources/DiffableDataSource.swift

Lines changed: 44 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -26,34 +26,23 @@ final class DiffableDataSource: UICollectionViewDiffableDataSource<AnyHashable,
2626
// Thus, unowned is safe here.
2727
private unowned let _collectionView: UICollectionView
2828

29-
private let _diffOnBackgroundQueue: Bool
30-
31-
private lazy var _diffingQueue = DispatchQueue(
32-
label: "com.jessesquires.ReactiveCollectionsKit",
33-
qos: .userInteractive,
34-
autoreleaseFrequency: .workItem
35-
)
36-
3729
var logger: Logging?
3830

3931
// MARK: Init
4032

4133
init(
4234
view: UICollectionView,
43-
diffOnBackgroundQueue: Bool,
4435
cellProvider: @escaping DiffableDataSource.CellProvider,
4536
supplementaryViewProvider: @escaping DiffableDataSource.SupplementaryViewProvider
4637
) {
4738
self._collectionView = view
48-
self._diffOnBackgroundQueue = diffOnBackgroundQueue
4939
super.init(collectionView: view, cellProvider: cellProvider)
5040
self.supplementaryViewProvider = supplementaryViewProvider
5141
}
5242

53-
convenience init(view: UICollectionView, diffOnBackgroundQueue: Bool) {
43+
convenience init(view: UICollectionView) {
5444
self.init(
5545
view: view,
56-
diffOnBackgroundQueue: diffOnBackgroundQueue,
5746
cellProvider: { _, _, _ in nil },
5847
supplementaryViewProvider: { _, _, _ in nil }
5948
)
@@ -66,12 +55,8 @@ final class DiffableDataSource: UICollectionViewDiffableDataSource<AnyHashable,
6655

6756
let snapshot = DiffableSnapshot(viewModel: viewModel)
6857
self.applySnapshotUsingReloadData(snapshot) {
69-
// UIKit guarantees `completion` is called on the main queue.
70-
dispatchPrecondition(condition: .onQueue(.main))
71-
MainActor.assumeIsolated {
72-
completion?()
73-
self.logger?.log("DataSource reload snapshot completed")
74-
}
58+
completion?()
59+
self.logger?.log("DataSource reload snapshot completed")
7560
}
7661
}
7762

@@ -89,33 +74,18 @@ final class DiffableDataSource: UICollectionViewDiffableDataSource<AnyHashable,
8974
// We need to inspect the current collection view state first, then pass this info downstream.
9075
let visibleItemIdentifiers = self._visibleItemIdentifiers()
9176

92-
if self._diffOnBackgroundQueue {
93-
self.logger?.log("DataSource using background queue")
94-
self._diffingQueue.async {
95-
self._applySnapshot(
96-
from: source,
97-
to: destination,
98-
withVisibleItems: visibleItemIdentifiers,
99-
animated: animated,
100-
completion: completion
101-
)
102-
}
103-
} else {
104-
self.logger?.log("DataSource using main queue")
105-
dispatchPrecondition(condition: .onQueue(.main))
106-
self._applySnapshot(
107-
from: source,
108-
to: destination,
109-
withVisibleItems: visibleItemIdentifiers,
110-
animated: animated,
111-
completion: completion
112-
)
113-
}
77+
self._applySnapshot(
78+
from: source,
79+
to: destination,
80+
withVisibleItems: visibleItemIdentifiers,
81+
animated: animated,
82+
completion: completion
83+
)
11484
}
11585

11686
// MARK: Private
11787

118-
nonisolated private func _applySnapshot(
88+
private func _applySnapshot(
11989
from source: CollectionViewModel,
12090
to destination: CollectionViewModel,
12191
withVisibleItems visibleItemIdentifiers: Set<UniqueIdentifier>,
@@ -153,59 +123,49 @@ final class DiffableDataSource: UICollectionViewDiffableDataSource<AnyHashable,
153123
destinationSnapshot.reconfigureItems(itemsToReconfigure)
154124

155125
// Apply the snapshot with item reconfigure updates.
156-
//
157-
// Swift 6 complains about 'call to main actor-isolated instance method' here.
158-
// However, call this method from a background thread is valid according to the docs.
159126
self.apply(destinationSnapshot, animatingDifferences: animated) { [weak self] in
160-
// UIKit guarantees `completion` is called on the main queue.
161-
dispatchPrecondition(condition: .onQueue(.main))
162-
163127
guard let self else {
164-
MainActor.assumeIsolated {
165-
completion?()
166-
}
128+
completion?()
167129
return
168130
}
169131

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?()
200-
201-
self.logger?.log("DataSource diffing snapshot complete")
202-
}
132+
// Once the snapshot with item reconfigures is applied,
133+
// we need to find and apply supplementary view reconfigures, if needed.
134+
//
135+
// This is necessary to update all headers, footers, and supplementary views.
136+
// Per notes above, supplementary views do not get reloaded / reconfigured
137+
// automatically by `DiffableDataSource` when they change.
138+
//
139+
// To trigger updates on supplementary views with the existing APIs,
140+
// the entire section must be reloaded. Yes, that sucks. We don't want to do that.
141+
// That causes all items in the section to be hard-reloaded, too.
142+
// Aside from the performance impact, doing that results in an ugly UI "flash"
143+
// for all item cells in the collection. Gross.
144+
//
145+
// However, we can actually do much better than a hard reload!
146+
// Instead of reloading the entire section, we can find and compare
147+
// the supplementary views and manually reconfigure them if they changed.
148+
//
149+
// NOTE: this only matters if supplementary views are not static.
150+
// That is, if they reflect data in the data source.
151+
//
152+
// For example, a header with a fixed title (e.g. "My Items") will NOT need to be reloaded.
153+
// However, a header that displays changing data WILL need to be reloaded.
154+
// (e.g. "My 10 Items")
155+
156+
// Check all the supplementary views and reconfigure them, if needed.
157+
self._reconfigureSupplementaryViewsIfNeeded(from: source, to: destination)
158+
159+
// Finally, we're done and can call completion.
160+
completion?()
161+
162+
self.logger?.log("DataSource diffing snapshot complete")
203163
}
204164
}
205165

206166
// MARK: Reconfiguring Cells
207167

208-
nonisolated private func _findItemsToReconfigure(
168+
private func _findItemsToReconfigure(
209169
from source: CollectionViewModel,
210170
to destination: CollectionViewModel,
211171
withVisibleItems visibleItemIdentifiers: Set<UniqueIdentifier>

Tests/TestCollectionViewDriver.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,7 @@ final class TestCollectionViewDriver: UnitTestCase, @unchecked Sendable {
434434
func test_update_callsCompletion_withBackgroundDiffing() {
435435
let driver = CollectionViewDriver(
436436
view: self.collectionView,
437-
options: .init(diffOnBackgroundQueue: true)
437+
options: .init()
438438
)
439439

440440
let expectation = self.expectation()

0 commit comments

Comments
 (0)