Skip to content

Isolate the UIKit-facing API to the main actor - #54

Open
radimvaculik wants to merge 1 commit into
futuredapp:mainfrom
radimvaculik:feature/main-actor-isolation
Open

radimvaculik wants to merge 1 commit into
futuredapp:mainfrom
radimvaculik:feature/main-actor-isolation

Conversation

@radimvaculik

Copy link
Copy Markdown

Motivation

CellKit is a UITableView/UICollectionView data source layer — every one of its protocols is only ever exercised on the main thread. But the protocols are declared nonisolated, so under Swift 6 every consumer conformance reports:

warning: conformance of 'FooCellModel' to protocol 'CellConvertible' crosses into
main actor-isolated code and can cause data races; this is an error in the Swift 6 language mode

In an app I migrated to Swift 6 this accounted for 61 of 498 warnings — the second largest group. The only consumer-side workaround is to annotate every single conformance:

extension FooCellModel: CellModel, @MainActor ReusableCellConvertible {

which had to be repeated 45 times, plus a @preconcurrency import CellKit where a cell model is built off the main thread.

Change

@MainActor on the API that touches UIKit:

  • CellKitReusableView, CellConfigurable, CellModel, SupplementaryViewModel, CellConvertible, CellModelSelectable, CellModelDataSourceDelegate, DataSource, CollectionSupplementaryViewModel, AbstractDataSource, CellModelDataSource, LazyCellProvider, LazySupplementaryViewProvider
  • DiffableCellKitDifferentiableCellModel, DifferentiableCellModelDataSource

DifferentiableCellModelWrapper needed a small rework: DifferenceKit's Differentiable/Equatable requirements are nonisolated, so differenceIdentifier is now computed once in a @MainActor init and stored, and == uses MainActor.assumeIsolated. Wrappers are only ever created and diffed by DifferentiableCellModelDataSource, which is now main actor-isolated, so the assumption holds.

MainActor.assumeIsolated requires iOS 13, so Package.swift platforms go from .v9 to .v13. DiffableCellKit already required iOS 13 throughout, and current SPM toolchains no longer accept iOS 9 anyway.

This is source-breaking

Any conformance that is not main actor-isolated will stop compiling. In practice these types are all cells, cell models and data sources, so this should be a no-op for real consumers — but it is a breaking change and probably warrants a minor version bump.

Verification

xcodebuild build passes for both the CellKit and DiffableCellKit schemes on generic/platform=iOS. SwiftLint reports the same 9 pre-existing violations as main — none added.

Building CellKit with SWIFT_STRICT_CONCURRENCY=complete succeeds; two warnings remain in SupplementaryElementKind, which reads UICollectionView.elementKindSectionHeader/Footer from a nonisolated RawRepresentable conformance. I left it alone because marking the enum @MainActor would break RawRepresentable, and the values are immutable strings. Happy to address it separately if you'd prefer.

I could not run the app-side end-to-end check (the consumer pins the released tag), so I have not empirically confirmed that all 45 @MainActor annotations become removable — though that is what the change is for.

@jmarek41

jmarek41 commented Sep 7, 2026

Copy link
Copy Markdown
Member

Thanks for this, @radimvaculik — the analysis in the description (the 61 conformance warnings, the 45 hand-written @MainActor conformances) is exactly what convinced us to fix this at the library level. We went a different route in #55, which is now the PR we intend to land:

  • swift-tools-version:6.2 with .defaultIsolation(MainActor.self) + Swift 6 language mode on both targets, instead of annotating each declaration. It needs zero @MainActor in Sources/, builds with zero warnings in Swift 6 mode, and a plain Swift 6 consumer like yours needs no annotations on its conformances.
  • Platforms move to iOS 15 / tvOS 15 and CocoaPods support is dropped (the podspec couldn't have expressed either approach without duplicating it).

For the record, so the information isn't lost — three things we hit while reviewing this branch in the Swift 6 language mode (SWIFT_VERSION=6):

  1. DifferentiableCellModel.swift — the MainActor.assumeIsolated { lhs… rhs… } in == fails with sending 'lhs' risks causing data races (non-Sendable wrappers captured into a @MainActor closure).
  2. DifferentiableCellModelSection.swift:29cells.map(DifferentiableCellModelWrapper.init) now passes a @MainActor init as a bare function value: converting … loses global actor 'MainActor'.
  3. DifferentiableCellModelDataSource.swift — the nested Container enum doesn't inherit the class's @MainActor, so Container.reload calls UIKit from a nonisolated context.

Plus CellKit.podspec still says ios.deployment_target 9.0, which can't compile MainActor at all.

With defaultIsolation none of the DifferentiableCellModelWrapper rework is needed — SE-0470 isolated conformances let DifferenceKit's nonisolated generic algorithm accept the main actor-isolated wrapper as long as it's called from the main actor, which it always is.

Leaving this open for you; feel free to close it once #55 is merged, or point out anything you think we missed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants