Effect
public struct Effect<Output, Failure> : Publisher where Failure : Error
extension Effect: CustomDebugOutputConvertible
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.
-
Undocumented
Declaration
Swift
public let upstream: AnyPublisher<Output, 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 thePublisher
protocol:NotificationCenter.default .publisher(for: UIApplication.userDidTakeScreenshotNotification) .eraseToEffect()
Declaration
Swift
public init<P>(_ publisher: P) where Output == P.Output, Failure == P.Failure, P : Publisher
Parameters
publisher
A publisher.
-
Declaration
Swift
public func receive<S>( subscriber: S ) where S: Combine.Subscriber, Failure == S.Failure, Output == S.Input
-
Initializes an effect that immediately emits the value passed in.
Declaration
Swift
public init(value: Output)
Parameters
value
The value that is immediately emitted by the effect.
-
Initializes an effect that immediately fails with the error passed in.
Declaration
Swift
public init(error: Failure)
Parameters
error
The error that is immediately emitted by the effect.
-
An effect that does nothing and completes immediately. Useful for situations where you must return an effect, but you don’t need to do anything.
Declaration
Swift
public static var none: Effect { get }
-
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 aSubscriber
value.Declaration
Swift
public static func future( _ attemptToFulfill: @escaping (@escaping (Result<Output, Failure>) -> Void) -> Void ) -> Effect
Parameters
attemptToFulfill
A closure that takes a
callback
as an argument which can be used to feed itResult<Output, Failure>
values. -
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 }
Declaration
Swift
public static func result(_ attemptToFulfill: @escaping () -> Result<Output, Failure>) -> Effect<Output, Failure>
Parameters
attemptToFulfill
A closure encapsulating some work to execute in the real world.
Return Value
An effect.
-
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 isnotDetermined
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. } }
Declaration
Swift
public static func run( _ work: @escaping (Effect.Subscriber) -> Cancellable ) -> Self
Parameters
work
A closure that accepts a
Subscriber
value and returns a cancellable. When theEffect
is completed, the cancellable will be used to clean up any resources created when the effect was started. -
Concatenates a variadic list of effects together into a single effect, which runs the effects one after the other.
Warning
Combine’sPublishers.Concatenate
operator, which this function uses, can leak when its suffix is aPublishers.MergeMany
operator, which is used throughout the Composable Architecture in functions likeReducer.combine
.Feedback filed: https://gist.github.com/mbrandonw/611c8352e1bd1c22461bd505e320ab58
Declaration
Swift
public static func concatenate(_ effects: Effect...) -> Effect
Parameters
effects
A variadic list of effects.
Return Value
A new effect
-
Concatenates a collection of effects together into a single effect, which runs the effects one after the other.
Warning
Combine’sPublishers.Concatenate
operator, which this function uses, can leak when its suffix is aPublishers.MergeMany
operator, which is used throughout the Composable Architecture in functions likeReducer.combine
.Feedback filed: https://gist.github.com/mbrandonw/611c8352e1bd1c22461bd505e320ab58
Declaration
Swift
public static func concatenate<C: Collection>( _ effects: C ) -> Effect where C.Element == Effect
Parameters
effects
A collection of effects.
Return Value
A new effect
-
Merges a variadic list of effects together into a single effect, which runs the effects at the same time.
Declaration
Swift
public static func merge( _ effects: Effect... ) -> Effect
Parameters
effects
A list of effects.
Return Value
A new effect
-
Merges a sequence of effects together into a single effect, which runs the effects at the same time.
Declaration
Swift
public static func merge<S>(_ effects: S) -> Effect where S : Sequence, S.Element == Effect<Output, Failure>
Parameters
effects
A sequence of effects.
Return Value
A new effect
-
Creates an effect that executes some work in the real world that doesn’t need to feed data back into the store.
Declaration
Swift
public static func fireAndForget(_ work: @escaping () -> Void) -> Effect
Parameters
work
A closure encapsulating some work to execute in the real world.
Return Value
An effect.
-
Transforms all elements from the upstream effect with a provided closure.
Declaration
Swift
public func map<T>(_ transform: @escaping (Output) -> T) -> Effect<T, Failure>
Parameters
transform
A closure that transforms the upstream effect’s output to a new output.
Return Value
A publisher that uses the provided closure to map elements from the upstream effect to new elements that it then publishes.
-
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 toHashable
, 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())
Declaration
Swift
public func cancellable(id: AnyHashable, cancelInFlight: Bool = false) -> Effect
Parameters
id
The effect’s identifier.
cancelInFlight
Determines if any in-flight effect with the same identifier should be canceled before starting this new one.
Return Value
A new effect that is capable of being canceled by an identifier.
-
An effect that will cancel any currently in-flight effect with the given identifier.
Declaration
Swift
public static func cancel(id: AnyHashable) -> Effect
Parameters
id
An effect identifier.
Return Value
A new effect that will cancel any currently in-flight effect with the given identifier.
-
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) .map(Action.searchResponse) .debounce(id: SearchId(), for: 0.5, scheduler: environment.mainQueue)
Declaration
Swift
public func debounce<S: Scheduler>( id: AnyHashable, for dueTime: S.SchedulerTimeType.Stride, scheduler: S, options: S.SchedulerOptions? = nil ) -> Effect
Parameters
id
The effect’s identifier.
dueTime
The duration you want to debounce for.
scheduler
The scheduler you want to deliver the debounced output to.
options
Scheduler options that customize the effect’s delivery of elements.
Return Value
An effect that publishes events only after a specified time elapses.
-
Undocumented
See moreDeclaration
Swift
public struct Subscriber
-
Undocumented
Declaration
Swift
public var debugOutput: String { get }
-
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>.catching { let fileUrl = URL( fileURLWithPath: NSSearchPathForDirectoriesInDomains( .documentDirectory, .userDomainMask, true )[0] ) .appendingPathComponent("user.json") let data = try Data(contentsOf: fileUrl) return try JSONDecoder().decode(User.self, from: $0) }
Declaration
Swift
public static func catching(_ work: @escaping () throws -> Output) -> Effect<Output, Failure>
Parameters
work
A closure encapsulating some work to execute in the real world.
Return Value
An effect.
-
Upcasts an
Effect<Never, Failure>
to anEffect<T, Failure>
for any typeT
. This is possible to do because anEffect<Never, Failure>
can never produce any values to feed back into the store (hence the name “fire and forget”), and therefore we can act like it’s an effect that produces values of any type (since it never produces values).This is useful for times you have an
Effect<Never, Failure>
but need to massage it into another type in order to return it from a reducer:case .buttonTapped: return analyticsClient.track("Button Tapped") .fireAndForget()
Declaration
Swift
public func fireAndForget<T>() -> Effect<T, Failure>
Return Value
An effect.
-
Returns an effect that repeatedly emits the current time of the given scheduler on the given interval.
While it is possible to use Foundation’s
Timer.publish(every:tolerance:on:in:options:)
API to create a timer in the Composable Architecture, it is not advisable. This API only allows creating a timer on a run loop, which means when writing tests you will need to explicitly wait for time to pass in order to see how the effect evolves in your feature.In the Composable Architecture we test time-based effects like this by using the
TestScheduler
, which allows us to explicitly and immediately advance time forward so that we can see how effects emit. However, becauseTimer.publish
takes a concreteRunLoop
as its scheduler, we can’t substitute in aTestScheduler
during tests`.That is why we provide the
Effect.timer
effect. It allows you to create a timer that works with any scheduler, not just a run loop, which means you can use aDispatchQueue
orRunLoop
when running your live app, but use aTestScheduler
in tests.To start and stop a timer in your feature you can create the timer effect from an action and then use the
.cancel(id:)
effect to stop the timer:struct AppState { var count = 0 } enum AppAction { case startButtonTapped, stopButtonTapped, timerTicked } struct AppEnvironment { var mainQueue: AnySchedulerOf<DispatchQueue> } let appReducer = Reducer<AppState, AppAction, AppEnvironment> { state, action, env in struct TimerId: Hashable {} switch action { case .startButtonTapped: return Effect.timer(id: TimerId(), every: 1, on: env.mainQueue) .map { _ in .timerTicked } case .stopButtonTapped: return .cancel(id: TimerId()) case let .timerTicked: state.count += 1 return .none }
Then to test the timer in this feature you can use a test scheduler to advance time:
func testTimer() { let scheduler = DispatchQueue.testScheduler
let store = TestStore( initialState: .init(), reducer: appReducer, envirnoment: .init( mainQueue: scheduler.eraseToAnyScheduler() ) ) store.assert( .send(.startButtonTapped), .do { scheduler.advance(by: .seconds(1)) }, .receive(.timerTicked) { $0.count = 1 }, .do { scheduler.advance(by: .seconds(5)) }, .receive(.timerTicked) { $0.count = 2 }, .receive(.timerTicked) { $0.count = 3 }, .receive(.timerTicked) { $0.count = 4 }, .receive(.timerTicked) { $0.count = 5 }, .receive(.timerTicked) { $0.count = 6 }, .send(.stopButtonTapped) )
}
Note
This effect is only meant to be used with features built in the Composable Architecture, and returned from a reducer. If you want a testable alternative to Foundation’s
Timer.publish
you can use the publisherPublishers.Timer
that is included in this library via theCombineSchedulers
module.Declaration
Swift
public static func timer<S>( id: AnyHashable, every interval: S.SchedulerTimeType.Stride, tolerance: S.SchedulerTimeType.Stride? = nil, on scheduler: S, options: S.SchedulerOptions? = nil ) -> Effect where S: Scheduler, S.SchedulerTimeType == Output
Parameters
interval
The time interval on which to publish events. For example, a value of
0.5
publishes an event approximately every half-second.scheduler
The scheduler on which the timer runs.
tolerance
The allowed timing variance when emitting events. Defaults to
nil
, which allows any variance.options
Scheduler options passed to the timer. Defaults to
nil
.