Structure
Effect
public struct Effect<Output, Failure: Error>: Publisher
The Effect
type encapsulates a unit of work that can be run in the outside world, and can
feed data back to the Store
. It is the perfect place to do side effects, such as network
requests, saving/loading from disk, creating timers, interacting with dependencies, and more.
Effects are returned from reducers so that the Store
can perform the effects after the
reducer is done running. It is important to note that Store
is not thread safe, and so all
effects must receive values on the same thread, and if the store is being used to drive UI
then it must receive values on the main thread.
An effect simply wraps a Publisher
value and provides some convenience initializers for
constructing some common types of effects.
Relationships
Nested Types
Effect.Subscriber
Conforms To
Publisher
Initializers
init(_:)
public init<P: Publisher>(_ publisher: P) where P.Output == Output, P.Failure == Failure
Initializes an effect that wraps a publisher. Each emission of the wrapped publisher will be emitted by the effect.
This initializer is useful for turning any publisher into an effect. For example:
Effect(
NotificationCenter.default
.publisher(for: UIApplication.userDidTakeScreenshotNotification)
)
Alternatively, you can use the .eraseToEffect()
method that is defined on the Publisher
protocol:
NotificationCenter.default
.publisher(for: UIApplication.userDidTakeScreenshotNotification)
.eraseToEffect()
Parameters
Name | Type | Description |
---|---|---|
publisher | P |
A publisher. |
init(value:)
public init(value: Output)
Initializes an effect that immediately emits the value passed in.
Parameters
Name | Type | Description |
---|---|---|
value | Output |
The value that is immediately emitted by the effect. |
init(error:)
public init(error: Failure)
Initializes an effect that immediately fails with the error passed in.
Parameters
Name | Type | Description |
---|---|---|
error | Failure |
The error that is immediately emitted by the effect. |
Properties
upstream
public let upstream: AnyPublisher<Output, Failure>
Methods
receive(subscriber:)
public func receive<S>(
subscriber: S
) where S: Combine.Subscriber, Failure == S.Failure, Output == S.Input
future(_:)
public static func future(
_ attemptToFulfill: @escaping (@escaping (Result<Output, Failure>) -> Void) -> Void
) -> Effect
Creates an effect that can supply a single value asynchronously in the future.
This can be helpful for converting APIs that are callback-based into ones that deal with
Effect
s.
For example, to create an effect that delivers an integer after waiting a second:
Effect<Int, Never>.future { callback in
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
callback(.success(42))
}
}
Note that you can only deliver a single value to the callback
. If you send more they will be
discarded:
Effect<Int, Never>.future { callback in
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
callback(.success(42))
callback(.success(1729)) // Will not be emitted by the effect
}
}
If you need to deliver more than one value to the effect, you should use the Effect
initializer that accepts a Subscriber
value.
Parameters
Name | Type | Description |
---|---|---|
attemptToFulfill | @escaping (@escaping (Result<Output, Failure>) -> Void) -> Void |
A closure that takes a |
result(_:)
public static func result(_ attemptToFulfill: @escaping () -> Result<Output, Failure>) -> Self
Initializes an effect that lazily executes some work in the real world and synchronously sends that data back into the store.
For example, to load a user from some JSON on the disk, one can wrap that work in an effect:
Effect<User, Error>.result {
let fileUrl = URL(
fileURLWithPath: NSSearchPathForDirectoriesInDomains(
.documentDirectory, .userDomainMask, true
)[0]
)
.appendingPathComponent("user.json")
let result = Result<User, Error> {
let data = try Data(contentsOf: fileUrl)
return try JSONDecoder().decode(User.self, from: $0)
}
return result
}
Parameters
Name | Type | Description |
---|---|---|
attemptToFulfill | @escaping () -> Result<Output, Failure> |
A closure encapsulating some work to execute in the real world. |
Returns
An effect.
run(_:)
public static func run(
_ work: @escaping (Effect.Subscriber) -> Cancellable
) -> Self
Initializes an effect from a callback that can send as many values as it wants, and can send a completion.
This initializer is useful for bridging callback APIs, delegate APIs, and manager APIs to the
Effect
type. One can wrap those APIs in an Effect so that its events are sent through the
effect, which allows the reducer to handle them.
For example, one can create an effect to ask for access to MPMediaLibrary
. It can start by
sending the current status immediately, and then if the current status is notDetermined
it
can request authorization, and once a status is received it can send that back to the effect:
Effect.run { subscriber in
subscriber.send(MPMediaLibrary.authorizationStatus())
guard MPMediaLibrary.authorizationStatus() == .notDetermined else {
subscriber.send(completion: .finished)
return AnyCancellable {}
}
MPMediaLibrary.requestAuthorization { status in
subscriber.send(status)
subscriber.send(completion: .finished)
}
return AnyCancellable {
// Typically clean up resources that were created here, but this effect doesn't
// have any.
}
}
Parameters
Name | Type | Description |
---|---|---|
work | @escaping (Effect.Subscriber) -> Cancellable |
A closure that accepts a |
concatenate(_:)
Concatenates a variadic list of effects together into a single effect, which runs the effects one after the other.
Parameters
Name | Type | Description |
---|---|---|
effects | Effect |
A variadic list of effects. |
Returns
A new effect
concatenate(_:)
Concatenates a collection of effects together into a single effect, which runs the effects one after the other.
Parameters
Name | Type | Description |
---|---|---|
effects | C |
A collection of effects. |
Returns
A new effect
merge(_:)
Merges a variadic list of effects together into a single effect, which runs the effects at the same time.
Parameters
Name | Type | Description |
---|---|---|
effects | Effect |
A list of effects. |
Returns
A new effect
merge(_:)
Merges a sequence of effects together into a single effect, which runs the effects at the same time.
Parameters
Name | Type | Description |
---|---|---|
effects | S |
A sequence of effects. |
Returns
A new effect
fireAndForget(_:)
public static func fireAndForget(_ work: @escaping () -> Void) -> Effect
Creates an effect that executes some work in the real world that doesn't need to feed data back into the store.
Parameters
Name | Type | Description |
---|---|---|
work | @escaping () -> Void |
A closure encapsulating some work to execute in the real world. |
Returns
An effect.
map(_:)
public func map<T>(_ transform: @escaping (Output) -> T) -> Effect<T, Failure>
Transforms all elements from the upstream effect with a provided closure.
Parameters
Name | Type | Description |
---|---|---|
transform | @escaping (Output) -> T |
A closure that transforms the upstream effect's output to a new output. |
Returns
A publisher that uses the provided closure to map elements from the upstream effect to new elements that it then publishes.
cancellable(id:cancelInFlight:)
public func cancellable(id: AnyHashable, cancelInFlight: Bool = false) -> Effect
Turns an effect into one that is capable of being canceled.
To turn an effect into a cancellable one you must provide an identifier, which is used in
Effect/cancel(id:)
to identify which in-flight effect should be canceled. Any hashable
value can be used for the identifier, such as a string, but you can add a bit of protection
against typos by defining a new type that conforms to Hashable
, such as an empty struct:
struct LoadUserId: Hashable {}
case .reloadButtonTapped:
// Start a new effect to load the user
return environment.loadUser
.map(Action.userResponse)
.cancellable(id: LoadUserId(), cancelInFlight: true)
case .cancelButtonTapped:
// Cancel any in-flight requests to load the user
return .cancel(id: LoadUserId())
Parameters
Name | Type | Description |
---|---|---|
id | AnyHashable |
The effect's identifier. |
cancelInFlight | Bool |
Determines if any in-flight effect with the same identifier should be canceled before starting this new one. |
Returns
A new effect that is capable of being canceled by an identifier.
cancel(id:)
public static func cancel(id: AnyHashable) -> Effect
An effect that will cancel any currently in-flight effect with the given identifier.
Parameters
Name | Type | Description |
---|---|---|
id | AnyHashable |
An effect identifier. |
Returns
A new effect that will cancel any currently in-flight effect with the given identifier.
cancel(ids:)
@_disfavoredOverload
public static func cancel(ids: AnyHashable...) -> Effect
An effect that will cancel multiple currently in-flight effects with the given identifiers.
Parameters
Name | Type | Description |
---|---|---|
ids | AnyHashable |
A variadic list of effect identifiers. |
Returns
A new effect that will cancel any currently in-flight effects with the given identifiers.
cancel(ids:)
public static func cancel<S: Sequence>(ids: S) -> Effect where S.Element == AnyHashable
An effect that will cancel multiple currently in-flight effects with the given identifiers.
Parameters
Name | Type | Description |
---|---|---|
ids | S |
A sequence of effect identifiers. |
Returns
A new effect that will cancel any currently in-flight effects with the given identifiers.
task(priority:operation:_:)
public static func task(
priority: TaskPriority? = nil,
operation: @escaping @Sendable () async -> Output
) -> Self where Failure == Never
Wraps an asynchronous unit of work in an effect.
This function is useful for executing work in an asynchronous context and capture the
result in an Effect
so that the reducer, a non-asynchronous context, can process it.
Effect.task {
guard case let .some((data, _)) = try? await URLSession.shared
.data(from: .init(string: "http://numbersapi.com/42")!)
else {
return "Could not load"
}
return String(decoding: data, as: UTF8.self)
}
Note that due to the lack of tools to control the execution of asynchronous work in Swift, it is not recommended to use this function in reducers directly. Doing so will introduce thread hops into your effects that will make testing difficult. You will be responsible for adding explicit expectations to wait for small amounts of time so that effects can deliver their output.
Instead, this function is most helpful for calling async
/await
functions from the live
implementation of dependencies, such as URLSession.data
, MKLocalSearch.start
and more.
Parameters
Name | Type | Description |
---|---|---|
priority | TaskPriority? |
Priority of the underlying task. If |
operation | @escaping @Sendable () |
The operation to execute. |
Returns
An effect wrapping the given asynchronous work.
debounce(id:for:scheduler:options:)
public func debounce<S: Scheduler>(
id: AnyHashable,
for dueTime: S.SchedulerTimeType.Stride,
scheduler: S,
options: S.SchedulerOptions? = nil
) -> Effect
Turns an effect into one that can be debounced.
To turn an effect into a debounce-able one you must provide an identifier, which is used to
determine which in-flight effect should be canceled in order to start a new effect. Any
hashable value can be used for the identifier, such as a string, but you can add a bit of
protection against typos by defining a new type that conforms to Hashable
, such as an empty
struct:
case let .textChanged(text):
struct SearchId: Hashable {}
return environment.search(text)
.debounce(id: SearchId(), for: 0.5, scheduler: environment.mainQueue)
.map(Action.searchResponse)
Parameters
Name | Type | Description |
---|---|---|
id | AnyHashable |
The effect's identifier. |
dueTime | S.SchedulerTimeType.Stride |
The duration you want to debounce for. |
scheduler | S |
The scheduler you want to deliver the debounced output to. |
options | S.SchedulerOptions? |
Scheduler options that customize the effect's delivery of elements. |
Returns
An effect that publishes events only after a specified time elapses.
deferred(for:scheduler:options:)
public func deferred<S: Scheduler>(
for dueTime: S.SchedulerTimeType.Stride,
scheduler: S,
options: S.SchedulerOptions? = nil
) -> Effect
Returns an effect that will be executed after given dueTime
.
case let .textChanged(text):
return environment.search(text)
.deferred(for: 0.5, scheduler: environment.mainQueue)
.map(Action.searchResponse)
Parameters
Name | Type | Description |
---|---|---|
upstream | the effect you want to defer. |
|
dueTime | S.SchedulerTimeType.Stride |
The duration you want to defer for. |
scheduler | S |
The scheduler you want to deliver the defer output to. |
options | S.SchedulerOptions? |
Scheduler options that customize the effect's delivery of elements. |
Returns
An effect that will be executed after dueTime
throttle(id:for:scheduler:latest:)
public func throttle<S>(
id: AnyHashable,
for interval: S.SchedulerTimeType.Stride,
scheduler: S,
latest: Bool
) -> Effect where S: Scheduler
Throttles an effect so that it only publishes one output per given interval.
Parameters
Name | Type | Description |
---|---|---|
id | AnyHashable |
The effect's identifier. |
interval | S.SchedulerTimeType.Stride |
The interval at which to find and emit the most recent element, expressed in the time system of the scheduler. |
scheduler | S |
The scheduler you want to deliver the throttled output to. |
latest | Bool |
A boolean value that indicates whether to publish the most recent element. If |
Returns
An effect that emits either the most-recent or first element received during the specified interval.
failing(_:)
public static func failing(_ prefix: String) -> Self
An effect that causes a test to fail if it runs.
This effect can provide an additional layer of certainty that a tested code path does not execute a particular effect.
For example, let's say we have a very simple counter application, where a user can increment and decrement a number. The state and actions are simple enough:
struct CounterState: Equatable {
var count = 0
}
enum CounterAction: Equatable {
case decrementButtonTapped
case incrementButtonTapped
}
Let's throw in a side effect. If the user attempts to decrement the counter below zero, the application should refuse and play an alert sound instead.
We can model playing a sound in the environment with an effect:
struct CounterEnvironment {
let playAlertSound: () -> Effect<Never, Never>
}
Now that we've defined the domain, we can describe the logic in a reducer:
let counterReducer = Reducer<
CounterState, CounterAction, CounterEnvironment
> { state, action, environment in
switch action {
case .decrementButtonTapped:
if state > 0 {
state.count -= 0
return .none
} else {
return environment.playAlertSound()
.fireAndForget()
}
case .incrementButtonTapped:
state.count += 1
return .non
}
}
Let's say we want to write a test for the increment path. We can see in the reducer that it should never play an alert, so we can configure the environment with an effect that will fail if it ever executes:
func testIncrement() {
let store = TestStore(
initialState: CounterState(count: 0)
reducer: counterReducer,
environment: CounterEnvironment(
playSound: .failing("playSound")
)
)
store.send(.increment) {
$0.count = 1
}
}
By using a .failing
effect in our environment we have strengthened the assertion and made
the test easier to understand at the same time. We can see, without consulting the reducer
itself, that this particular action should not access this effect.
Parameters
Name | Type | Description |
---|---|---|
prefix | String |
A string that identifies this scheduler and will prefix all failure messages. |
Returns
An effect that causes a test to fail if it runs.