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 run(priority:operation:catch:fileID:filePath:line:column:)
. 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 {
@ObservableState
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))
await send(.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.