Skip to content

Adopt default main actor isolation and drop CocoaPods - #55

Merged
jmarek41 merged 7 commits into
mainfrom
feature/default-main-actor-isolation
Sep 7, 2026
Merged

Adopt default main actor isolation and drop CocoaPods#55
jmarek41 merged 7 commits into
mainfrom
feature/default-main-actor-isolation

Conversation

@jmarek41

@jmarek41 jmarek41 commented Aug 15, 2026

Copy link
Copy Markdown
Member

Supersedes #54 (left open for its author; see the comment there). Credit to @radimvaculik for the analysis that motivated this.

Motivation

CellKit is a UITableView/UICollectionView data-source layer — every one of its protocols is only ever exercised on the main thread. Under Swift 6 that mismatch surfaces as conformance of 'FooCellModel' to protocol 'CellConvertible' crosses into main actor-isolated code on every consumer conformance (61 warnings in one app, 45 hand-written @MainActor conformances to silence them).

Change

defaultIsolation set to MainActor with the Swift 6 language mode on both targets (SE-0466; the setting Apple's WWDC25 guidance recommends for UI-focused modules):

let swiftSettings: [SwiftSetting] = [
    .swiftLanguageMode(.v6),
    .defaultIsolation(MainActor.self),
    .enableUpcomingFeature("NonisolatedNonsendingByDefault"),
    .enableUpcomingFeature("InferIsolatedConformances")
]

Zero @MainActor annotations in Sources/. The only Sources/ edits are removals: redundant @available(iOS 13, *), #if SWIFT_PACKAGE guards, two long-standing SwiftLint violations.

  • Platforms → iOS 15 / tvOS 15, matching FTAPIKit. Requires swift-tools-version:6.2 → Xcode 26+.
  • CocoaPods removed (CellKit.podspec, Gemfile, Gemfile.lock). Swift Package only from 1.0.0. Removing Gemfile.lock also clears the 11 open rubygems security alerts; Bump cocoapods-downloader from 1.3.0 to 1.6.3 #51Bump activesupport from 4.2.11.3 to 7.2.3.1 #53 were closed as superseded.
  • DifferenceKit floor → 1.3.0 (the only release with the current API, already what Package.resolved pinned).
  • Example app: Swift 6 language mode, SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor, Approachable Concurrency — how our apps are configured. Adopts the UIScene lifecycle (iOS 27 terminates apps that don't). Fixes cellHeight declared as CGFloat — it never satisfied the Double requirement, so every row silently fell back to 44pt.
  • Example UI tests actually run now. The project had no shared scheme, so xcodebuild test had never built ExampleUITests. Added Example.xcscheme, and replaced the XCTAssert(true) placeholder with three tests that drive the main actor-isolated DifferentiableCellModelDataSource on a real table: sections render; tapping a cell updates it through a diff; inserting rows and resetting diffs correctly.
  • CI rewritten to match FTAPIKit's ci.yml (actions/checkout@v7, brew install swiftlint, swiftlint --strict), with xcodebuild in place of swift build/swift test because CellKit is UIKit-only: both library builds, then the Example UI tests on a simulator chosen dynamically (newest iOS runtime, first iPhone) so runner image changes don't break it. Job id stays test (the required check on main).

Comparison with #54

#54 annotates each declaration by hand and reworks DifferentiableCellModelWrapper with MainActor.assumeIsolated. Building that branch in the Swift 6 language mode still fails in DiffableCellKit:

  1. ==: sending 'lhs' risks causing data races — non-Sendable wrappers captured into the @MainActor closure.
  2. arraySection: cells.map(DifferentiableCellModelWrapper.init) passes a @MainActor init as a bare function value — loses global actor 'MainActor'.
  3. Nested Container.reload doesn't inherit the class's @MainActor and calls UIKit from a nonisolated context.

It also leaves CellKit.podspec at iOS 9, which cannot compile MainActor. With defaultIsolation none of the wrapper rework is needed: SE-0470 isolated conformances let DifferenceKit's nonisolated generic algorithm accept the main actor-isolated wrapper because it is always called from the main actor.

Consumer impact

Verified with a real consumer module against this branch:

Consumer module (Swift 6) main today This branch
Default nonisolated isolation (#54's case) 1 annotation per model type none
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor 1 per model type @MainActor per CellKit protocol conformed to (Xcode 26); none on Xcode 27 / Swift 6.4, which infers them

The second row is an SE-0470 inference gap for conformances to protocols that are themselves main actor-isolated; it pre-dates this PR and is documented in the README. The Example app shows that configuration.

Verification

  • swiftlint --strict: 0 violations.
  • xcodebuild build for CellKit and DiffableCellKit (generic/platform=iOS): 0 warnings, Swift 6 language mode. Verified on Xcode 26.6 / Swift 6.3.3 and Xcode 27.0 beta / Swift 6.4.
  • xcodebuild test Example: 3/3 UI tests pass on iPhone 17 Pro Max (iOS 27.0) and iPhone 16 (iOS 18.6), twice each with retries disabled. The app was also launched on the iPhone 16 simulator and visually checked: nav bar actions, 170pt "Welcome!" cell, "Cell Phones" header, five iPhone and five Android rows at 60pt.
  • Consumer matrix above re-run with swiftc against the built module.

Breaking

iOS 15+ / tvOS 15+, Xcode 26+ to build, CocoaPods dropped. Any conformance that is not main actor-isolated stops compiling. Ships as 1.0.0 — the README already points at from: "1.0.0"; the tag is cut right after merge.

Set `defaultIsolation` to `MainActor` and the Swift 6 language mode for
both targets, which makes the whole UIKit-facing API main actor-isolated
without a single annotation in the library sources.

Platforms move to iOS 15 / tvOS 15 to match FTAPIKit, which makes the
`@available(iOS 13.0, tvOS 13.0, *)` annotations redundant.

CocoaPods support is removed in favour of the Swift package, so the
`#if SWIFT_PACKAGE` guards around `import CellKit` go away too.

The Example app is updated to the Swift 6 language mode with default main
actor isolation and approachable concurrency, matching how apps consuming
CellKit are configured.

CI is rewritten to match FTAPIKit: no `pod lib lint`, current runner
actions, and a build of both library schemes plus the Example app. The two
long-standing SwiftLint violations in DataSource.swift are fixed so
`swiftlint --strict` passes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jmarek41
jmarek41 requested a review from a team as a code owner August 15, 2026 18:27
@jmarek41

Copy link
Copy Markdown
Member Author

Closing for now — keeping the work on the branch while we settle the approach.

@jmarek41 jmarek41 closed this Aug 15, 2026
Replace the two individual SWIFT_UPCOMING_FEATURE_* settings with
SWIFT_APPROACHABLE_CONCURRENCY, which is what our projects actually set and
which resolves to the same upcoming features in the Swift 6 language mode.

Clarify in the README why a main actor-isolated consumer still needs
`@MainActor` on each conformance: conformance isolation is not inferred for
conformances to protocols that are themselves main actor-isolated, and
InferIsolatedConformances does not cover that case.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
jmarek41 and others added 3 commits September 7, 2026 15:15
The Example project had no shared scheme, so `xcodebuild test` never
built or ran ExampleUITests. Add Example.xcscheme with the UI test bundle
in its Test action.

Adopt the UIScene lifecycle: iOS 27 terminates apps that still rely on
the legacy `AppDelegate.window` path. Add SceneDelegate and the scene
manifest, and drop the now redundant UIMainStoryboardFile.

The UI test target inherits SWIFT_DEFAULT_ACTOR_ISOLATION from the
project; XCTestCase overrides must stay nonisolated, so set the test
target back to nonisolated.

Replace the placeholder launch test with three tests that exercise the
main actor-isolated DifferentiableCellModelDataSource on a real table
view: sections render, tapping a cell updates it through a diff, and
inserting and resetting rows diffs correctly.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1.3.0 is the only DifferenceKit release with the current API and is what
Package.resolved already pins. Dependabot now watches the Swift package
and GitHub Actions instead of the removed Gemfile.

Note in the README that Swift 6.4 infers the isolated conformances a
main actor-isolated consumer otherwise has to write by hand.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Pick the newest iOS runtime and its first available iPhone simulator at
run time so the step keeps working when runner images change, and print
the toolchain version up front. Keep the job id `test`, which is the
required status check on main.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
jmarek41 and others added 2 commits September 7, 2026 15:19
The cell models declared `cellHeight` as `CGFloat`, which never matched
the protocol's `Double` requirement, so every row silently fell back to
the 44pt default and the 50pt "Welcome!" label was squeezed out of view.

The insert/reset test read the first device cell's label immediately
after tapping, racing the animated insert. Wait for the label to change
with a predicate expectation instead of assuming it already has.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Bump actions/checkout to v7, remove the toolchain diagnostic step, and
keep the same lint setup as FTAPIKit (SwiftLint is not preinstalled on
macos-latest, so brew install stays). xcodebuild remains in place of
`swift build`/`swift test` because CellKit is UIKit-only.

Neither FTAPIKit nor FuturedKit ships a dependabot.yml, so remove it here
too.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@jmarek41
jmarek41 merged commit a6f457d into main Sep 7, 2026
1 check passed
@jmarek41
jmarek41 deleted the feature/default-main-actor-isolation branch September 7, 2026 14:34
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