Skip to content

Swift Concurrency/Isolation Support (MainActor)#49

Open
alexvbush wants to merge 33 commits intouber:mainfrom
alexvbush:swift-concurrency-main-isolation
Open

Swift Concurrency/Isolation Support (MainActor)#49
alexvbush wants to merge 33 commits intouber:mainfrom
alexvbush:swift-concurrency-main-isolation

Conversation

@alexvbush
Copy link
Copy Markdown
Collaborator

Swift 6 Strict Concurrency Modernization: @MainActor isolation across the framework

Background

The original RIBs implementation predates Swift's strict concurrency model. At runtime, RIBs has always operated on the main thread — builders build on main, routers route on main, interactors activate and deactivate on main, workers start and stop on main. This was implicit convention, not enforced by the type system.

Swift 6's strict concurrency mode surfaces this as compile-time errors: the compiler sees shared mutable state with no declared isolation and flags it.

This PR is an alternative approach to #45, which addressed the same problem by marking everything nonisolated. While that silences the compiler, it works against the grain — it declares "this code has no actor isolation" when in practice it has always been main-thread-only. This PR takes the opposite approach: make the implicit explicit by annotating the entire framework with @MainActor.

What changed

@MainActor on all core framework types and protocols:
Buildable, Builder, Component, EmptyComponent, Dependency, EmptyDependency, InteractorScope, Interactable, Interactor, PresentableInteractor, Presentable, Presenter, RouterLifecycle, RouterScope, Routing, Router, ViewableRouting, ViewableRouter, LaunchRouting, LaunchRouter, ViewControllable, Working, Worker, Workflow, Step, LeakDetector, and all utility extensions (confineTo, disposeOnDeactivate, disposeOnStop, fork, disposeWith).

isolated deinit on all classes with non-trivial teardown:
Interactor, PresentableInteractor, Router, ViewableRouter, and Worker. These deinits call into @MainActor-isolated code — deactivate(), LeakDetector.instance.expectDeallocate() — which is unsafe if deinit runs off the actor. isolated deinit is a Swift 6.2 feature that guarantees deallocation runs on the main actor's executor. Requires Xcode 26.2 / Swift 6.2.

Tests: All test classes annotated @MainActor. Tests exercising deinit behavior marked async to accommodate the new isolation semantics.

CI: Pinned to macos-15, Xcode 26.2 explicitly selected (Swift 6.2 required for isolated deinit). Simulator destination pinned to iPhone SE (3rd generation) / iOS 18.5. Working directory updated to Examples/Example1 to reflect project structure.

Podspec: RxSwift/RxRelay dependency tightened from ~> 6.0 to ~> 6.9.0.

Migration guide (SWIFT6_STRICT_CONCURRENCY_MIGRATION.md): A step-by-step document covering how to migrate a RIBs-based iOS project to Swift's strict concurrency model — enabling @MainActor default isolation, resolving common compile errors, and handling the RxSwift @Sendable caveat.

New example app (RIBsAppExample2): A comprehensive example project illustrating framework patterns — headless RIBs, Worker, presenter separation, deep link workflows with Workflow and actionable item protocols, ComponentizedBuilder with dynamic component dependencies (scoped UserSession/CurrentUserService), and async/await bridging to RxSwift via Single.create.

Compatibility

Swift Default isolation Strictness Result
5 nonisolated Minimal ✅ Works, no changes needed
5 nonisolated Targeted ✅ Works, no changes needed
5 nonisolated Complete ✅ Works, no changes needed
5 @MainActor Minimal ✅ Works, no changes needed
5 @MainActor Targeted ✅ Works, no changes needed
5 @MainActor Complete ✅ Works, no changes needed
6 nonisolated Minimal ❌ Compile errors without codebase changes
6 nonisolated Targeted ❌ Compile errors without codebase changes
6 nonisolated Complete ❌ Compile errors without codebase changes
6 @MainActor Minimal ✅ Works (RxSwift @Sendable caveat — see below)
6 @MainActor Targeted ✅ Works (RxSwift @Sendable caveat — see below)
6 @MainActor Complete ✅ Works (RxSwift @Sendable caveat — see below)

Summary: All Swift 5 configurations require no changes. Swift 6 requires @MainActor as the project's default isolation — nonisolated default with Swift 6 produces compile errors due to actor isolation mismatches with the now-@MainActor-annotated framework types. Strictness level does not affect the outcome.

RxSwift @Sendable caveat (Swift 6 + @MainActor only)

Projects using Swift 6 with @MainActor default isolation may encounter a runtime crash in RxSwift operators (map, filter, flatMap, etc.) unless closures are annotated @Sendable. This is a known RxSwift limitation tracked in ReactiveX/RxSwift#2639 and is not introduced or caused by these RIBs changes.

// annotate affected closures
observable.map { @Sendable value in transform(value) }

The alternative is to migrate those pieces to async/await — RIBs now fully supports this at the type system level, and additional async/await convenience utilities are planned as a follow-up to this PR.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant