Files
swift-composable-architectu…/Sources/ComposableArchitecture/Documentation.docc/Articles/SwiftConcurrency.md
Stephen Celis 57e804f1cc Macro bonanza (#2553)
* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* Silence test warnings

* wip

* wip

* wip

* update a bunch of docs

* wip

* wip

* fix

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* Kill integration tests for now

* wip

* wip

* wip

* wip

* updating docs for @Reducer macro

* replaced more Reducer protocols with @Reducer

* Fixed some broken docc references

* wip

* Some @Reducer docs

* more docs

* convert some old styles to new style

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* bump

* update tutorials to use body

* update tutorials to use DML on destination state enum

* Add diagnostic

* wip

* updated a few more tests

* wip

* wip

* Add another gotcha

* wip

* wip

* wip

* fixes

* wip

* wip

* wip

* wip

* wip

* fix

* wip

* remove for now

* wip

* wip

* updated some docs

* migration guides

* more migration guide

* fix ci

* fix

* soft deprecate all apis using AnyCasePath

* wip

* Fix

* fix tests

* swift-format 509 compatibility

* wip

* wip

* Update Sources/ComposableArchitecture/Macros.swift

Co-authored-by: Mateusz Bąk <bakmatthew@icloud.com>

* wip

* wip

* update optional state case study

* remove initializer

* Don't use @State for BasicsView integration demo

* fix tests

* remove reduce diagnostics for now

* diagnose error not warning

* Update Sources/ComposableArchitecture/Macros.swift

Co-authored-by: Jesse Tipton <jesse@jessetipton.com>

* wip

* move integration tests to cron

* Revert "move integration tests to cron"

This reverts commit f9bdf2f04b.

* disable flakey tests on CI

* wip

* wip

* Revert "Revert "move integration tests to cron""

This reverts commit 66aafa7327.

* fix

* wip

* fix

---------

Co-authored-by: Brandon Williams <mbrandonw@hey.com>
Co-authored-by: Mateusz Bąk <bakmatthew@icloud.com>
Co-authored-by: Brandon Williams <135203+mbrandonw@users.noreply.github.com>
Co-authored-by: Jesse Tipton <jesse@jessetipton.com>
2023-11-13 12:57:35 -08:00

3.6 KiB

Adopting Swift concurrency

Learn how to write safe, concurrent effects using Swift's structured concurrency.

As of version 5.6, Swift can provide many warnings for situations in which you might be using types and functions that are not thread-safe in concurrent contexts. Many of these warnings can be ignored for the time being, but in Swift 6 most (if not all) of these warnings will become errors, and so you will need to know how to prove to the compiler that your types are safe to use concurrently.

There primary way to create an Effect in the library is via Effect/run(priority:operation:catch:fileID:line:). It takes a @Sendable, asynchronous closure, which restricts the types of closures you can use for your effects. In particular, the closure can only capture Sendable variables that are bound with let. Mutable variables and non-Sendable types are simply not allowed to be passed to @Sendable closures.

There are two primary ways you will run into this restriction when building a feature in the Composable Architecture: accessing state from within an effect, and accessing a dependency from within an effect.

Accessing state in an effect

Reducers are executed with a mutable, inout state variable, and such variables cannot be accessed from within @Sendable closures:

@Reducer
struct Feature {
  struct State { /* ... */ }
  enum Action { /* ... */ }

  var body: some Reducer<State, Action> {
    Reduce { state, action in
      switch action {
      case .buttonTapped:
        return .run { send in
          try await Task.sleep(for: .seconds(1))
          await send(.delayed(state.count))
          // 🛑 Mutable capture of 'inout' parameter 'state' is
          //    not allowed in concurrently-executing code
        }

        // ...
      }
    }
  }
}

To work around this you must explicitly capture the state as an immutable value for the scope of the closure:

return .run { [state] send in
  try await Task.sleep(for: .seconds(1))
  await send(.delayed(state.count))  // ✅
}

You can also capture just the minimal parts of the state you need for the effect by binding a new variable name for the capture:

return .run { [count = state.count] send in
  try await Task.sleep(for: .seconds(1))
  return .delayed(count)  // ✅
}

Accessing dependencies in an effect

In the Composable Architecture, one provides dependencies to a reducer so that it can interact with the outside world in a deterministic and controlled manner. Those dependencies can be used from asynchronous and concurrent contexts, and so must be Sendable.

If your dependency is not sendable, you will be notified at the time of registering it with the library. In particular, when extending DependencyValues to provide the computed property:

extension DependencyValues {
  var factClient: FactClient {
    get { self[FactClient.self] }
    set { self[FactClient.self] = newValue }
  }
}

If FactClient is not Sendable, for whatever reason, you will get a warning in the get and set lines:

⚠️ Type 'FactClient' does not conform to the 'Sendable' protocol

To fix this you need to make each dependency Sendable. This usually just means making sure that the interface type only holds onto Sendable data, and in particular, any closure-based endpoints should be annotated as @Sendable:

struct FactClient {
  var fetch: @Sendable (Int) async throws -> String
}

This will restrict the kinds of closures that can be used when constructing FactClient values, thus making the entire FactClient sendable itself.