A retrospective of tracing the @Reducer / @ObservableState macro isolation trap in 5 attempts during new development of a personal iOS app on TCA 1.25.5. Under Swift 6 strict concurrency + module-default isolation inference, the macros auto-generate conformances that are forced to be main-actor-isolated, breaking compatibility with nonisolated struct.
Starting Point — Deprecation Warnings During New Development
I started building a personal app on TCA 1.25.5 from scratch. As soon as I wrote the first entry screen (an API key input form) using the WithViewStore pattern, the Issue Navigator lit up with deprecation warnings:
'WithViewStore' is deprecated: Use '@ObservableState' instead.
'init(_:observe:content:file:line:)' is deprecated: Use '@ObservableState' instead.
'ViewStoreOf' is deprecated: Use '@ObservableState' instead.
See the following migration guide:
https://swiftpackageindex.com/pointfreeco/swift-composable-architecture/main/documentation/composablearchitecture/migrationguides/migratingto17#using-observablestate
WithViewStore is deprecated in TCA 1.7+, so even brand-new code triggers the warnings immediately. I followed the official migration guide to apply the @ObservableState macro pattern.
(As I added more views in the same pattern, the deprecation warnings accumulated to 30+.)
Environment
- Swift 6.0
- Xcode 26.x (the exact minor version relies on memory and is uncertain)
- TCA 1.25.5
- iOS 17+
-
SWIFT_APPROACHABLE_CONCURRENCY = NO(set explicitly on the main target)
Time Axis — Same Trap, Different Detection Points
2026-04-12 — While writing the first entry screen (API key input form), I applied @ObservableState + @Reducer macros. The build passed, but the simulator hit a runtime stack overflow on launch. The macro-applied code at this point was never committed (it was in-progress development; no traces in git).
I fixed it with macro-avoidance + nonisolated struct + explicit Reducer pattern → the project's first commit reflects the fix.
2026-05-11 (today, about a month later) — Trying to reproduce the same pattern. The build itself fails this time. The compiler now validates the isolation of macro-generated conformances at compile time.
Same trap, different detection points. In one month, the compiler moved the failure mode from runtime to compile time.
Attempt 1 — @Reducer struct (no nonisolated)
@Reducer
struct SomeFeature {
@ObservableState
struct State: Equatable { ... }
enum Action: Equatable { ... }
var body: some ReducerOf<Self> {
Reduce { state, action in ... }
}
}
Build result:
SomeView.swift:13: error: default argument cannot be both
main actor-isolated and nonisolated
@State private var globalStore = Store(
^
→ The View runs in @MainActor context (SwiftUI default). At Store init, the default argument is main-actor-isolated, while the macro-applied struct is inferred as nonisolated under SWIFT_APPROACHABLE_CONCURRENCY = NO. They collide.
Attempt 2 — @Reducer nonisolated struct + @ObservableState
@Reducer
nonisolated struct SomeFeature {
@ObservableState
struct State: Equatable { ... }
...
}
Build result:
@__swiftmacro_*_State_role_ObservationTracked.swift:7:32:
error: main actor-isolated conformance of 'SomeFeature.State'
to 'Observable' cannot be used in nonisolated context
→ The error originates from the macro-expanded file (@__swiftmacro_*). The @ObservableState macro auto-generates an Observable conformance that is main-actor-isolated. The struct is marked nonisolated, but the conformance is main-actor-isolated → collision.
A macro-expanded file is generated for every State property → multiple errors.
Attempt 3 — Removing @ObservableState
What if we keep @Reducer but drop @ObservableState?
@Reducer
nonisolated struct SomeFeature {
struct State: Equatable { ... } // @ObservableState removed
...
}
Build result:
@__swiftmacro_*_Action_CasePathable.swift:3:17:
error: main actor-isolated conformance of 'SomeFeature.Action'
to 'CasePathable' cannot be used in nonisolated context
→ The @Reducer macro also auto-generates a CasePathable conformance for Action that is main-actor-isolated. Removing only @ObservableState doesn't solve it.
All macro-generated conformances are main-actor-isolated. Macro usage itself is incompatible with nonisolated.
Attempt 4 — Removing @Reducer (no : Reducer)
nonisolated struct SomeFeature { // : Reducer not declared
var body: some ReducerOf<Self> { ... }
}
Build result:
SomeView.swift: error: argument type 'SomeFeature' does not
conform to expected type 'Reducer'
→ Without the @Reducer macro, the auto-added Reducer protocol conformance disappears. You need to declare : Reducer explicitly.
Resolution — Avoid Macros Entirely
nonisolated struct SomeFeature: Reducer {
struct State: Equatable { ... }
enum Action: Equatable { ... }
var body: some ReducerOf<Self> {
Reduce { state, action in ... }
}
}
→ Avoid macros entirely + nonisolated struct + explicit : Reducer conformance + var body. Build passes; unit and integration tests pass.
A direct func reduce is also equivalent:
nonisolated struct SomeFeature: Reducer {
func reduce(into state: inout State, action: Action) -> Effect<Action> {
switch action { ... }
}
}
Both patterns work. My project standardized on var body + Reduce for consistency across all reducers.
Side Discovery — SWIFT_APPROACHABLE_CONCURRENCY = NO Doesn't Save You
The hypothesis I held going in:
"Setting
SWIFT_APPROACHABLE_CONCURRENCY = NOswitches the default inference back to nonisolated → macro-auto conformances also become nonisolated → no collision."
Verification — partially correct. The module-default isolation inference does fall back to nonisolated, but TCA macros (@Reducer / @ObservableState) generate main-actor-isolated conformances regardless of SWIFT_APPROACHABLE_CONCURRENCY.
This project already has SWIFT_APPROACHABLE_CONCURRENCY = NO set explicitly on the main target. Applying macros still triggers the same isolation collisions. → The macros themselves seem to enforce main-actor isolation for SwiftUI integration purposes.
Side Discovery — Macros Bind Feature and View as a Pair
| Scenario | Result |
|---|---|
| Feature macro + View macro | Isolation collisions (verified in attempts 1–3, workaround unknown) |
Feature macro + View WithViewStore
|
(Deprecation warnings remain) |
Feature macro-avoidance + View @Bindable |
Compiler type-check timeout |
Feature macro-avoidance + View WithViewStore
|
✅ Current pick (deprecation warnings only) |
Trying a macro-avoiding Feature with a macro-assuming View:
// Feature: macro-avoidance
nonisolated struct SomeFeature: Reducer {
struct State: Equatable {
var apiKey: String = ""
...
}
...
}
// View: assumes macros
struct SomeView: View {
@Bindable var store: StoreOf<SomeFeature>
var body: some View {
Form {
TextField("API Key", text: $store.apiKey.sending(\.apiKeyChanged))
}
}
}
Build result:
The compiler is unable to type-check this expression in reasonable time;
try breaking up the expression into distinct sub-expressions
→ Without @ObservableState, $store.apiKey.sending(...) can't infer the keypath. The compiler keeps trying to resolve types and times out. Macros bind Feature and View as a pair. Partial application doesn't work.
Conclusion
The TCA 1.7+ official migration guide explains the SwiftUI integration path well, but it doesn't address the incompatibility between Swift 6 strict concurrency and macro-generated isolation.
Macro usage vs avoidance — pick one:
- All macros — TCA 1.7+ modern style. But within my verified scope, there's no known workaround for the Swift 6 isolation conflict.
-
All macro avoidance — Accept the
WithViewStoredeprecation warnings + nonisolated struct + explicit Reducer protocol conformance. No build/runtime/test impact.
This app went with the latter. Accept 30+ warnings, avoid macros.
Same Trap, Different Detection Points — Meta
The most striking meta point — in about one month, the compiler moved the failure point.
- 2026-04-12: Build passed → runtime stack overflow
- 2026-05-11: Caught at compile time (conformance isolation validation)
Same pattern, same environment (assumed), but different detection points. A trace of an Xcode minor update or environment change. On my current Xcode 26.2 / Swift 6.0, getting caught at compile time is the new normal.
Top comments (0)