DEV Community

Morgan
Morgan

Posted on

TCA 1.7 @ObservableState + Swift 6 — Tracing the @Reducer Macro Isolation Trap

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
Enter fullscreen mode Exit fullscreen mode

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 ... }
    }
}
Enter fullscreen mode Exit fullscreen mode

Build result:

SomeView.swift:13: error: default argument cannot be both
  main actor-isolated and nonisolated
    @State private var globalStore = Store(
     ^
Enter fullscreen mode Exit fullscreen mode

→ 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 { ... }
    ...
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

→ 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
    ...
}
Enter fullscreen mode Exit fullscreen mode

Build result:

@__swiftmacro_*_Action_CasePathable.swift:3:17:
  error: main actor-isolated conformance of 'SomeFeature.Action'
         to 'CasePathable' cannot be used in nonisolated context
Enter fullscreen mode Exit fullscreen mode

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> { ... }
}
Enter fullscreen mode Exit fullscreen mode

Build result:

SomeView.swift: error: argument type 'SomeFeature' does not
  conform to expected type 'Reducer'
Enter fullscreen mode Exit fullscreen mode

→ 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 ... }
    }
}
Enter fullscreen mode Exit fullscreen mode

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 { ... }
    }
}
Enter fullscreen mode Exit fullscreen mode

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 = NO switches 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))
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Build result:

The compiler is unable to type-check this expression in reasonable time;
try breaking up the expression into distinct sub-expressions
Enter fullscreen mode Exit fullscreen mode

→ 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 WithViewStore deprecation 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)