Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ NEXT

- TBA

0.2.0
-----

This release closes the [0.2.0 milestone](https://github.com/jessesquires/ReactiveCollectionsKit/milestone/3?closed=1).

**Breaking Changes:**
- 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`.**

0.1.9
-----

Expand Down
1 change: 0 additions & 1 deletion Example/Sources/List/ListViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ final class ListViewController: ExampleViewController, CellEventCoordinator {
lazy var driver: CollectionViewDriver = {
let driver = CollectionViewDriver(
view: self.collectionView,
options: .init(diffOnBackgroundQueue: true),
emptyViewProvider: sharedEmptyViewProvider,
cellEventCoordinator: self
)
Expand Down
13 changes: 1 addition & 12 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,5 @@ let package = Package(
]
)
],
swiftLanguageModes: [.v5]
swiftLanguageModes: [.v6]
)

#warning("Remove after Swift 6 language mode")
let swiftSettings = [
SwiftSetting.enableExperimentalFeature("StrictConcurrency")
]

for target in package.targets {
var settings = target.swiftSettings ?? []
settings.append(contentsOf: swiftSettings)
target.swiftSettings = settings
}
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ driver.update(viewModel: updated)
## Requirements

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

Expand Down
4 changes: 2 additions & 2 deletions ReactiveCollectionsKit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_STRICT_CONCURRENCY = complete;
SWIFT_VERSION = 5.0;
SWIFT_VERSION = 6.0;
TARGETED_DEVICE_FAMILY = "1,2";
VERSIONING_SYSTEM = "apple-generic";
};
Expand Down Expand Up @@ -351,7 +351,7 @@
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_STRICT_CONCURRENCY = complete;
SWIFT_VERSION = 5.0;
SWIFT_VERSION = 6.0;
TARGETED_DEVICE_FAMILY = "1,2";
VALIDATE_PRODUCT = YES;
VERSIONING_SYSTEM = "apple-generic";
Expand Down
3 changes: 1 addition & 2 deletions Sources/CollectionViewDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ public final class CollectionViewDriver: NSObject {

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

super.init()

Expand All @@ -106,7 +106,6 @@ public final class CollectionViewDriver: NSObject {
// `self` owns the `_dataSource`, so we know that `self` will always exist.
self._dataSource = DiffableDataSource(
view: view,
diffOnBackgroundQueue: options.diffOnBackgroundQueue,
cellProvider: { [unowned self] view, indexPath, itemIdentifier in
self._cellProvider(
collectionView: view,
Expand Down
8 changes: 0 additions & 8 deletions Sources/CollectionViewDriverOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,6 @@ import Foundation

/// Defines various options to customize behavior of a ``CollectionViewDriver``.
public struct CollectionViewDriverOptions: Hashable {
/// Specifies whether or not to perform diffing on a background queue.
/// Pass `true` to perform diffing in the background,
/// pass `false` to perform diffing on the main thread.
public let diffOnBackgroundQueue: Bool

/// Specifies whether or not the ``CollectionViewDriver`` should
/// perform a hard `reloadData()` when replacing the ``CollectionViewModel`` with
/// a new one, or if it should always perform a diff.
Expand All @@ -31,13 +26,10 @@ public struct CollectionViewDriverOptions: Hashable {
/// Initializes a `CollectionViewDriverOptions` object.
///
/// - Parameters:
/// - diffOnBackgroundQueue: Whether or not to perform diffing on a background queue. Default is `false`.
/// - reloadDataOnReplacingViewModel: Whether or not to reload or diff during replacement. Default is `false`.
public init(
diffOnBackgroundQueue: Bool = false,
reloadDataOnReplacingViewModel: Bool = false
) {
self.diffOnBackgroundQueue = diffOnBackgroundQueue
self.reloadDataOnReplacingViewModel = reloadDataOnReplacingViewModel
}
}
Expand Down
2 changes: 0 additions & 2 deletions Sources/DebugDescriptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,6 @@ private func debugDescriptionBuilder<Target: TextOutputStream>(
debugDescriptionBuilder(
elements: [
(.type(CollectionViewDriverOptions.self), indent + 2),
(.field(label: "diffOnBackgroundQueue", value: options.diffOnBackgroundQueue), indent + 4),
(.field(label: "reloadDataOnReplacingViewModel", value: options.reloadDataOnReplacingViewModel), indent + 4),
(.end, indent + 2)
],
Expand Down Expand Up @@ -203,7 +202,6 @@ func driverOptionsDebugDescription(_ options: CollectionViewDriverOptions) -> St
debugDescriptionBuilder(
elements: [
(.type(CollectionViewDriverOptions.self), 0),
(.field(label: "diffOnBackgroundQueue", value: options.diffOnBackgroundQueue), 2),
(.field(label: "reloadDataOnReplacingViewModel", value: options.reloadDataOnReplacingViewModel), 2),
(.end, 0)
],
Expand Down
128 changes: 44 additions & 84 deletions Sources/DiffableDataSource.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,34 +26,23 @@ final class DiffableDataSource: UICollectionViewDiffableDataSource<AnyHashable,
// Thus, unowned is safe here.
private unowned let _collectionView: UICollectionView

private let _diffOnBackgroundQueue: Bool

private lazy var _diffingQueue = DispatchQueue(
label: "com.jessesquires.ReactiveCollectionsKit",
qos: .userInteractive,
autoreleaseFrequency: .workItem
)

var logger: Logging?

// MARK: Init

init(
view: UICollectionView,
diffOnBackgroundQueue: Bool,
cellProvider: @escaping DiffableDataSource.CellProvider,
supplementaryViewProvider: @escaping DiffableDataSource.SupplementaryViewProvider
) {
self._collectionView = view
self._diffOnBackgroundQueue = diffOnBackgroundQueue
super.init(collectionView: view, cellProvider: cellProvider)
self.supplementaryViewProvider = supplementaryViewProvider
}

convenience init(view: UICollectionView, diffOnBackgroundQueue: Bool) {
convenience init(view: UICollectionView) {
self.init(
view: view,
diffOnBackgroundQueue: diffOnBackgroundQueue,
cellProvider: { _, _, _ in nil },
supplementaryViewProvider: { _, _, _ in nil }
)
Expand All @@ -66,12 +55,8 @@ final class DiffableDataSource: UICollectionViewDiffableDataSource<AnyHashable,

let snapshot = DiffableSnapshot(viewModel: viewModel)
self.applySnapshotUsingReloadData(snapshot) {
// UIKit guarantees `completion` is called on the main queue.
dispatchPrecondition(condition: .onQueue(.main))
MainActor.assumeIsolated {
completion?()
self.logger?.log("DataSource reload snapshot completed")
}
completion?()
self.logger?.log("DataSource reload snapshot completed")
}
}

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

if self._diffOnBackgroundQueue {
self.logger?.log("DataSource using background queue")
self._diffingQueue.async {
self._applySnapshot(
from: source,
to: destination,
withVisibleItems: visibleItemIdentifiers,
animated: animated,
completion: completion
)
}
} else {
self.logger?.log("DataSource using main queue")
dispatchPrecondition(condition: .onQueue(.main))
self._applySnapshot(
from: source,
to: destination,
withVisibleItems: visibleItemIdentifiers,
animated: animated,
completion: completion
)
}
self._applySnapshot(
from: source,
to: destination,
withVisibleItems: visibleItemIdentifiers,
animated: animated,
completion: completion
)
}

// MARK: Private

nonisolated private func _applySnapshot(
private func _applySnapshot(
from source: CollectionViewModel,
to destination: CollectionViewModel,
withVisibleItems visibleItemIdentifiers: Set<UniqueIdentifier>,
Expand Down Expand Up @@ -153,59 +123,49 @@ final class DiffableDataSource: UICollectionViewDiffableDataSource<AnyHashable,
destinationSnapshot.reconfigureItems(itemsToReconfigure)

// Apply the snapshot with item reconfigure updates.
//
// Swift 6 complains about 'call to main actor-isolated instance method' here.
// However, call this method from a background thread is valid according to the docs.
self.apply(destinationSnapshot, animatingDifferences: animated) { [weak self] in
// UIKit guarantees `completion` is called on the main queue.
dispatchPrecondition(condition: .onQueue(.main))

guard let self else {
MainActor.assumeIsolated {
completion?()
}
completion?()
return
}

MainActor.assumeIsolated {
// Once the snapshot with item reconfigures is applied,
// we need to find and apply supplementary view reconfigures, if needed.
//
// This is necessary to update all headers, footers, and supplementary views.
// Per notes above, supplementary views do not get reloaded / reconfigured
// automatically by `DiffableDataSource` when they change.
//
// To trigger updates on supplementary views with the existing APIs,
// the entire section must be reloaded. Yes, that sucks. We don't want to do that.
// That causes all items in the section to be hard-reloaded, too.
// Aside from the performance impact, doing that results in an ugly UI "flash"
// for all item cells in the collection. Gross.
//
// However, we can actually do much better than a hard reload!
// Instead of reloading the entire section, we can find and compare
// the supplementary views and manually reconfigure them if they changed.
//
// NOTE: this only matters if supplementary views are not static.
// That is, if they reflect data in the data source.
//
// For example, a header with a fixed title (e.g. "My Items") will NOT need to be reloaded.
// However, a header that displays changing data WILL need to be reloaded.
// (e.g. "My 10 Items")

// Check all the supplementary views and reconfigure them, if needed.
self._reconfigureSupplementaryViewsIfNeeded(from: source, to: destination)

// Finally, we're done and can call completion.
completion?()

self.logger?.log("DataSource diffing snapshot complete")
}
// Once the snapshot with item reconfigures is applied,
// we need to find and apply supplementary view reconfigures, if needed.
//
// This is necessary to update all headers, footers, and supplementary views.
// Per notes above, supplementary views do not get reloaded / reconfigured
// automatically by `DiffableDataSource` when they change.
//
// To trigger updates on supplementary views with the existing APIs,
// the entire section must be reloaded. Yes, that sucks. We don't want to do that.
// That causes all items in the section to be hard-reloaded, too.
// Aside from the performance impact, doing that results in an ugly UI "flash"
// for all item cells in the collection. Gross.
//
// However, we can actually do much better than a hard reload!
// Instead of reloading the entire section, we can find and compare
// the supplementary views and manually reconfigure them if they changed.
//
// NOTE: this only matters if supplementary views are not static.
// That is, if they reflect data in the data source.
//
// For example, a header with a fixed title (e.g. "My Items") will NOT need to be reloaded.
// However, a header that displays changing data WILL need to be reloaded.
// (e.g. "My 10 Items")

// Check all the supplementary views and reconfigure them, if needed.
self._reconfigureSupplementaryViewsIfNeeded(from: source, to: destination)

// Finally, we're done and can call completion.
completion?()

self.logger?.log("DataSource diffing snapshot complete")
}
}

// MARK: Reconfiguring Cells

nonisolated private func _findItemsToReconfigure(
private func _findItemsToReconfigure(
from source: CollectionViewModel,
to destination: CollectionViewModel,
withVisibleItems visibleItemIdentifiers: Set<UniqueIdentifier>
Expand Down
17 changes: 0 additions & 17 deletions Tests/TestCollectionViewDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -430,23 +430,6 @@ final class TestCollectionViewDriver: UnitTestCase, @unchecked Sendable {
self.waitForExpectations()
}

@MainActor
func test_update_callsCompletion_withBackgroundDiffing() {
let driver = CollectionViewDriver(
view: self.collectionView,
options: .init(diffOnBackgroundQueue: true)
)

let expectation = self.expectation()

let newModel = self.fakeCollectionViewModel()
driver.update(viewModel: newModel, animated: true) { _ in
expectation.fulfillAndLog()
}

self.waitForExpectations()
}

@MainActor
func test_update_callsCompletion_withReloadOnReplace() {
let driver = CollectionViewDriver(
Expand Down
4 changes: 0 additions & 4 deletions Tests/TestCollectionViewDriverOptions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ final class TestCollectionViewDriverOptions: XCTestCase {

func test_defaultValues() {
let options = CollectionViewDriverOptions()
XCTAssertFalse(options.diffOnBackgroundQueue)
XCTAssertFalse(options.reloadDataOnReplacingViewModel)
}

Expand All @@ -29,21 +28,18 @@ final class TestCollectionViewDriverOptions: XCTestCase {
options.debugDescription,
"""
CollectionViewDriverOptions {
diffOnBackgroundQueue: false
reloadDataOnReplacingViewModel: false
}
"""
)

let options2 = CollectionViewDriverOptions(
diffOnBackgroundQueue: true,
reloadDataOnReplacingViewModel: true
)
XCTAssertEqual(
options2.debugDescription,
"""
CollectionViewDriverOptions {
diffOnBackgroundQueue: true
reloadDataOnReplacingViewModel: true
}
"""
Expand Down
3 changes: 0 additions & 3 deletions Tests/TestDebugDescriptionDriver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@ final class TestDebugDescriptionDriver: XCTestCase {
CollectionViewDriver \\{
options:
CollectionViewDriverOptions \\{
diffOnBackgroundQueue: false
reloadDataOnReplacingViewModel: false
\\}
viewModel:
Expand Down Expand Up @@ -94,7 +93,6 @@ final class TestDebugDescriptionDriver: XCTestCase {
CollectionViewDriver \\{
options:
CollectionViewDriverOptions \\{
diffOnBackgroundQueue: false
reloadDataOnReplacingViewModel: false
\\}
viewModel:
Expand Down Expand Up @@ -155,7 +153,6 @@ final class TestDebugDescriptionDriver: XCTestCase {
CollectionViewDriver \\{
options:
CollectionViewDriverOptions \\{
diffOnBackgroundQueue: false
reloadDataOnReplacingViewModel: false
\\}
viewModel:
Expand Down
2 changes: 1 addition & 1 deletion Tests/Utils/CollectionViewDriverOptions+Extensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ import ReactiveCollectionsKit

extension CollectionViewDriverOptions {
static func test() -> Self {
.init(diffOnBackgroundQueue: false, reloadDataOnReplacingViewModel: true)
.init(reloadDataOnReplacingViewModel: true)
}
}