Support DocC Xcode 13 (#591)

* wip

* finish

* revert back code snippet identation to 5

* Update Sources/ComposableArchitecture/Effect.swift

Co-authored-by: Brandon Williams <135203+mbrandonw@users.noreply.github.com>

Co-authored-by: Brandon Williams <135203+mbrandonw@users.noreply.github.com>
This commit is contained in:
Wendy Liga
2021-06-15 01:10:08 +07:00
committed by GitHub
parent c99a2f9636
commit 1a8bccc62e
20 changed files with 300 additions and 132 deletions

View File

@@ -1,16 +1,20 @@
import CasePaths import CasePaths
import Dispatch import Dispatch
/// Determines how the string description of an action should be printed when using the `.debug()` /// Determines how the string description of an action should be printed when using the ``Reducer/debug(prefix:state:action:environment:)``
/// higher-order reducer. /// higher-order reducer.
public enum ActionFormat { public enum ActionFormat {
/// Prints the action in a single line by only specifying the labels of the associated values: /// Prints the action in a single line by only specifying the labels of the associated values:
/// ///
/// ```swift
/// Action.screenA(.row(index:, action: .textChanged(query:))) /// Action.screenA(.row(index:, action: .textChanged(query:)))
/// ```
///
case labelsOnly case labelsOnly
/// Prints the action in a multiline, pretty-printed format, including all the labels of /// Prints the action in a multiline, pretty-printed format, including all the labels of
/// any associated values, as well as the data held in the associated values: /// any associated values, as well as the data held in the associated values:
/// ///
/// ```swift
/// Action.screenA( /// Action.screenA(
/// ScreenA.row( /// ScreenA.row(
/// index: 1, /// index: 1,
@@ -19,6 +23,8 @@ public enum ActionFormat {
/// ) /// )
/// ) /// )
/// ) /// )
/// ```
///
case prettyPrint case prettyPrint
} }
@@ -31,7 +37,7 @@ extension Reducer {
/// - prefix: A string with which to prefix all debug messages. /// - prefix: A string with which to prefix all debug messages.
/// - toDebugEnvironment: A function that transforms an environment into a debug environment by /// - toDebugEnvironment: A function that transforms an environment into a debug environment by
/// describing a print function and a queue to print from. Defaults to a function that ignores /// describing a print function and a queue to print from. Defaults to a function that ignores
/// the environment and returns a default `DebugEnvironment` that uses Swift's `print` /// the environment and returns a default ``DebugEnvironment`` that uses Swift's `print`
/// function and a background queue. /// function and a background queue.
/// - Returns: A reducer that prints debug messages for all received actions. /// - Returns: A reducer that prints debug messages for all received actions.
public func debug( public func debug(
@@ -58,7 +64,7 @@ extension Reducer {
/// - prefix: A string with which to prefix all debug messages. /// - prefix: A string with which to prefix all debug messages.
/// - toDebugEnvironment: A function that transforms an environment into a debug environment by /// - toDebugEnvironment: A function that transforms an environment into a debug environment by
/// describing a print function and a queue to print from. Defaults to a function that ignores /// describing a print function and a queue to print from. Defaults to a function that ignores
/// the environment and returns a default `DebugEnvironment` that uses Swift's `print` /// the environment and returns a default ``DebugEnvironment`` that uses Swift's `print`
/// function and a background queue. /// function and a background queue.
/// - Returns: A reducer that prints debug messages for all received actions. /// - Returns: A reducer that prints debug messages for all received actions.
public func debugActions( public func debugActions(
@@ -87,7 +93,7 @@ extension Reducer {
/// - toLocalAction: A case path that filters actions that are printed. /// - toLocalAction: A case path that filters actions that are printed.
/// - toDebugEnvironment: A function that transforms an environment into a debug environment by /// - toDebugEnvironment: A function that transforms an environment into a debug environment by
/// describing a print function and a queue to print from. Defaults to a function that ignores /// describing a print function and a queue to print from. Defaults to a function that ignores
/// the environment and returns a default `DebugEnvironment` that uses Swift's `print` /// the environment and returns a default ``DebugEnvironment`` that uses Swift's `print`
/// function and a background queue. /// function and a background queue.
/// - Returns: A reducer that prints debug messages for all received actions. /// - Returns: A reducer that prints debug messages for all received actions.
public func debug<LocalState, LocalAction>( public func debug<LocalState, LocalAction>(

View File

@@ -1,12 +1,12 @@
import Combine import Combine
import Foundation import Foundation
/// The `Effect` type encapsulates a unit of work that can be run in the outside world, and can feed /// 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, /// 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. /// 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 /// 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 /// 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 same thread, **and** if the store is being used to drive UI then it
/// must receive values on the main thread. /// must receive values on the main thread.
/// ///
@@ -20,17 +20,21 @@ public struct Effect<Output, Failure: Error>: Publisher {
/// ///
/// This initializer is useful for turning any publisher into an effect. For example: /// This initializer is useful for turning any publisher into an effect. For example:
/// ///
/// ```swift
/// Effect( /// Effect(
/// NotificationCenter.default /// NotificationCenter.default
/// .publisher(for: UIApplication.userDidTakeScreenshotNotification) /// .publisher(for: UIApplication.userDidTakeScreenshotNotification)
/// ) /// )
/// ```
/// ///
/// Alternatively, you can use the `.eraseToEffect()` method that is defined on the `Publisher` /// Alternatively, you can use the `.eraseToEffect()` method that is defined on the `Publisher`
/// protocol: /// protocol:
/// ///
/// ```swift
/// NotificationCenter.default /// NotificationCenter.default
/// .publisher(for: UIApplication.userDidTakeScreenshotNotification) /// .publisher(for: UIApplication.userDidTakeScreenshotNotification)
/// .eraseToEffect() /// .eraseToEffect()
/// ```
/// ///
/// - Parameter publisher: A publisher. /// - Parameter publisher: A publisher.
public init<P: Publisher>(_ publisher: P) where P.Output == Output, P.Failure == Failure { public init<P: Publisher>(_ publisher: P) where P.Output == Output, P.Failure == Failure {
@@ -74,28 +78,32 @@ public struct Effect<Output, Failure: Error>: Publisher {
/// Creates an effect that can supply a single value asynchronously in the future. /// 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 /// This can be helpful for converting APIs that are callback-based into ones that deal with
/// `Effect`s. /// ``Effect``s.
/// ///
/// For example, to create an effect that delivers an integer after waiting a second: /// For example, to create an effect that delivers an integer after waiting a second:
/// ///
/// ```swift
/// Effect<Int, Never>.future { callback in /// Effect<Int, Never>.future { callback in
/// DispatchQueue.main.asyncAfter(deadline: .now() + 1) { /// DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
/// callback(.success(42)) /// callback(.success(42))
/// } /// }
/// } /// }
/// ```
/// ///
/// Note that you can only deliver a single value to the `callback`. If you send more they will be /// Note that you can only deliver a single value to the `callback`. If you send more they will be
/// discarded: /// discarded:
/// ///
/// ```swift
/// Effect<Int, Never>.future { callback in /// Effect<Int, Never>.future { callback in
/// DispatchQueue.main.asyncAfter(deadline: .now() + 1) { /// DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
/// callback(.success(42)) /// callback(.success(42))
/// callback(.success(1729)) // Will not be emitted by the effect /// 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` /// If you need to deliver more than one value to the effect, you should use the ``Effect``
/// initializer that accepts a `Subscriber` value. /// initializer that accepts a ``Subscriber`` value.
/// ///
/// - Parameter attemptToFulfill: A closure that takes a `callback` as an argument which can be /// - Parameter attemptToFulfill: A closure that takes a `callback` as an argument which can be
/// used to feed it `Result<Output, Failure>` values. /// used to feed it `Result<Output, Failure>` values.
@@ -115,6 +123,7 @@ public struct Effect<Output, Failure: Error>: Publisher {
/// ///
/// For example, to load a user from some JSON on the disk, one can wrap that work in an effect: /// For example, to load a user from some JSON on the disk, one can wrap that work in an effect:
/// ///
/// ```swift
/// Effect<User, Error>.result { /// Effect<User, Error>.result {
/// let fileUrl = URL( /// let fileUrl = URL(
/// fileURLWithPath: NSSearchPathForDirectoriesInDomains( /// fileURLWithPath: NSSearchPathForDirectoriesInDomains(
@@ -130,6 +139,7 @@ public struct Effect<Output, Failure: Error>: Publisher {
/// ///
/// return result /// return result
/// } /// }
/// ```
/// ///
/// - Parameter attemptToFulfill: A closure encapsulating some work to execute in the real world. /// - Parameter attemptToFulfill: A closure encapsulating some work to execute in the real world.
/// - Returns: An effect. /// - Returns: An effect.
@@ -141,13 +151,14 @@ public struct Effect<Output, Failure: Error>: Publisher {
/// a completion. /// a completion.
/// ///
/// This initializer is useful for bridging callback APIs, delegate APIs, and manager APIs to the /// 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`` 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. /// 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 /// 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 /// 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: /// can request authorization, and once a status is received it can send that back to the effect:
/// ///
/// ```swift
/// Effect.run { subscriber in /// Effect.run { subscriber in
/// subscriber.send(MPMediaLibrary.authorizationStatus()) /// subscriber.send(MPMediaLibrary.authorizationStatus())
/// ///
@@ -165,9 +176,10 @@ public struct Effect<Output, Failure: Error>: Publisher {
/// // have any. /// // have any.
/// } /// }
/// } /// }
/// ```
/// ///
/// - Parameter work: A closure that accepts a `Subscriber` value and returns a cancellable. When /// - Parameter work: A closure that accepts a ``Subscriber`` value and returns a cancellable. When
/// the `Effect` is completed, the cancellable will be used to clean up any resources created /// the ``Effect`` is completed, the cancellable will be used to clean up any resources created
/// when the effect was started. /// when the effect was started.
public static func run( public static func run(
_ work: @escaping (Effect.Subscriber) -> Cancellable _ work: @escaping (Effect.Subscriber) -> Cancellable
@@ -180,7 +192,7 @@ public struct Effect<Output, Failure: Error>: Publisher {
/// ///
/// - Warning: Combine's `Publishers.Concatenate` operator, which this function uses, can leak /// - Warning: Combine's `Publishers.Concatenate` operator, which this function uses, can leak
/// when its suffix is a `Publishers.MergeMany` operator, which is used throughout the /// when its suffix is a `Publishers.MergeMany` operator, which is used throughout the
/// Composable Architecture in functions like `Reducer.combine`. /// Composable Architecture in functions like ``Reducer/combine(_:)-1ern2``.
/// ///
/// Feedback filed: <https://gist.github.com/mbrandonw/611c8352e1bd1c22461bd505e320ab58> /// Feedback filed: <https://gist.github.com/mbrandonw/611c8352e1bd1c22461bd505e320ab58>
/// ///
@@ -195,7 +207,7 @@ public struct Effect<Output, Failure: Error>: Publisher {
/// ///
/// - Warning: Combine's `Publishers.Concatenate` operator, which this function uses, can leak /// - Warning: Combine's `Publishers.Concatenate` operator, which this function uses, can leak
/// when its suffix is a `Publishers.MergeMany` operator, which is used throughout the /// when its suffix is a `Publishers.MergeMany` operator, which is used throughout the
/// Composable Architecture in functions like `Reducer.combine`. /// Composable Architecture in functions like `Reducer/combine(_:)-1ern2``.
/// ///
/// Feedback filed: <https://gist.github.com/mbrandonw/611c8352e1bd1c22461bd505e320ab58> /// Feedback filed: <https://gist.github.com/mbrandonw/611c8352e1bd1c22461bd505e320ab58>
/// ///
@@ -269,6 +281,7 @@ extension Effect where Failure == Swift.Error {
/// ///
/// For example, to load a user from some JSON on the disk, one can wrap that work in an effect: /// For example, to load a user from some JSON on the disk, one can wrap that work in an effect:
/// ///
/// ```swift
/// Effect<User, Error>.catching { /// Effect<User, Error>.catching {
/// let fileUrl = URL( /// let fileUrl = URL(
/// fileURLWithPath: NSSearchPathForDirectoriesInDomains( /// fileURLWithPath: NSSearchPathForDirectoriesInDomains(
@@ -280,6 +293,7 @@ extension Effect where Failure == Swift.Error {
/// let data = try Data(contentsOf: fileUrl) /// let data = try Data(contentsOf: fileUrl)
/// return try JSONDecoder().decode(User.self, from: $0) /// return try JSONDecoder().decode(User.self, from: $0)
/// } /// }
/// ```
/// ///
/// - Parameter work: A closure encapsulating some work to execute in the real world. /// - Parameter work: A closure encapsulating some work to execute in the real world.
/// - Returns: An effect. /// - Returns: An effect.
@@ -289,31 +303,35 @@ extension Effect where Failure == Swift.Error {
} }
extension Publisher { extension Publisher {
/// Turns any publisher into an `Effect`. /// Turns any publisher into an ``Effect``.
/// ///
/// This can be useful for when you perform a chain of publisher transformations in a reducer, and /// This can be useful for when you perform a chain of publisher transformations in a reducer, and
/// you need to convert that publisher to an effect so that you can return it from the reducer: /// you need to convert that publisher to an effect so that you can return it from the reducer:
/// ///
/// ```swift
/// case .buttonTapped: /// case .buttonTapped:
/// return fetchUser(id: 1) /// return fetchUser(id: 1)
/// .filter(\.isAdmin) /// .filter(\.isAdmin)
/// .eraseToEffect() /// .eraseToEffect()
/// ```
/// ///
/// - Returns: An effect that wraps `self`. /// - Returns: An effect that wraps `self`.
public func eraseToEffect() -> Effect<Output, Failure> { public func eraseToEffect() -> Effect<Output, Failure> {
Effect(self) Effect(self)
} }
/// Turns any publisher into an `Effect` that cannot fail by wrapping its output and failure in a /// Turns any publisher into an ``Effect`` that cannot fail by wrapping its output and failure in a
/// result. /// result.
/// ///
/// This can be useful when you are working with a failing API but want to deliver its data to an /// This can be useful when you are working with a failing API but want to deliver its data to an
/// action that handles both success and failure. /// action that handles both success and failure.
/// ///
/// ```swift
/// case .buttonTapped: /// case .buttonTapped:
/// return fetchUser(id: 1) /// return fetchUser(id: 1)
/// .catchToEffect() /// .catchToEffect()
/// .map(ProfileAction.userResponse) /// .map(ProfileAction.userResponse)
/// ```
/// ///
/// - Returns: An effect that wraps `self`. /// - Returns: An effect that wraps `self`.
public func catchToEffect() -> Effect<Result<Output, Failure>, Never> { public func catchToEffect() -> Effect<Result<Output, Failure>, Never> {
@@ -322,16 +340,18 @@ extension Publisher {
.eraseToEffect() .eraseToEffect()
} }
/// Turns any publisher into an `Effect` for any output and failure type by ignoring all output /// Turns any publisher into an ``Effect`` for any output and failure type by ignoring all output
/// and any failure. /// and any failure.
/// ///
/// This is useful for times you want to fire off an effect but don't want to feed any data back /// This is useful for times you want to fire off an effect but don't want to feed any data back
/// into the system. It can automatically promote an effect to your reducer's domain. /// into the system. It can automatically promote an effect to your reducer's domain.
/// ///
/// ```swift
/// case .buttonTapped: /// case .buttonTapped:
/// return analyticsClient.track("Button Tapped") /// return analyticsClient.track("Button Tapped")
/// .fireAndForget() /// .fireAndForget()
/// /// ```
///
/// - Parameters: /// - Parameters:
/// - outputType: An output type. /// - outputType: An output type.
/// - failureType: A failure type. /// - failureType: A failure type.

View File

@@ -5,10 +5,11 @@ extension Effect {
/// Turns an effect into one that is capable of being canceled. /// 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 /// 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 /// ``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 /// 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: /// against typos by defining a new type that conforms to `Hashable`, such as an empty struct:
/// ///
/// ```swift
/// struct LoadUserId: Hashable {} /// struct LoadUserId: Hashable {}
/// ///
/// case .reloadButtonTapped: /// case .reloadButtonTapped:
@@ -20,6 +21,7 @@ extension Effect {
/// case .cancelButtonTapped: /// case .cancelButtonTapped:
/// // Cancel any in-flight requests to load the user /// // Cancel any in-flight requests to load the user
/// return .cancel(id: LoadUserId()) /// return .cancel(id: LoadUserId())
/// ```
/// ///
/// - Parameters: /// - Parameters:
/// - id: The effect's identifier. /// - id: The effect's identifier.

View File

@@ -9,13 +9,15 @@ extension Effect {
/// protection against typos by defining a new type that conforms to `Hashable`, such as an empty /// protection against typos by defining a new type that conforms to `Hashable`, such as an empty
/// struct: /// struct:
/// ///
/// ```swift
/// case let .textChanged(text): /// case let .textChanged(text):
/// struct SearchId: Hashable {} /// struct SearchId: Hashable {}
/// ///
/// return environment.search(text) /// return environment.search(text)
/// .map(Action.searchResponse) /// .map(Action.searchResponse)
/// .debounce(id: SearchId(), for: 0.5, scheduler: environment.mainQueue) /// .debounce(id: SearchId(), for: 0.5, scheduler: environment.mainQueue)
/// /// ```
///
/// - Parameters: /// - Parameters:
/// - id: The effect's identifier. /// - id: The effect's identifier.
/// - dueTime: The duration you want to debounce for. /// - dueTime: The duration you want to debounce for.

View File

@@ -15,13 +15,14 @@ extension Effect where Failure == Never {
/// we can see how effects emit. However, because `Timer.publish` takes a concrete `RunLoop` as /// we can see how effects emit. However, because `Timer.publish` takes a concrete `RunLoop` as
/// its scheduler, we can't substitute in a `TestScheduler` during tests`. /// its scheduler, we can't substitute in a `TestScheduler` during tests`.
/// ///
/// That is why we provide the `Effect.timer` effect. It allows you to create a timer that works /// That is why we provide the ``Effect/timer(id:every:tolerance:on:options:)`` effect. It allows you to create a timer that works
/// with any scheduler, not just a run loop, which means you can use a `DispatchQueue` or /// with any scheduler, not just a run loop, which means you can use a `DispatchQueue` or
/// `RunLoop` when running your live app, but use a `TestScheduler` in tests. /// `RunLoop` when running your live app, but use a `TestScheduler` in tests.
/// ///
/// To start and stop a timer in your feature you can create the timer effect from an action /// 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: /// and then use the ``Effect/cancel(id:)`` effect to stop the timer:
/// ///
/// ```swift
/// struct AppState { /// struct AppState {
/// var count = 0 /// var count = 0
/// } /// }
@@ -49,34 +50,37 @@ extension Effect where Failure == Never {
/// state.count += 1 /// state.count += 1
/// return .none /// return .none
/// } /// }
/// ```
/// ///
/// Then to test the timer in this feature you can use a test scheduler to advance time: /// Then to test the timer in this feature you can use a test scheduler to advance time:
/// ///
/// func testTimer() { /// ```swift
/// let scheduler = DispatchQueue.test /// func testTimer() {
/// let scheduler = DispatchQueue.test
/// ///
/// let store = TestStore( /// let store = TestStore(
/// initialState: .init(), /// initialState: .init(),
/// reducer: appReducer, /// reducer: appReducer,
/// envirnoment: .init( /// envirnoment: .init(
/// mainQueue: scheduler.eraseToAnyScheduler() /// mainQueue: scheduler.eraseToAnyScheduler()
/// )
/// ) /// )
/// )
/// ///
/// store.send(.startButtonTapped) /// store.send(.startButtonTapped)
/// ///
/// scheduler.advance(by: .seconds(1)) /// scheduler.advance(by: .seconds(1))
/// store.receive(.timerTicked) { $0.count = 1 } /// store.receive(.timerTicked) { $0.count = 1 }
/// ///
/// scheduler.advance(by: .seconds(5)) /// scheduler.advance(by: .seconds(5))
/// store.receive(.timerTicked) { $0.count = 2 } /// store.receive(.timerTicked) { $0.count = 2 }
/// store.receive(.timerTicked) { $0.count = 3 } /// store.receive(.timerTicked) { $0.count = 3 }
/// store.receive(.timerTicked) { $0.count = 4 } /// store.receive(.timerTicked) { $0.count = 4 }
/// store.receive(.timerTicked) { $0.count = 5 } /// store.receive(.timerTicked) { $0.count = 5 }
/// store.receive(.timerTicked) { $0.count = 6 } /// store.receive(.timerTicked) { $0.count = 6 }
/// ///
/// store.send(.stopButtonTapped) /// store.send(.stopButtonTapped)
/// } /// }
/// ```
/// ///
/// - Note: This effect is only meant to be used with features built in the Composable /// - 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 /// Architecture, and returned from a reducer. If you want a testable alternative to

View File

@@ -2,19 +2,19 @@ import CasePaths
import Combine import Combine
/// A reducer describes how to evolve the current state of an application to the next state, given /// A reducer describes how to evolve the current state of an application to the next state, given
/// an action, and describes what `Effect`s should be executed later by the store, if any. /// an action, and describes what ``Effect``s should be executed later by the store, if any.
/// ///
/// Reducers have 3 generics: /// Reducers have 3 generics:
/// ///
/// * `State`: A type that holds the current state of the application. /// * `State`: A type that holds the current state of the application.
/// * `Action`: A type that holds all possible actions that cause the state of the application to /// * `Action`: A type that holds all possible actions that cause the state of the application to
/// change. /// change.
/// * `Environment`: A type that holds all dependencies needed in order to produce `Effect`s, such /// * `Environment`: A type that holds all dependencies needed in order to produce ``Effect``s, such
/// as API clients, analytics clients, random number generators, etc. /// as API clients, analytics clients, random number generators, etc.
/// ///
/// - Note: The thread on which effects output is important. An effect's output is immediately sent /// - Note: The thread on which effects output is important. An effect's output is immediately sent
/// back into the store, and `Store` is not thread safe. This means all effects must receive /// back into the store, and ``Store`` is not thread safe. This means all effects must receive
/// values on the same thread, **and** if the `Store` is being used to drive UI then all output /// values on the same thread, **and** if the ``Store`` is being used to drive UI then all output
/// must be on the main thread. You can use the `Publisher` method `receive(on:)` for make the /// must be on the main thread. You can use the `Publisher` method `receive(on:)` for make the
/// effect output its values on the thread of your choice. /// effect output its values on the thread of your choice.
public struct Reducer<State, Action, Environment> { public struct Reducer<State, Action, Environment> {
@@ -25,10 +25,10 @@ public struct Reducer<State, Action, Environment> {
/// The reducer takes three arguments: state, action and environment. The state is `inout` so that /// The reducer takes three arguments: state, action and environment. The state is `inout` so that
/// you can make any changes to it directly inline. The reducer must return an effect, which /// you can make any changes to it directly inline. The reducer must return an effect, which
/// typically would be constructed by using the dependencies inside the `environment` value. If /// typically would be constructed by using the dependencies inside the `environment` value. If
/// no effect needs to be executed, a `.none` effect can be returned. /// no effect needs to be executed, a ``Effect/none`` effect can be returned.
/// ///
/// For example: /// For example:
/// /// ```swift
/// struct MyState { var count = 0, text = "" } /// struct MyState { var count = 0, text = "" }
/// enum MyAction { case buttonTapped, textChanged(String) } /// enum MyAction { case buttonTapped, textChanged(String) }
/// struct MyEnvironment { var analyticsClient: AnalyticsClient } /// struct MyEnvironment { var analyticsClient: AnalyticsClient }
@@ -44,6 +44,7 @@ public struct Reducer<State, Action, Environment> {
/// return .none /// return .none
/// } /// }
/// } /// }
/// ```
/// ///
/// - Parameter reducer: A function signature that takes state, action and /// - Parameter reducer: A function signature that takes state, action and
/// environment. /// environment.
@@ -67,21 +68,22 @@ public struct Reducer<State, Action, Environment> {
/// its state, it can make a difference if `reducerA` chooses to modify `reducerB`'s state /// its state, it can make a difference if `reducerA` chooses to modify `reducerB`'s state
/// _before_ or _after_ `reducerB` runs. /// _before_ or _after_ `reducerB` runs.
/// ///
/// This is perhaps most easily seen when working with `optional` reducers, where the parent /// This is perhaps most easily seen when working with ``Reducer/optional`` reducers, where the parent
/// domain may listen to the child domain and `nil` out its state. If the parent reducer runs /// domain may listen to the child domain and `nil` out its state. If the parent reducer runs
/// before the child reducer, then the child reducer will not be able to react to its own action. /// before the child reducer, then the child reducer will not be able to react to its own action.
/// ///
/// Similar can be said for a `forEach` reducer. If the parent domain modifies the child /// Similar can be said for a ``Reducer/forEach(state:action:environment:breakpointOnNil:_:_:)-3ic87`` reducer. If the parent domain modifies the child
/// collection by moving, removing, or modifying an element before the `forEach` reducer runs, the /// collection by moving, removing, or modifying an element before the ``Reducer/forEach(state:action:environment:breakpointOnNil:_:_:)-3ic87`` reducer runs, the
/// `forEach` reducer may perform its action against the wrong element, an element that no longer /// ``Reducer/forEach(state:action:environment:breakpointOnNil:_:_:)-3ic87`` reducer may perform its action against the wrong element, an element that no longer
/// exists, or an element in an unexpected state. /// exists, or an element in an unexpected state.
/// ///
/// Running a parent reducer before a child reducer can be considered an application logic /// Running a parent reducer before a child reducer can be considered an application logic
/// error, and can produce assertion failures. So you should almost always combine reducers in /// error, and can produce assertion failures. So you should almost always combine reducers in
/// order from child to parent domain. /// order from child to parent domain.
/// ///
/// Here is an example of how you should combine an `optional` reducer with a parent domain: /// Here is an example of how you should combine an ``Reducer/optional`` reducer with a parent domain:
/// ///
/// ```swift
/// let parentReducer = Reducer<ParentState, ParentAction, ParentEnvironment>.combine( /// let parentReducer = Reducer<ParentState, ParentAction, ParentEnvironment>.combine(
/// // Combined before parent so that it can react to `.dismiss` while state is non-`nil`. /// // Combined before parent so that it can react to `.dismiss` while state is non-`nil`.
/// childReducer.optional().pullback( /// childReducer.optional().pullback(
@@ -99,6 +101,7 @@ public struct Reducer<State, Action, Environment> {
/// } /// }
/// }, /// },
/// ) /// )
/// ```
/// ///
/// - Parameter reducers: A list of reducers. /// - Parameter reducers: A list of reducers.
/// - Returns: A single reducer. /// - Returns: A single reducer.
@@ -117,21 +120,22 @@ public struct Reducer<State, Action, Environment> {
/// its state, it can make a difference if `reducerA` chooses to modify `reducerB`'s state /// its state, it can make a difference if `reducerA` chooses to modify `reducerB`'s state
/// _before_ or _after_ `reducerB` runs. /// _before_ or _after_ `reducerB` runs.
/// ///
/// This is perhaps most easily seen when working with `optional` reducers, where the parent /// This is perhaps most easily seen when working with ``Reducer/optional`` reducers, where the parent
/// domain may listen to the child domain and `nil` out its state. If the parent reducer runs /// domain may listen to the child domain and `nil` out its state. If the parent reducer runs
/// before the child reducer, then the child reducer will not be able to react to its own action. /// before the child reducer, then the child reducer will not be able to react to its own action.
/// ///
/// Similar can be said for a `forEach` reducer. If the parent domain modifies the child /// Similar can be said for a ``Reducer/forEach(state:action:environment:breakpointOnNil:_:_:)-3ic87`` reducer. If the parent domain modifies the child
/// collection by moving, removing, or modifying an element before the `forEach` reducer runs, the /// collection by moving, removing, or modifying an element before the ``Reducer/forEach(state:action:environment:breakpointOnNil:_:_:)-3ic87`` reducer runs, the
/// `forEach` reducer may perform its action against the wrong element, an element that no longer /// ``Reducer/forEach(state:action:environment:breakpointOnNil:_:_:)-3ic87`` reducer may perform its action against the wrong element, an element that no longer
/// exists, or an element in an unexpected state. /// exists, or an element in an unexpected state.
/// ///
/// Running a parent reducer before a child reducer can be considered an application logic /// Running a parent reducer before a child reducer can be considered an application logic
/// error, and can produce assertion failures. So you should almost always combine reducers in /// error, and can produce assertion failures. So you should almost always combine reducers in
/// order from child to parent domain. /// order from child to parent domain.
/// ///
/// Here is an example of how you should combine an `optional` reducer with a parent domain: /// Here is an example of how you should combine an ``Reducer/optional`` reducer with a parent domain:
/// ///
/// ```swift
/// let parentReducer = Reducer<ParentState, ParentAction, ParentEnvironment>.combine( /// let parentReducer = Reducer<ParentState, ParentAction, ParentEnvironment>.combine(
/// // Combined before parent so that it can react to `.dismiss` while state is non-`nil`. /// // Combined before parent so that it can react to `.dismiss` while state is non-`nil`.
/// childReducer.optional().pullback( /// childReducer.optional().pullback(
@@ -149,6 +153,7 @@ public struct Reducer<State, Action, Environment> {
/// } /// }
/// }, /// },
/// ) /// )
/// ```
/// ///
/// - Parameter reducers: An array of reducers. /// - Parameter reducers: An array of reducers.
/// - Returns: A single reducer. /// - Returns: A single reducer.
@@ -169,21 +174,22 @@ public struct Reducer<State, Action, Environment> {
/// its state, it can make a difference if `reducerA` chooses to modify `reducerB`'s state /// its state, it can make a difference if `reducerA` chooses to modify `reducerB`'s state
/// _before_ or _after_ `reducerB` runs. /// _before_ or _after_ `reducerB` runs.
/// ///
/// This is perhaps most easily seen when working with `optional` reducers, where the parent /// This is perhaps most easily seen when working with ``Reducer/optional`` reducers, where the parent
/// domain may listen to the child domain and `nil` out its state. If the parent reducer runs /// domain may listen to the child domain and `nil` out its state. If the parent reducer runs
/// before the child reducer, then the child reducer will not be able to react to its own action. /// before the child reducer, then the child reducer will not be able to react to its own action.
/// ///
/// Similar can be said for a `forEach` reducer. If the parent domain modifies the child /// Similar can be said for a ``Reducer/forEach(state:action:environment:breakpointOnNil:_:_:)-3ic87`` reducer. If the parent domain modifies the child
/// collection by moving, removing, or modifying an element before the `forEach` reducer runs, the /// collection by moving, removing, or modifying an element before the ``Reducer/forEach(state:action:environment:breakpointOnNil:_:_:)-3ic87`` reducer runs, the
/// `forEach` reducer may perform its action against the wrong element, an element that no longer /// ``Reducer/forEach(state:action:environment:breakpointOnNil:_:_:)-3ic87`` reducer may perform its action against the wrong element, an element that no longer
/// exists, or an element in an unexpected state. /// exists, or an element in an unexpected state.
/// ///
/// Running a parent reducer before a child reducer can be considered an application logic /// Running a parent reducer before a child reducer can be considered an application logic
/// error, and can produce assertion failures. So you should almost always combine reducers in /// error, and can produce assertion failures. So you should almost always combine reducers in
/// order from child to parent domain. /// order from child to parent domain.
/// ///
/// Here is an example of how you should combine an `optional` reducer with a parent domain: /// Here is an example of how you should combine an ``Reducer/optional`` reducer with a parent domain:
/// ///
/// ```swift
/// let parentReducer: Reducer<ParentState, ParentAction, ParentEnvironment> = /// let parentReducer: Reducer<ParentState, ParentAction, ParentEnvironment> =
/// // Run before parent so that it can react to `.dismiss` while state is non-`nil`. /// // Run before parent so that it can react to `.dismiss` while state is non-`nil`.
/// childReducer /// childReducer
@@ -204,6 +210,7 @@ public struct Reducer<State, Action, Environment> {
/// } /// }
/// } /// }
/// ) /// )
/// ```
/// ///
/// - Parameter other: Another reducer. /// - Parameter other: Another reducer.
/// - Returns: A single reducer. /// - Returns: A single reducer.
@@ -220,9 +227,10 @@ public struct Reducer<State, Action, Environment> {
/// * A function that can transform the global environment into a local environment. /// * A function that can transform the global environment into a local environment.
/// ///
/// This operation is important for breaking down large reducers into small ones. When used with /// This operation is important for breaking down large reducers into small ones. When used with
/// the `combine` operator you can define many reducers that work on small pieces of domain, and /// the ``combine(_:)-1ern2`` operator you can define many reducers that work on small pieces of domain, and
/// then _pull them back_ and _combine_ them into one big reducer that works on a large domain. /// then _pull them back_ and _combine_ them into one big reducer that works on a large domain.
/// ///
/// ```swift
/// // Global domain that holds a local domain: /// // Global domain that holds a local domain:
/// struct AppState { var settings: SettingsState, /* rest of state */ } /// struct AppState { var settings: SettingsState, /* rest of state */ }
/// enum AppAction { case settings(SettingsAction), /* other actions */ } /// enum AppAction { case settings(SettingsAction), /* other actions */ }
@@ -241,6 +249,7 @@ public struct Reducer<State, Action, Environment> {
/// ///
/// /* other reducers */ /// /* other reducers */
/// ) /// )
/// ```
/// ///
/// - Parameters: /// - Parameters:
/// - toLocalState: A key path that can get/set `State` inside `GlobalState`. /// - toLocalState: A key path that can get/set `State` inside `GlobalState`.
@@ -475,10 +484,11 @@ public struct Reducer<State, Action, Environment> {
/// Transforms a reducer that works on non-optional state into one that works on optional state by /// Transforms a reducer that works on non-optional state into one that works on optional state by
/// only running the non-optional reducer when state is non-nil. /// only running the non-optional reducer when state is non-nil.
/// ///
/// Often used in tandem with `pullback` to transform a reducer on a non-optional child domain /// Often used in tandem with ``pullback(state:action:environment:)`` to transform a reducer on a non-optional child domain
/// into a reducer that can be combined with a reducer on a parent domain that contains some /// into a reducer that can be combined with a reducer on a parent domain that contains some
/// optional child domain: /// optional child domain:
/// ///
/// ```swift
/// // Global domain that holds an optional local domain: /// // Global domain that holds an optional local domain:
/// struct AppState { var modal: ModalState? } /// struct AppState { var modal: ModalState? }
/// enum AppAction { case modal(ModalAction) } /// enum AppAction { case modal(ModalAction) }
@@ -498,6 +508,7 @@ public struct Reducer<State, Action, Environment> {
/// ... /// ...
/// } /// }
/// ) /// )
/// ```
/// ///
/// Take care when combining optional reducers into parent domains. An optional reducer cannot /// Take care when combining optional reducers into parent domains. An optional reducer cannot
/// process actions in its domain when its state is `nil`. If a child action is sent to an /// process actions in its domain when its state is `nil`. If a child action is sent to an
@@ -507,6 +518,7 @@ public struct Reducer<State, Action, Environment> {
/// * A parent reducer sets child state to `nil` when processing a child action and runs /// * A parent reducer sets child state to `nil` when processing a child action and runs
/// _before_ the child reducer: /// _before_ the child reducer:
/// ///
/// ```swift
/// let parentReducer = Reducer<ParentState, ParentAction, ParentEnvironment>.combine( /// let parentReducer = Reducer<ParentState, ParentAction, ParentEnvironment>.combine(
/// // When combining reducers, the parent reducer runs first /// // When combining reducers, the parent reducer runs first
/// Reducer { state, action, environment in /// Reducer { state, action, environment in
@@ -529,10 +541,12 @@ public struct Reducer<State, Action, Environment> {
/// // This action is never received here because child state is `nil` in the parent /// // This action is never received here because child state is `nil` in the parent
/// ... /// ...
/// } /// }
/// ```
/// ///
/// To ensure that a child reducer can process any action that a parent may use to `nil` out /// To ensure that a child reducer can process any action that a parent may use to `nil` out
/// its state, combine it _before_ the parent: /// its state, combine it _before_ the parent:
/// ///
/// ```swift
/// let parentReducer = Reducer<ParentState, ParentAction, ParentEnvironment>.combine( /// let parentReducer = Reducer<ParentState, ParentAction, ParentEnvironment>.combine(
/// // The child runs first /// // The child runs first
/// childReducer.optional().pullback(...), /// childReducer.optional().pullback(...),
@@ -541,9 +555,11 @@ public struct Reducer<State, Action, Environment> {
/// ... /// ...
/// } /// }
/// ) /// )
/// ```
/// ///
/// * A child effect feeds a child action back into the store when child state is `nil`: /// * A child effect feeds a child action back into the store when child state is `nil`:
/// ///
/// ```swift
/// let childReducer = Reducer< /// let childReducer = Reducer<
/// ChildState, ChildAction, ChildEnvironment /// ChildState, ChildAction, ChildEnvironment
/// > { state, action environment in /// > { state, action environment in
@@ -559,11 +575,13 @@ public struct Reducer<State, Action, Environment> {
/// ... /// ...
/// } /// }
/// } /// }
/// ```
/// ///
/// It is perfectly reasonable to ignore the result of an effect when child state is `nil`, /// It is perfectly reasonable to ignore the result of an effect when child state is `nil`,
/// for example one-off effects that you don't want to cancel. However, many long-living /// for example one-off effects that you don't want to cancel. However, many long-living
/// effects _should_ be explicitly canceled when tearing down a child domain: /// effects _should_ be explicitly canceled when tearing down a child domain:
/// ///
/// ```swift
/// let childReducer = Reducer< /// let childReducer = Reducer<
/// ChildState, ChildAction, ChildEnvironment /// ChildState, ChildAction, ChildEnvironment
/// > { state, action environment in /// > { state, action environment in
@@ -583,18 +601,22 @@ public struct Reducer<State, Action, Environment> {
/// ... /// ...
/// } /// }
/// } /// }
/// ```
/// ///
/// * A view store sends a child action when child state is `nil`: /// * A view store sends a child action when child state is `nil`:
/// ///
/// ```swift
/// WithViewStore(self.parentStore) { parentViewStore in /// WithViewStore(self.parentStore) { parentViewStore in
/// // If child state is `nil`, it cannot process this action. /// // If child state is `nil`, it cannot process this action.
/// Button("Child Action") { parentViewStore.send(.child(.action)) } /// Button("Child Action") { parentViewStore.send(.child(.action)) }
/// ... /// ...
/// } /// }
/// ```
/// ///
/// Use `Store.scope` with `IfLetStore` or `Store.ifLet` to ensure that views can only send /// Use ``Store/scope(state:action:)-9iai9`` with ``IfLetStore`` or ``Store/ifLet(then:else:)`` to ensure that views can only send
/// child actions when the child domain is non-`nil`. /// child actions when the child domain is non-`nil`.
/// ///
/// ```swift
/// IfLetStore( /// IfLetStore(
/// self.parentStore.scope(state: { $0.child }, action: { .child($0) } /// self.parentStore.scope(state: { $0.child }, action: { .child($0) }
/// ) { childStore in /// ) { childStore in
@@ -605,10 +627,11 @@ public struct Reducer<State, Action, Environment> {
/// } /// }
/// ... /// ...
/// } /// }
/// ```
/// ///
/// - See also: `IfLetStore`, a SwiftUI helper for transforming a store on optional state into a /// - See also: ``IfLetStore``, a SwiftUI helper for transforming a store on optional state into a
/// store on non-optional state. /// store on non-optional state.
/// - See also: `Store.ifLet`, a UIKit helper for doing imperative work with a store on optional /// - See also: ``Store/ifLet(then:else:)``, a UIKit helper for doing imperative work with a store on optional
/// state. /// state.
/// ///
/// - Parameter breakpointOnNil: Raises `SIGTRAP` signal when an action is sent to the reducer /// - Parameter breakpointOnNil: Raises `SIGTRAP` signal when an action is sent to the reducer
@@ -656,9 +679,9 @@ public struct Reducer<State, Action, Environment> {
} }
} }
/// A version of `pullback` that transforms a reducer that works on an element into one that works /// A version of ``pullback(state:action:environment:)`` that transforms a reducer that works on an element into one that works
/// on a collection of elements. /// on a collection of elements.
/// /// ```swift
/// // Global domain that holds a collection of local domains: /// // Global domain that holds a collection of local domains:
/// struct AppState { var todos: [Todo] } /// struct AppState { var todos: [Todo] }
/// enum AppAction { case todo(index: Int, action: TodoAction) } /// enum AppAction { case todo(index: Int, action: TodoAction) }
@@ -678,9 +701,10 @@ public struct Reducer<State, Action, Environment> {
/// ... /// ...
/// } /// }
/// ) /// )
/// ```
/// ///
/// Take care when combining `forEach` reducers into parent domains, as order matters. Always /// Take care when combining ``forEach(state:action:environment:breakpointOnNil:_:_:)-3ic87`` reducers into parent domains, as order matters. Always
/// combine `forEach` reducers _before_ parent reducers that can modify the collection. /// combine ``forEach(state:action:environment:breakpointOnNil:_:_:)-3ic87`` reducers _before_ parent reducers that can modify the collection.
/// ///
/// - Parameters: /// - Parameters:
/// - toLocalState: A key path that can get/set an array of `State` elements inside. /// - toLocalState: A key path that can get/set an array of `State` elements inside.
@@ -745,9 +769,10 @@ public struct Reducer<State, Action, Environment> {
} }
} }
/// A version of `pullback` that transforms a reducer that works on an element into one that works /// A version of ``pullback(state:action:environment:)`` that transforms a reducer that works on an element into one that works
/// on an identified array of elements. /// on an identified array of elements.
/// ///
/// ```swift
/// // Global domain that holds a collection of local domains: /// // Global domain that holds a collection of local domains:
/// struct AppState { var todos: IdentifiedArrayOf<Todo> } /// struct AppState { var todos: IdentifiedArrayOf<Todo> }
/// enum AppAction { case todo(id: Todo.ID, action: TodoAction) } /// enum AppAction { case todo(id: Todo.ID, action: TodoAction) }
@@ -767,9 +792,10 @@ public struct Reducer<State, Action, Environment> {
/// ... /// ...
/// } /// }
/// ) /// )
/// ```
/// ///
/// Take care when combining `forEach` reducers into parent domains, as order matters. Always /// Take care when combining ``forEach(state:action:environment:breakpointOnNil:_:_:)-90ox5`` reducers into parent domains, as order matters. Always
/// combine `forEach` reducers _before_ parent reducers that can modify the collection. /// combine ``forEach(state:action:environment:breakpointOnNil:_:_:)-90ox5`` reducers _before_ parent reducers that can modify the collection.
/// ///
/// - Parameters: /// - Parameters:
/// - toLocalState: A key path that can get/set a collection of `State` elements inside /// - toLocalState: A key path that can get/set a collection of `State` elements inside
@@ -836,11 +862,11 @@ public struct Reducer<State, Action, Environment> {
} }
} }
/// A version of `pullback` that transforms a reducer that works on an element into one that works /// A version of ``pullback(state:action:environment:)`` that transforms a reducer that works on an element into one that works
/// on a dictionary of element values. /// on a dictionary of element values.
/// ///
/// Take care when combining `forEach` reducers into parent domains, as order matters. Always /// Take care when combining ``forEach(state:action:environment:breakpointOnNil:_:_:)-xv1z`` reducers into parent domains, as order matters. Always
/// combine `forEach` reducers _before_ parent reducers that can modify the dictionary. /// combine ``forEach(state:action:environment:breakpointOnNil:_:_:)-xv1z`` reducers _before_ parent reducers that can modify the dictionary.
/// ///
/// - Parameters: /// - Parameters:
/// - toLocalState: A key path that can get/set a dictionary of `State` values inside /// - toLocalState: A key path that can get/set a dictionary of `State` values inside

View File

@@ -5,7 +5,7 @@ import Foundation
/// around to views that need to interact with the application. /// around to views that need to interact with the application.
/// ///
/// You will typically construct a single one of these at the root of your application, and then use /// You will typically construct a single one of these at the root of your application, and then use
/// the `scope` method to derive more focused stores that can be passed to subviews. /// the ``scope(state:action:)-9iai9`` method to derive more focused stores that can be passed to subviews.
public final class Store<State, Action> { public final class Store<State, Action> {
var state: CurrentValueSubject<State, Never> var state: CurrentValueSubject<State, Never>
var effectCancellables: [UUID: AnyCancellable] = [:] var effectCancellables: [UUID: AnyCancellable] = [:]
@@ -34,7 +34,7 @@ public final class Store<State, Action> {
/// ///
/// This can be useful for deriving new stores to hand to child views in an application. For /// This can be useful for deriving new stores to hand to child views in an application. For
/// example: /// example:
/// /// ```swift
/// // Application state made from local states. /// // Application state made from local states.
/// struct AppState { var login: LoginState, ... } /// struct AppState { var login: LoginState, ... }
/// struct AppAction { case login(LoginAction), ... } /// struct AppAction { case login(LoginAction), ... }
@@ -53,7 +53,7 @@ public final class Store<State, Action> {
/// action: { AppAction.login($0) } /// action: { AppAction.login($0) }
/// ) /// )
/// ) /// )
/// /// ```
/// Scoping in this fashion allows you to better modularize your application. In this case, /// Scoping in this fashion allows you to better modularize your application. In this case,
/// `LoginView` could be extracted to a module that has no access to `AppState` or `AppAction`. /// `LoginView` could be extracted to a module that has no access to `AppState` or `AppAction`.
/// ///
@@ -63,7 +63,7 @@ public final class Store<State, Action> {
/// For example, the above login domain could model a two screen login flow: a login form followed /// For example, the above login domain could model a two screen login flow: a login form followed
/// by a two-factor authentication screen. The second screen's domain might be nested in the /// by a two-factor authentication screen. The second screen's domain might be nested in the
/// first: /// first:
/// /// ```swift
/// struct LoginState: Equatable { /// struct LoginState: Equatable {
/// var email = "" /// var email = ""
/// var password = "" /// var password = ""
@@ -77,15 +77,15 @@ public final class Store<State, Action> {
/// case passwordChanged(String) /// case passwordChanged(String)
/// case twoFactorAuth(TwoFactorAuthAction) /// case twoFactorAuth(TwoFactorAuthAction)
/// } /// }
/// /// ```
/// The login view holds onto a store of this domain: /// The login view holds onto a store of this domain:
/// /// ```swift
/// struct LoginView: View { /// struct LoginView: View {
/// let store: Store<LoginState, LoginAction> /// let store: Store<LoginState, LoginAction>
/// ///
/// var body: some View { ... } /// var body: some View { ... }
/// } /// }
/// /// ```
/// If its body were to use a view store of the same domain, this would introduce a number of /// If its body were to use a view store of the same domain, this would introduce a number of
/// problems: /// problems:
/// ///
@@ -104,7 +104,7 @@ public final class Store<State, Action> {
/// ///
/// To avoid these issues, one can introduce a view-specific domain that slices off the subset of /// To avoid these issues, one can introduce a view-specific domain that slices off the subset of
/// state and actions that a view cares about: /// state and actions that a view cares about:
/// /// ```swift
/// extension LoginView { /// extension LoginView {
/// struct State: Equatable { /// struct State: Equatable {
/// var email: String /// var email: String
@@ -117,10 +117,10 @@ public final class Store<State, Action> {
/// case passwordChanged(String) /// case passwordChanged(String)
/// } /// }
/// } /// }
/// /// ```
/// One can also introduce a couple helpers that transform feature state into view state and /// One can also introduce a couple helpers that transform feature state into view state and
/// transform view actions into feature actions. /// transform view actions into feature actions.
/// /// ```swift
/// extension LoginState { /// extension LoginState {
/// var view: LoginView.State { /// var view: LoginView.State {
/// .init(email: self.email, password: self.password) /// .init(email: self.email, password: self.password)
@@ -139,10 +139,11 @@ public final class Store<State, Action> {
/// } /// }
/// } /// }
/// } /// }
/// ```
/// ///
/// With these helpers defined, `LoginView` can now scope its store's feature domain into its view /// With these helpers defined, `LoginView` can now scope its store's feature domain into its view
/// domain: /// domain:
/// /// ```swift
/// var body: some View { /// var body: some View {
/// WithViewStore( /// WithViewStore(
/// self.store.scope(state: { $0.view }, action: { $0.feature }) /// self.store.scope(state: { $0.view }, action: { $0.feature })
@@ -150,7 +151,7 @@ public final class Store<State, Action> {
/// ... /// ...
/// } /// }
/// } /// }
/// /// ```
/// This view store is now incapable of reading any state but view state (and will not recompute /// This view store is now incapable of reading any state but view state (and will not recompute
/// when non-view state changes), and is incapable of sending any actions but view actions. /// when non-view state changes), and is incapable of sending any actions but view actions.
/// ///

View File

@@ -13,6 +13,7 @@ import SwiftUI
/// ///
/// To use this API, you model all the action sheet actions in your domain's action enum: /// To use this API, you model all the action sheet actions in your domain's action enum:
/// ///
/// ```swift
/// enum AppAction: Equatable { /// enum AppAction: Equatable {
/// case cancelTapped /// case cancelTapped
/// case deleteTapped /// case deleteTapped
@@ -21,19 +22,23 @@ import SwiftUI
/// ///
/// // Your other actions /// // Your other actions
/// } /// }
/// ```
/// ///
/// And you model the state for showing the action sheet in your domain's state, and it can start /// And you model the state for showing the action sheet in your domain's state, and it can start
/// off in a `nil` state: /// off in a `nil` state:
/// ///
/// ```swift
/// struct AppState: Equatable { /// struct AppState: Equatable {
/// var actionSheet: ActionSheetState<AppAction>? /// var actionSheet: ActionSheetState<AppAction>?
/// ///
/// // Your other state /// // Your other state
/// } /// }
/// ```
/// ///
/// Then, in the reducer you can construct an `ActionSheetState` value to represent the action /// Then, in the reducer you can construct an `ActionSheetState` value to represent the action
/// sheet you want to show to the user: /// sheet you want to show to the user:
/// ///
/// ```swift
/// let appReducer = Reducer<AppState, AppAction, AppEnvironment> { state, action, env in /// let appReducer = Reducer<AppState, AppAction, AppEnvironment> { state, action, env in
/// switch action /// switch action
/// case .cancelTapped: /// case .cancelTapped:
@@ -60,15 +65,18 @@ import SwiftUI
/// return .none /// return .none
/// } /// }
/// } /// }
/// ```
/// ///
/// And then, in your view you can use the `.actionSheet(_:send:dismiss:)` method on `View` in order /// And then, in your view you can use the `.actionSheet(_:send:dismiss:)` method on `View` in order
/// to present the action sheet in a way that works best with the Composable Architecture: /// to present the action sheet in a way that works best with the Composable Architecture:
/// ///
/// ```swift
/// Button("Info") { viewStore.send(.infoTapped) } /// Button("Info") { viewStore.send(.infoTapped) }
/// .actionSheet( /// .actionSheet(
/// self.store.scope(state: \.actionSheet), /// self.store.scope(state: \.actionSheet),
/// dismiss: .cancelTapped /// dismiss: .cancelTapped
/// ) /// )
/// ```
/// ///
/// This makes your reducer in complete control of when the action sheet is shown or dismissed, and /// This makes your reducer in complete control of when the action sheet is shown or dismissed, and
/// makes it so that any choice made in the action sheet is automatically fed back into the reducer /// makes it so that any choice made in the action sheet is automatically fed back into the reducer
@@ -76,6 +84,7 @@ import SwiftUI
/// ///
/// Even better, you can instantly write tests that your action sheet behavior works as expected: /// Even better, you can instantly write tests that your action sheet behavior works as expected:
/// ///
/// ```swift
/// let store = TestStore( /// let store = TestStore(
/// initialState: AppState(), /// initialState: AppState(),
/// reducer: appReducer, /// reducer: appReducer,
@@ -96,6 +105,7 @@ import SwiftUI
/// $0.actionSheet = nil /// $0.actionSheet = nil
/// // Also verify that favoriting logic executed correctly /// // Also verify that favoriting logic executed correctly
/// } /// }
/// ```
/// ///
@available(iOS 13, *) @available(iOS 13, *)
@available(macCatalyst 13, *) @available(macCatalyst 13, *)

View File

@@ -13,6 +13,7 @@ import SwiftUI
/// ///
/// To use this API, you model all the alert actions in your domain's action enum: /// To use this API, you model all the alert actions in your domain's action enum:
/// ///
/// ```swift
/// enum AppAction: Equatable { /// enum AppAction: Equatable {
/// case cancelTapped /// case cancelTapped
/// case confirmTapped /// case confirmTapped
@@ -20,19 +21,23 @@ import SwiftUI
/// ///
/// // Your other actions /// // Your other actions
/// } /// }
/// ```
/// ///
/// And you model the state for showing the alert in your domain's state, and it can start off /// And you model the state for showing the alert in your domain's state, and it can start off
/// `nil`: /// `nil`:
/// ///
/// ```swift
/// struct AppState: Equatable { /// struct AppState: Equatable {
/// var alert: AlertState<AppAction>? /// var alert: AlertState<AppAction>?
/// ///
/// // Your other state /// // Your other state
/// } /// }
/// ```
/// ///
/// Then, in the reducer you can construct an `AlertState` value to represent the alert you want /// Then, in the reducer you can construct an ``AlertState`` value to represent the alert you want
/// to show to the user: /// to show to the user:
/// ///
/// ```swift
/// let appReducer = Reducer<AppState, AppAction, AppEnvironment> { state, action, env in /// let appReducer = Reducer<AppState, AppAction, AppEnvironment> { state, action, env in
/// switch action /// switch action
/// case .cancelTapped: /// case .cancelTapped:
@@ -53,15 +58,18 @@ import SwiftUI
/// return .none /// return .none
/// } /// }
/// } /// }
/// ```
/// ///
/// And then, in your view you can use the `.alert(_:send:dismiss:)` method on `View` in order /// And then, in your view you can use the `.alert(_:send:dismiss:)` method on `View` in order
/// to present the alert in a way that works best with the Composable Architecture: /// to present the alert in a way that works best with the Composable Architecture:
/// ///
/// ```swift
/// Button("Delete") { viewStore.send(.deleteTapped) } /// Button("Delete") { viewStore.send(.deleteTapped) }
/// .alert( /// .alert(
/// self.store.scope(state: \.alert), /// self.store.scope(state: \.alert),
/// dismiss: .cancelTapped /// dismiss: .cancelTapped
/// ) /// )
/// ```
/// ///
/// This makes your reducer in complete control of when the alert is shown or dismissed, and makes /// This makes your reducer in complete control of when the alert is shown or dismissed, and makes
/// it so that any choice made in the alert is automatically fed back into the reducer so that you /// it so that any choice made in the alert is automatically fed back into the reducer so that you
@@ -69,6 +77,7 @@ import SwiftUI
/// ///
/// Even better, you can instantly write tests that your alert behavior works as expected: /// Even better, you can instantly write tests that your alert behavior works as expected:
/// ///
/// ```swift
/// let store = TestStore( /// let store = TestStore(
/// initialState: AppState(), /// initialState: AppState(),
/// reducer: appReducer, /// reducer: appReducer,
@@ -87,6 +96,7 @@ import SwiftUI
/// $0.alert = nil /// $0.alert = nil
/// // Also verify that delete logic executed correctly /// // Also verify that delete logic executed correctly
/// } /// }
/// ```
/// ///
public struct AlertState<Action> { public struct AlertState<Action> {
public let id = UUID() public let id = UUID()

View File

@@ -8,6 +8,7 @@ import SwiftUI
/// ///
/// For example, a settings screen may model its state with the following struct: /// For example, a settings screen may model its state with the following struct:
/// ///
/// ```swift
/// struct SettingsState { /// struct SettingsState {
/// var digest = Digest.daily /// var digest = Digest.daily
/// var displayName = "" /// var displayName = ""
@@ -16,11 +17,13 @@ import SwiftUI
/// var sendEmailNotifications = false /// var sendEmailNotifications = false
/// var sendMobileNotifications = false /// var sendMobileNotifications = false
/// } /// }
/// ```
/// ///
/// Each of these fields should be editable, and in the Composable Architecture this means that each /// Each of these fields should be editable, and in the Composable Architecture this means that each
/// field requires a corresponding action that can be sent to the store. Typically this comes in the /// field requires a corresponding action that can be sent to the store. Typically this comes in the
/// form of an enum with a case per field: /// form of an enum with a case per field:
/// ///
/// ```swift
/// enum SettingsAction { /// enum SettingsAction {
/// case digestChanged(Digest) /// case digestChanged(Digest)
/// case displayNameChanged(String) /// case displayNameChanged(String)
@@ -29,10 +32,12 @@ import SwiftUI
/// case sendEmailNotificationsChanged(Bool) /// case sendEmailNotificationsChanged(Bool)
/// case sendMobileNotificationsChanged(Bool) /// case sendMobileNotificationsChanged(Bool)
/// } /// }
/// ```
/// ///
/// And we're not even done yet. In the reducer we must now handle each action, which simply /// And we're not even done yet. In the reducer we must now handle each action, which simply
/// replaces the state at each field with a new value: /// replaces the state at each field with a new value:
/// ///
/// ```swift
/// let settingsReducer = Reducer< /// let settingsReducer = Reducer<
/// SettingsState, SettingsAction, SettingsEnvironment /// SettingsState, SettingsAction, SettingsEnvironment
/// > { state, action, environment in /// > { state, action, environment in
@@ -62,19 +67,23 @@ import SwiftUI
/// return .none /// return .none
/// } /// }
/// } /// }
/// ```
/// ///
/// This is a _lot_ of boilerplate for something that should be simple. Luckily, we can dramatically /// This is a _lot_ of boilerplate for something that should be simple. Luckily, we can dramatically
/// eliminate this boilerplate using `BindingAction`. First, we can collapse all of these /// eliminate this boilerplate using ``BindingAction``. First, we can collapse all of these
/// field-mutating actions into a single case that holds a `BindingAction` generic over the /// field-mutating actions into a single case that holds a ``BindingAction`` generic over the
/// reducer's root `SettingsState`: /// reducer's root `SettingsState`:
/// ///
/// ```swift
/// enum SettingsAction { /// enum SettingsAction {
/// case binding(BindingAction<SettingsState>) /// case binding(BindingAction<SettingsState>)
/// } /// }
/// ```
/// ///
/// And then, we can simplify the settings reducer by allowing the `binding` method to handle these /// And then, we can simplify the settings reducer by allowing the `binding` method to handle these
/// field mutations for us: /// field mutations for us:
/// ///
/// ```swift
/// let settingsReducer = Reducer< /// let settingsReducer = Reducer<
/// SettingsState, SettingsAction, SettingsEnvironment /// SettingsState, SettingsAction, SettingsEnvironment
/// > { /// > {
@@ -84,30 +93,36 @@ import SwiftUI
/// } /// }
/// } /// }
/// .binding(action: /SettingsAction.binding) /// .binding(action: /SettingsAction.binding)
/// ```
/// ///
/// Binding actions are constructed and sent to the store by providing a writable key path from root /// Binding actions are constructed and sent to the store by providing a writable key path from root
/// state to the field being mutated. There is even a view store helper that simplifies this work. /// state to the field being mutated. There is even a view store helper that simplifies this work.
/// You can derive a binding by specifying the key path and binding action case: /// You can derive a binding by specifying the key path and binding action case:
/// ///
/// ```swift
/// TextField( /// TextField(
/// "Display name", /// "Display name",
/// text: viewStore.binding(keyPath: \.displayName, send: SettingsAction.binding) /// text: viewStore.binding(keyPath: \.displayName, send: SettingsAction.binding)
/// ) /// )
/// ```
/// ///
/// Should you need to layer additional functionality over these bindings, your reducer can pattern /// Should you need to layer additional functionality over these bindings, your reducer can pattern
/// match the action for a given key path: /// match the action for a given key path:
/// ///
/// ```swift
/// case .binding(\.displayName): /// case .binding(\.displayName):
/// // Validate display name /// // Validate display name
/// ///
/// case .binding(\.enableNotifications): /// case .binding(\.enableNotifications):
/// // Return an authorization request effect /// // Return an authorization request effect
/// ```
/// ///
/// Binding actions can also be tested in much the same way regular actions are tested. Rather than /// Binding actions can also be tested in much the same way regular actions are tested. Rather than
/// send a specific action describing how a binding changed, such as `displayNameChanged("Blob")`, /// send a specific action describing how a binding changed, such as `displayNameChanged("Blob")`,
/// you will send a `.binding` action that describes which key path is being set to what value, such /// you will send a ``Reducer/binding(action:)`` action that describes which key path is being set to what value, such
/// as `.binding(.set(\.displayName, "Blob"))`: /// as `.binding(.set(\.displayName, "Blob"))`:
/// ///
/// ```swift
/// let store = TestStore( /// let store = TestStore(
/// initialState: SettingsState(), /// initialState: SettingsState(),
/// reducer: settingsReducer, /// reducer: settingsReducer,
@@ -120,6 +135,7 @@ import SwiftUI
/// store.send(.binding(.set(\.protectMyPosts, true))) { /// store.send(.binding(.set(\.protectMyPosts, true))) {
/// $0.protectMyPosts = true /// $0.protectMyPosts = true
/// ) /// )
/// ```
/// ///
public struct BindingAction<Root>: Equatable { public struct BindingAction<Root>: Equatable {
public let keyPath: PartialKeyPath<Root> public let keyPath: PartialKeyPath<Root>
@@ -177,28 +193,32 @@ public struct BindingAction<Root>: Equatable {
} }
extension Reducer { extension Reducer {
/// Returns a reducer that applies `BindingAction` mutations to `State` before running this /// Returns a reducer that applies ``BindingAction`` mutations to `State` before running this
/// reducer's logic. /// reducer's logic.
/// ///
/// For example, a settings screen may gather its binding actions into a single `BindingAction` /// For example, a settings screen may gather its binding actions into a single ``BindingAction``
/// case: /// case:
/// ///
/// ```swift
/// enum SettingsAction { /// enum SettingsAction {
/// ... /// ...
/// case binding(BindingAction<SettingsState>) /// case binding(BindingAction<SettingsState>)
/// } /// }
/// ```
/// ///
/// The reducer can then be enhanced to automatically handle these mutations for you by tacking on /// The reducer can then be enhanced to automatically handle these mutations for you by tacking on
/// the `binding` method: /// the ``binding(action:)`` method:
/// ///
/// ```swift
/// let settingsReducer = Reducer<SettingsState, SettingsAction, SettingsEnvironment { /// let settingsReducer = Reducer<SettingsState, SettingsAction, SettingsEnvironment {
/// ... /// ...
/// } /// }
/// .binding(action: /SettingsAction.binding) /// .binding(action: /SettingsAction.binding)
/// ```
/// ///
/// - Parameter toBindingAction: A case path from this reducer's `Action` type to a /// - Parameter toBindingAction: A case path from this reducer's `Action` type to a
/// `BindingAction` over this reducer's `State`. /// `BindingAction` over this reducer's `State`.
/// - Returns: A reducer that applies `BindingAction` mutations to `State` before running this /// - Returns: A reducer that applies ``BindingAction`` mutations to `State` before running this
/// reducer's logic. /// reducer's logic.
public func binding(action toBindingAction: CasePath<Action, BindingAction<State>>) -> Self { public func binding(action toBindingAction: CasePath<Action, BindingAction<State>>) -> Self {
Self { state, action, environment in Self { state, action, environment in
@@ -211,10 +231,11 @@ extension Reducer {
extension ViewStore { extension ViewStore {
/// Derives a binding from the store that mutates state at the given writable key path by wrapping /// Derives a binding from the store that mutates state at the given writable key path by wrapping
/// a `BindingAction` with the store's action type. /// a ``BindingAction`` with the store's action type.
/// ///
/// For example, a text field binding can be created like this: /// For example, a text field binding can be created like this:
/// ///
/// ```swift
/// struct State { var text = "" } /// struct State { var text = "" }
/// enum Action { case binding(BindingAction<State>) } /// enum Action { case binding(BindingAction<State>) }
/// ///
@@ -222,6 +243,7 @@ extension ViewStore {
/// "Enter text", /// "Enter text",
/// text: viewStore.binding(keyPath: \.text, Action.binding) /// text: viewStore.binding(keyPath: \.text, Action.binding)
/// ) /// )
/// ```
/// ///
/// - Parameters: /// - Parameters:
/// - keyPath: A writable key path from the view store's state to a mutable field /// - keyPath: A writable key path from the view store's state to a mutable field

View File

@@ -3,12 +3,13 @@ import SwiftUI
/// A Composable Architecture-friendly wrapper around `ForEach` that simplifies working with /// A Composable Architecture-friendly wrapper around `ForEach` that simplifies working with
/// collections of state. /// collections of state.
/// ///
/// `ForEachStore` loops over a store's collection with a store scoped to the domain of each /// ``ForEachStore`` loops over a store's collection with a store scoped to the domain of each
/// element. This allows you to extract and modularize an element's view and avoid concerns around /// element. This allows you to extract and modularize an element's view and avoid concerns around
/// collection index math and parent-child store communication. /// collection index math and parent-child store communication.
/// ///
/// For example, a todos app may define the domain and logic associated with an individual todo: /// For example, a todos app may define the domain and logic associated with an individual todo:
/// ///
/// ```swift
/// struct TodoState: Equatable, Identifiable { /// struct TodoState: Equatable, Identifiable {
/// let id: UUID /// let id: UUID
/// var description = "" /// var description = ""
@@ -20,42 +21,54 @@ import SwiftUI
/// } /// }
/// struct TodoEnvironment {} /// struct TodoEnvironment {}
/// let todoReducer = Reducer<TodoState, TodoAction, TodoEnvironment { ... } /// let todoReducer = Reducer<TodoState, TodoAction, TodoEnvironment { ... }
/// ```
/// ///
/// As well as a view with a domain-specific store: /// As well as a view with a domain-specific store:
/// ///
/// ```swift
/// struct TodoView: View { /// struct TodoView: View {
/// let store: Store<TodoState, TodoAction> /// let store: Store<TodoState, TodoAction>
/// var body: some View { ... } /// var body: some View { ... }
/// } /// }
/// ```
/// ///
/// For a parent domain to work with a collection of todos, it can hold onto this collection in /// For a parent domain to work with a collection of todos, it can hold onto this collection in
/// state: /// state:
/// ///
/// ```swift
/// struct AppState: Equatable { /// struct AppState: Equatable {
/// var todos: IdentifiedArrayOf<TodoState> = [] /// var todos: IdentifiedArrayOf<TodoState> = []
/// } /// }
/// ```
/// ///
/// Define a case to handle actions sent to the child domain: /// Define a case to handle actions sent to the child domain:
/// ///
/// ```swift
/// enum AppAction { /// enum AppAction {
/// case todo(id: TodoState.ID, action: TodoAction) /// case todo(id: TodoState.ID, action: TodoAction)
/// } /// }
/// ```
/// ///
/// Enhance its reducer using `forEach`: /// Enhance its reducer using ``Reducer/forEach(state:action:environment:breakpointOnNil:_:_:)-3ic87``:
/// ///
/// ```swift
/// let appReducer = todoReducer.forEach( /// let appReducer = todoReducer.forEach(
/// state: \.todos, /// state: \.todos,
/// action: /AppAction.todo(id:action:), /// action: /AppAction.todo(id:action:),
/// environment: { _ in TodoEnvironment() } /// environment: { _ in TodoEnvironment() }
/// ) /// )
/// ```
/// ///
/// And finally render a list of `TodoView`s using `ForEachStore`: /// And finally render a list of `TodoView`s using ``ForEachStore``:
/// ///
/// ```swift
/// ForEachStore( /// ForEachStore(
/// self.store.scope(state: \.todos, AppAction.todo(id:action:)) /// self.store.scope(state: \.todos, AppAction.todo(id:action:))
/// ) { todoStore in /// ) { todoStore in
/// TodoView(store: todoStore) /// TodoView(store: todoStore)
/// } /// }
/// ```
///
public struct ForEachStore<EachState, EachAction, Data, ID, Content>: DynamicViewContent public struct ForEachStore<EachState, EachAction, Data, ID, Content>: DynamicViewContent
where Data: Collection, ID: Hashable, Content: View { where Data: Collection, ID: Hashable, Content: View {
public let data: Data public let data: Data

View File

@@ -17,7 +17,9 @@ public struct Identified<ID, Value>: Identifiable where ID: Hashable {
/// Initializes an identified value from a given value and a function that can return a hashable /// Initializes an identified value from a given value and a function that can return a hashable
/// identifier from the value. /// identifier from the value.
/// ///
/// ```swift
/// Identified(uuid, id: \.self) /// Identified(uuid, id: \.self)
/// ```
/// ///
/// - Parameters: /// - Parameters:
/// - value: A value. /// - value: A value.
@@ -30,7 +32,9 @@ public struct Identified<ID, Value>: Identifiable where ID: Hashable {
/// Initializes an identified value from a given value and a function that can return a hashable /// Initializes an identified value from a given value and a function that can return a hashable
/// identifier from the value. /// identifier from the value.
/// ///
/// ```swift
/// Identified(uuid, id: \.self) /// Identified(uuid, id: \.self)
/// ```
/// ///
/// - Parameters: /// - Parameters:
/// - value: A value. /// - value: A value.

View File

@@ -1,19 +1,22 @@
import Foundation import Foundation
/// An array of elements that can be identified by a given key path. /// An array of elements that can be identified by a given key path.
/// ///`
/// A useful container of state that is intended to interface with `SwiftUI.ForEach`. For example, /// A useful container of state that is intended to interface with `SwiftUI/ForEach`. For example,
/// your application may model a counter in an identifiable fashion: /// your application may model a counter in an identifiable fashion:
/// ///
/// ```swift
/// struct CounterState: Identifiable { /// struct CounterState: Identifiable {
/// let id: UUID /// let id: UUID
/// var count = 0 /// var count = 0
/// } /// }
/// enum CounterAction { case incr, decr } /// enum CounterAction { case incr, decr }
/// let counterReducer = Reducer<CounterState, CounterAction, Void> { ... } /// let counterReducer = Reducer<CounterState, CounterAction, Void> { ... }
/// ```
/// ///
/// This domain can be pulled back to a larger domain with the `forEach` method: /// This domain can be pulled back to a larger domain with the ``Reducer/forEach(state:action:environment:breakpointOnNil:_:_:)-90ox5`` method:
/// ///
/// ```swift
/// struct AppState { var counters = IdentifiedArrayOf<CounterState>() } /// struct AppState { var counters = IdentifiedArrayOf<CounterState>() }
/// enum AppAction { case counter(id: UUID, action: CounterAction) } /// enum AppAction { case counter(id: UUID, action: CounterAction) }
/// let appReducer = counterReducer.forEach( /// let appReducer = counterReducer.forEach(
@@ -21,9 +24,11 @@ import Foundation
/// action: /AppAction.counter(id:action:), /// action: /AppAction.counter(id:action:),
/// environment: { $0 } /// environment: { $0 }
/// ) /// )
/// ```
/// ///
/// And then SwiftUI can work with this array of identified elements in a list view: /// And then SwiftUI can work with this array of identified elements in a list view:
/// ///
/// ```swift
/// struct AppView: View { /// struct AppView: View {
/// let store: Store<AppState, AppAction> /// let store: Store<AppState, AppAction>
/// ///
@@ -36,6 +41,8 @@ import Foundation
/// } /// }
/// } /// }
/// } /// }
/// ```
///
public struct IdentifiedArray<ID, Element>: MutableCollection, RandomAccessCollection public struct IdentifiedArray<ID, Element>: MutableCollection, RandomAccessCollection
where ID: Hashable { where ID: Hashable {
/// A key path to a value that identifies an element. /// A key path to a value that identifies an element.

View File

@@ -2,19 +2,22 @@ import SwiftUI
/// A view that safely unwraps a store of optional state in order to show one of two views. /// A view that safely unwraps a store of optional state in order to show one of two views.
/// ///
/// When the underlying state is non-`nil`, the `then` closure will be performed with a `Store` that /// When the underlying state is non-`nil`, the `then` closure will be performed with a ``Store`` that
/// holds onto non-optional state, and otherwise the `else` closure will be performed. /// holds onto non-optional state, and otherwise the `else` closure will be performed.
/// ///
/// This is useful for deciding between two views to show depending on an optional piece of state: /// This is useful for deciding between two views to show depending on an optional piece of state:
/// ///
/// ```swift
/// IfLetStore( /// IfLetStore(
/// store.scope(state: \SearchState.results, action: SearchAction.results), /// store.scope(state: \SearchState.results, action: SearchAction.results),
/// then: SearchResultsView.init(store:), /// then: SearchResultsView.init(store:),
/// else: { Text("Loading search results...") } /// else: { Text("Loading search results...") }
/// ) /// )
/// ```
/// ///
/// And for performing navigation when a piece of state becomes non-`nil`: /// And for performing navigation when a piece of state becomes non-`nil`:
/// ///
/// ```swift
/// NavigationLink( /// NavigationLink(
/// destination: IfLetStore( /// destination: IfLetStore(
/// self.store.scope(state: \.detail, action: AppAction.detail), /// self.store.scope(state: \.detail, action: AppAction.detail),
@@ -27,12 +30,13 @@ import SwiftUI
/// ) { /// ) {
/// Text("Start!") /// Text("Start!")
/// } /// }
/// ```
/// ///
public struct IfLetStore<State, Action, Content>: View where Content: View { public struct IfLetStore<State, Action, Content>: View where Content: View {
private let content: (ViewStore<State?, Action>) -> Content private let content: (ViewStore<State?, Action>) -> Content
private let store: Store<State?, Action> private let store: Store<State?, Action>
/// Initializes an `IfLetStore` view that computes content depending on if a store of optional /// Initializes an ``IfLetStore`` view that computes content depending on if a store of optional
/// state is `nil` or non-`nil`. /// state is `nil` or non-`nil`.
/// ///
/// - Parameters: /// - Parameters:
@@ -55,7 +59,7 @@ public struct IfLetStore<State, Action, Content>: View where Content: View {
} }
} }
/// Initializes an `IfLetStore` view that computes content depending on if a store of optional /// Initializes an ``IfLetStore`` view that computes content depending on if a store of optional
/// state is `nil` or non-`nil`. /// state is `nil` or non-`nil`.
/// ///
/// - Parameters: /// - Parameters:

View File

@@ -7,42 +7,50 @@ import SwiftUI
/// `Equatable`, their `==` do not return `true` when used with seemingly equal values. If we were /// `Equatable`, their `==` do not return `true` when used with seemingly equal values. If we were
/// to naively store these values in state, our tests may begin to fail. /// to naively store these values in state, our tests may begin to fail.
/// ///
/// `TextState` solves this problem by providing an interface similar to `SwiftUI.Text` that can be /// ``TextState`` solves this problem by providing an interface similar to `SwiftUI.Text` that can be
/// held in state and asserted against. /// held in state and asserted against.
/// ///
/// Let's say you wanted to hold some dynamic, styled text content in your app state. You could use /// Let's say you wanted to hold some dynamic, styled text content in your app state. You could use
/// `TextState`: /// ``TextState``:
/// ///
/// ```swift
/// struct AppState: Equatable { /// struct AppState: Equatable {
/// var label: TextState /// var label: TextState
/// } /// }
/// ```
/// ///
/// Your reducer can then assign a value to this state using an API similar to that of /// Your reducer can then assign a value to this state using an API similar to that of
/// `SwiftUI.Text`. /// `SwiftUI.Text`.
/// ///
/// ```swift
/// state.label = TextState("Hello, ") + TextState(name).bold() + TextState("!") /// state.label = TextState("Hello, ") + TextState(name).bold() + TextState("!")
/// ```
/// ///
/// And your view store can render it directly: /// And your view store can render it directly:
/// ///
/// ```swift
/// var body: some View { /// var body: some View {
/// WithViewStore(self.store) { viewStore in /// WithViewStore(self.store) { viewStore in
/// viewStore.label /// viewStore.label
/// } /// }
/// } /// }
/// ```
/// ///
/// Certain SwiftUI APIs, like alerts and action sheets, take `Text` values and, not views. To /// Certain SwiftUI APIs, like alerts and action sheets, take `Text` values and, not views. To
/// convert `TextState` to `SwiftUI.Text` for this purpose, you can use the `Text` initializer: /// convert ``TextState`` to `SwiftUI.Text` for this purpose, you can use the `Text` initializer:
/// ///
/// ```swift
/// Alert(title: Text(viewStore.label)) /// Alert(title: Text(viewStore.label))
/// ```
/// ///
/// The Composable Architecture comes with a few convenience APIs for alerts and action sheets that /// The Composable Architecture comes with a few convenience APIs for alerts and action sheets that
/// wrap `TextState` under the hood. See `AlertState` and `ActionState` accordingly. /// wrap ``TextState`` under the hood. See ``AlertState`` and `ActionState` accordingly.
/// ///
/// In the future, should `SwiftUI.Text` and `SwiftUI.LocalizedStringKey` reliably conform to /// In the future, should `SwiftUI.Text` and `SwiftUI.LocalizedStringKey` reliably conform to
/// `Equatable`, `TextState` may be deprecated. /// `Equatable`, ``TextState`` may be deprecated.
/// ///
/// - Note: `TextState` does not support _all_ `LocalizedStringKey` permutations at this time /// - Note: ``TextState`` does not support _all_ `LocalizedStringKey` permutations at this time
/// (interpolated `SwiftUI.Image`s, for example. `TextState` also uses reflection to determine /// (interpolated `SwiftUI.Image`s, for example. ``TextState`` also uses reflection to determine
/// `LocalizedStringKey` equatability, so be mindful of edge cases. /// `LocalizedStringKey` equatability, so be mindful of edge cases.
public struct TextState: Equatable, Hashable { public struct TextState: Equatable, Hashable {
fileprivate var modifiers: [Modifier] = [] fileprivate var modifiers: [Modifier] = []

View File

@@ -7,14 +7,14 @@ import SwiftUI
/// Due to a bug in SwiftUI, there are times that use of this view can interfere with some core /// Due to a bug in SwiftUI, there are times that use of this view can interfere with some core
/// views provided by SwiftUI. The known problematic views are: /// views provided by SwiftUI. The known problematic views are:
/// ///
/// * If a `GeometryReader` or `ScrollViewReader` is used inside a `WithViewStore` it will not /// * If a `GeometryReader` or `ScrollViewReader` is used inside a ``WithViewStore`` it will not
/// receive state updates correctly. To work around you either need to reorder the views so that /// receive state updates correctly. To work around you either need to reorder the views so that
/// the `GeometryReader` or `ScrollViewReader` wraps `WithViewStore`, or, if that is not /// the `GeometryReader` or `ScrollViewReader` wraps ``WithViewStore``, or, if that is not
/// possible, then you must hold onto an explicit /// possible, then you must hold onto an explicit
/// `@ObservedObject var viewStore: ViewStore<State, Action>` in your view in lieu of using this /// `@ObservedObject var viewStore: ViewStore<State, Action>` in your view in lieu of using this
/// helper (see [here](https://gist.github.com/mbrandonw/cc5da3d487bcf7c4f21c27019a440d18)). /// helper (see [here](https://gist.github.com/mbrandonw/cc5da3d487bcf7c4f21c27019a440d18)).
/// * If you create a `Stepper` via the `Stepper.init(onIncrement:onDecrement:label:)` initializer /// * If you create a `Stepper` via the `Stepper.init(onIncrement:onDecrement:label:)` initializer
/// inside a `WithViewStore` it will behave erratically. To work around you should use the /// inside a ``WithViewStore`` it will behave erratically. To work around you should use the
/// initializer that takes a binding (see /// initializer that takes a binding (see
/// [here](https://gist.github.com/mbrandonw/dee2ceac2c316a1619cfdf1dc7945f66)). /// [here](https://gist.github.com/mbrandonw/dee2ceac2c316a1619cfdf1dc7945f66)).
public struct WithViewStore<State, Action, Content> { public struct WithViewStore<State, Action, Content> {

View File

@@ -10,6 +10,7 @@
/// For example, let's say we have a very simple counter application, where a user can increment /// 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: /// and decrement a number. The state and actions are simple enough:
/// ///
/// ```swift
/// struct CounterState: Equatable { /// struct CounterState: Equatable {
/// var count = 0 /// var count = 0
/// } /// }
@@ -18,18 +19,22 @@
/// case decrementButtonTapped /// case decrementButtonTapped
/// case incrementButtonTapped /// case incrementButtonTapped
/// } /// }
/// ```
/// ///
/// Let's throw in a side effect. If the user attempts to decrement the counter below zero, the /// 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. /// application should refuse and play an alert sound instead.
/// ///
/// We can model playing a sound in the environment with an effect: /// We can model playing a sound in the environment with an effect:
/// ///
/// ```swift
/// struct CounterEnvironment { /// struct CounterEnvironment {
/// let playAlertSound: () -> Effect<Never, Never> /// let playAlertSound: () -> Effect<Never, Never>
/// } /// }
/// ```
/// ///
/// Now that we've defined the domain, we can describe the logic in a reducer: /// Now that we've defined the domain, we can describe the logic in a reducer:
/// ///
/// ```swift
/// let counterReducer = Reducer< /// let counterReducer = Reducer<
/// CounterState, CounterAction, CounterEnvironment /// CounterState, CounterAction, CounterEnvironment
/// > { state, action, environment in /// > { state, action, environment in
@@ -48,11 +53,13 @@
/// return .non /// return .non
/// } /// }
/// } /// }
/// ```
/// ///
/// Let's say we want to write a test for the increment path. We can see in the reducer that it /// 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 /// should never play an alert, so we can configure the environment with an effect that will
/// fail if it ever executes: /// fail if it ever executes:
/// ///
/// ```swift
/// func testIncrement() { /// func testIncrement() {
/// let store = TestStore( /// let store = TestStore(
/// initialState: CounterState(count: 0) /// initialState: CounterState(count: 0)
@@ -66,6 +73,7 @@
/// $0.count = 1 /// $0.count = 1
/// } /// }
/// } /// }
/// ```
/// ///
/// By using a `.failing` effect in our environment we have strengthened the assertion and made /// 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 /// the test easier to understand at the same time. We can see, without consulting the reducer

View File

@@ -38,6 +38,7 @@
/// ///
/// For example, given a simple counter reducer: /// For example, given a simple counter reducer:
/// ///
/// ```swift
/// struct CounterState { /// struct CounterState {
/// var count = 0 /// var count = 0
/// } /// }
@@ -58,9 +59,11 @@
/// return .none /// return .none
/// } /// }
/// } /// }
/// ```
/// ///
/// One can assert against its behavior over time: /// One can assert against its behavior over time:
/// ///
/// ```swift
/// class CounterTests: XCTestCase { /// class CounterTests: XCTestCase {
/// func testCounter() { /// func testCounter() {
/// let store = TestStore( /// let store = TestStore(
@@ -73,14 +76,16 @@
/// } /// }
/// } /// }
/// } /// }
/// ```
/// ///
/// Note that in the trailing closure of `.send(.incrementButtonTapped)` we are given a single /// Note that in the trailing closure of `.send(.incrementButtonTapped)` we are given a single
/// mutable value of the state before the action was sent, and it is our job to mutate the value /// mutable value of the state before the action was sent, and it is our job to mutate the value
/// to match the state after the action was sent. In this case the `count` field changes to `1`. /// to match the state after the action was sent. In this case the `count` field changes to `1`.
/// ///
/// For a more complex example, consider the following bare-bones search feature that uses the /// For a more complex example, consider the following bare-bones search feature that uses the
/// `.debounce` operator to wait for the user to stop typing before making a network request: /// ``Effect/debounce(id:for:scheduler:options:)`` operator to wait for the user to stop typing before making a network request:
/// ///
/// ```swift
/// struct SearchState: Equatable { /// struct SearchState: Equatable {
/// var query = "" /// var query = ""
/// var results: [String] = [] /// var results: [String] = []
@@ -112,9 +117,11 @@
/// return .none /// return .none
/// } /// }
/// } /// }
/// ```swift
/// ///
/// It can be fully tested by controlling the environment's scheduler and effect: /// It can be fully tested by controlling the environment's scheduler and effect:
/// ///
/// ```swift
/// // Create a test dispatch scheduler to control the timing of effects /// // Create a test dispatch scheduler to control the timing of effects
/// let scheduler = DispatchQueue.test /// let scheduler = DispatchQueue.test
/// ///
@@ -153,6 +160,7 @@
/// // Assert that state updates accordingly /// // Assert that state updates accordingly
/// $0.results = ["Composable Architecture"] /// $0.results = ["Composable Architecture"]
/// } /// }
/// ```
/// ///
/// This test is proving that the debounced network requests are correctly canceled when we do not /// This test is proving that the debounced network requests are correctly canceled when we do not
/// wait longer than the 0.5 seconds, because if it wasn't and it delivered an action when we did /// wait longer than the 0.5 seconds, because if it wasn't and it delivered an action when we did
@@ -618,7 +626,7 @@
/// A step that captures a sub-sequence of steps. /// A step that captures a sub-sequence of steps.
/// ///
/// - Parameter steps: An array of `Step` /// - Parameter steps: An array of ``TestStore/Step``
/// - Returns: A step that captures a sub-sequence of steps. /// - Returns: A step that captures a sub-sequence of steps.
public static func sequence( public static func sequence(
_ steps: [Step], _ steps: [Step],
@@ -630,7 +638,7 @@
/// A step that captures a sub-sequence of steps. /// A step that captures a sub-sequence of steps.
/// ///
/// - Parameter steps: A variadic list of `Step` /// - Parameter steps: A variadic list of ``TestStore/Step``
/// - Returns: A step that captures a sub-sequence of steps. /// - Returns: A step that captures a sub-sequence of steps.
public static func sequence( public static func sequence(
_ steps: Step..., _ steps: Step...,

View File

@@ -12,6 +12,7 @@ extension Store {
/// to navigate to can be held as an optional value in the parent, and when that value goes from /// to navigate to can be held as an optional value in the parent, and when that value goes from
/// `nil` to non-`nil`, or non-`nil` to `nil`, you can update the navigation stack accordingly: /// `nil` to non-`nil`, or non-`nil` to `nil`, you can update the navigation stack accordingly:
/// ///
/// ```swift
/// class ParentViewController: UIViewController { /// class ParentViewController: UIViewController {
/// let store: Store<ParentState, ParentAction> /// let store: Store<ParentState, ParentAction>
/// var cancellables: Set<AnyCancellable> = [] /// var cancellables: Set<AnyCancellable> = []
@@ -35,6 +36,7 @@ extension Store {
/// .store(in: &self.cancellables) /// .store(in: &self.cancellables)
/// } /// }
/// } /// }
/// ```
/// ///
/// - Parameters: /// - Parameters:
/// - unwrap: A function that is called with a store of non-optional state when the store's /// - unwrap: A function that is called with a store of non-optional state when the store's

View File

@@ -1,14 +1,15 @@
import Combine import Combine
import SwiftUI import SwiftUI
/// A `ViewStore` is an object that can observe state changes and send actions. They are most /// A ``ViewStore`` is an object that can observe state changes and send actions. They are most
/// commonly used in views, such as SwiftUI views, UIView or UIViewController, but they can be /// commonly used in views, such as SwiftUI views, UIView or UIViewController, but they can be
/// used anywhere it makes sense to observe state and send actions. /// used anywhere it makes sense to observe state and send actions.
/// ///
/// In SwiftUI applications, a `ViewStore` is accessed most commonly using the `WithViewStore` view. /// In SwiftUI applications, a ``ViewStore`` is accessed most commonly using the ``WithViewStore`` view.
/// It can be initialized with a store and a closure that is handed a view store and must return a /// It can be initialized with a store and a closure that is handed a view store and must return a
/// view to be rendered: /// view to be rendered:
/// ///
/// ```swift
/// var body: some View { /// var body: some View {
/// WithViewStore(self.store) { viewStore in /// WithViewStore(self.store) { viewStore in
/// VStack { /// VStack {
@@ -17,10 +18,11 @@ import SwiftUI
/// } /// }
/// } /// }
/// } /// }
/// /// ```
/// In UIKit applications a `ViewStore` can be created from a `Store` and then subscribed to for /// In UIKit applications a ``ViewStore`` can be created from a ``Store`` and then subscribed to for
/// state updates: /// state updates:
/// ///
/// ```swift
/// let store: Store<State, Action> /// let store: Store<State, Action>
/// let viewStore: ViewStore<State, Action> /// let viewStore: ViewStore<State, Action>
/// ///
@@ -40,6 +42,7 @@ import SwiftUI
/// @objc func incrementButtonTapped() { /// @objc func incrementButtonTapped() {
/// self.viewStore.send(.incrementButtonTapped) /// self.viewStore.send(.incrementButtonTapped)
/// } /// }
/// ```
/// ///
@dynamicMemberLookup @dynamicMemberLookup
public final class ViewStore<State, Action>: ObservableObject { public final class ViewStore<State, Action>: ObservableObject {
@@ -85,10 +88,10 @@ public final class ViewStore<State, Action>: ObservableObject {
/// Sends an action to the store. /// Sends an action to the store.
/// ///
/// `ViewStore` is not thread safe and you should only send actions to it from the main thread. /// ``ViewStore`` is not thread safe and you should only send actions to it from the main thread.
/// If you are wanting to send actions on background threads due to the fact that the reducer /// If you are wanting to send actions on background threads due to the fact that the reducer
/// is performing computationally expensive work, then a better way to handle this is to wrap /// is performing computationally expensive work, then a better way to handle this is to wrap
/// that work in an `Effect` that is performed on a background thread so that the result can /// that work in an ``Effect`` that is performed on a background thread so that the result can
/// be fed back into the store. /// be fed back into the store.
/// ///
/// - Parameter action: An action. /// - Parameter action: An action.
@@ -100,11 +103,12 @@ public final class ViewStore<State, Action>: ObservableObject {
/// actions to the store. /// actions to the store.
/// ///
/// The method is useful for dealing with SwiftUI components that work with two-way `Binding`s /// The method is useful for dealing with SwiftUI components that work with two-way `Binding`s
/// since the `Store` does not allow directly writing its state; it only allows reading state and /// since the ``Store`` does not allow directly writing its state; it only allows reading state and
/// sending actions. /// sending actions.
/// ///
/// For example, a text field binding can be created like this: /// For example, a text field binding can be created like this:
/// ///
/// ```swift
/// struct State { var name = "" } /// struct State { var name = "" }
/// enum Action { case nameChanged(String) } /// enum Action { case nameChanged(String) }
/// ///
@@ -115,6 +119,7 @@ public final class ViewStore<State, Action>: ObservableObject {
/// send: { Action.nameChanged($0) } /// send: { Action.nameChanged($0) }
/// ) /// )
/// ) /// )
/// ```
/// ///
/// - Parameters: /// - Parameters:
/// - get: A function to get the state for the binding from the view /// - get: A function to get the state for the binding from the view
@@ -144,11 +149,12 @@ public final class ViewStore<State, Action>: ObservableObject {
/// actions to the store. /// actions to the store.
/// ///
/// The method is useful for dealing with SwiftUI components that work with two-way `Binding`s /// The method is useful for dealing with SwiftUI components that work with two-way `Binding`s
/// since the `Store` does not allow directly writing its state; it only allows reading state and /// since the ``Store`` does not allow directly writing its state; it only allows reading state and
/// sending actions. /// sending actions.
/// ///
/// For example, an alert binding can be dealt with like this: /// For example, an alert binding can be dealt with like this:
/// ///
/// ```swift
/// struct State { var alert: String? } /// struct State { var alert: String? }
/// enum Action { case alertDismissed } /// enum Action { case alertDismissed }
/// ///
@@ -158,6 +164,7 @@ public final class ViewStore<State, Action>: ObservableObject {
/// send: .alertDismissed /// send: .alertDismissed
/// ) /// )
/// ) { alert in Alert(title: Text(alert.message)) } /// ) { alert in Alert(title: Text(alert.message)) }
/// ```
/// ///
/// - Parameters: /// - Parameters:
/// - get: A function to get the state for the binding from the view store's full state. /// - get: A function to get the state for the binding from the view store's full state.
@@ -174,11 +181,12 @@ public final class ViewStore<State, Action>: ObservableObject {
/// actions to the store. /// actions to the store.
/// ///
/// The method is useful for dealing with SwiftUI components that work with two-way `Binding`s /// The method is useful for dealing with SwiftUI components that work with two-way `Binding`s
/// since the `Store` does not allow directly writing its state; it only allows reading state and /// since the ``Store`` does not allow directly writing its state; it only allows reading state and
/// sending actions. /// sending actions.
/// ///
/// For example, a text field binding can be created like this: /// For example, a text field binding can be created like this:
/// ///
/// ```swift
/// typealias State = String /// typealias State = String
/// enum Action { case nameChanged(String) } /// enum Action { case nameChanged(String) }
/// ///
@@ -188,6 +196,7 @@ public final class ViewStore<State, Action>: ObservableObject {
/// send: { Action.nameChanged($0) } /// send: { Action.nameChanged($0) }
/// ) /// )
/// ) /// )
/// ```
/// ///
/// - Parameters: /// - Parameters:
/// - localStateToViewAction: A function that transforms the binding's value /// - localStateToViewAction: A function that transforms the binding's value
@@ -203,11 +212,12 @@ public final class ViewStore<State, Action>: ObservableObject {
/// actions to the store. /// actions to the store.
/// ///
/// The method is useful for dealing with SwiftUI components that work with two-way `Binding`s /// The method is useful for dealing with SwiftUI components that work with two-way `Binding`s
/// since the `Store` does not allow directly writing its state; it only allows reading state and /// since the ``Store`` does not allow directly writing its state; it only allows reading state and
/// sending actions. /// sending actions.
/// ///
/// For example, an alert binding can be dealt with like this: /// For example, an alert binding can be dealt with like this:
/// ///
/// ```swift
/// typealias State = String /// typealias State = String
/// enum Action { case alertDismissed } /// enum Action { case alertDismissed }
/// ///
@@ -216,6 +226,7 @@ public final class ViewStore<State, Action>: ObservableObject {
/// send: .alertDismissed /// send: .alertDismissed
/// ) /// )
/// ) { title in Alert(title: Text(title)) } /// ) { title in Alert(title: Text(title)) }
/// ```
/// ///
/// - Parameters: /// - Parameters:
/// - action: The action to send when the binding is written to. /// - action: The action to send when the binding is written to.