Skip to content

Commit cfdf2e9

Browse files
Improve some docs. (#597)
* Improve some docs. * wip * Update Sources/ComposableArchitecture/Store.swift Co-authored-by: Stephen Celis <[email protected]> * Update Sources/ComposableArchitecture/Store.swift Co-authored-by: Stephen Celis <[email protected]> * Update Sources/ComposableArchitecture/Store.swift Co-authored-by: Stephen Celis <[email protected]> * Update Sources/ComposableArchitecture/Store.swift Co-authored-by: Stephen Celis <[email protected]> * Update Sources/ComposableArchitecture/Store.swift Co-authored-by: Stephen Celis <[email protected]> * Update Sources/ComposableArchitecture/Store.swift Co-authored-by: Stephen Celis <[email protected]> * Update Sources/ComposableArchitecture/ViewStore.swift Co-authored-by: Stephen Celis <[email protected]> * 110 column * update link * add docc to switchstore Co-authored-by: Stephen Celis <[email protected]>
1 parent fd87f4d commit cfdf2e9

File tree

4 files changed

+253
-131
lines changed

4 files changed

+253
-131
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -385,7 +385,7 @@ The Composable Architecture comes with a number of tools to aid in debugging.
385385

386386
1. If done simply with `DispatchQueue.main.async` you will incur a thread hop even when you are already on the main thread. This can lead to unexpected behavior in UIKit and SwiftUI, where sometimes you are required to do work synchronously, such as in animation blocks.
387387

388-
2. It is possible to create a scheduler that performs its work immediately when on the main thread and otherwise uses `DispatchQueue.main.async` (_e.g._ see ReactiveSwift's [`UIScheduler`](https://github.com/ReactiveCocoa/ReactiveSwift/blob/f97db218c0236b0c6ef74d32adb3d578792969c0/Sources/Scheduler.swift)). This introduces a lot more complexity, and should probably not be adopted without having a very good reason.
388+
2. It is possible to create a scheduler that performs its work immediately when on the main thread and otherwise uses `DispatchQueue.main.async` (_e.g._ see [CombineScheduler](https://github.com/pointfreeco/combine-schedulers)'s [`UIScheduler`](https://github.com/pointfreeco/combine-schedulers/blob/main/Sources/CombineSchedulers/UIScheduler.swift)). This introduces a lot more complexity, and should probably not be adopted without having a very good reason.
389389

390390
This is why we require all actions be sent from the same thread. This requirement is in the same spirit of how `URLSession` and other Apple APIs are designed. Those APIs tend to deliver their outputs on whatever thread is most convenient for them, and then it is your responsibility to dispatch back to the main queue if that's what you need. The Composable Architecture makes you responsible for making sure to send actions on the main thread. If you are using an effect that may deliver its output on a non-main thread, you must explicitly perform `.observe(on:)` in order to force it back on the main thread.
391391

Sources/ComposableArchitecture/Store.swift

Lines changed: 194 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,112 @@ import ReactiveSwift
55
/// around to views that need to interact with the application.
66
///
77
/// You will typically construct a single one of these at the root of your application, and then use
8-
/// the ``scope(state:action:)-9iai9`` method to derive more focused stores that can be passed to subviews.
8+
/// the ``scope(state:action:)-9iai9`` method to derive more focused stores that can be passed to
9+
/// subviews:
10+
///
11+
/// ```swift
12+
/// @main
13+
/// struct MyApp: App {
14+
/// var body: some Scene {
15+
/// WindowGroup {
16+
/// RootView(
17+
/// store: Store(
18+
/// initialState: AppState(),
19+
/// reducer: appReducer,
20+
/// environment: AppEnvironment(
21+
/// ...
22+
/// )
23+
/// )
24+
/// )
25+
/// }
26+
/// }
27+
/// }
28+
/// ```
29+
///
30+
/// ### Scoping
31+
///
32+
/// The most important operation defined on ``Store`` is the ``scope(state:action:)-9iai9`` method,
33+
/// which allows you to transform a store into one that deals with local state and actions. This is
34+
/// necessary for passing stores to subviews that only care about a small portion of the entire
35+
/// application's domain.
36+
///
37+
/// For example, if an application has a tab view at its root with tabs for activity, search, and
38+
/// profile, then we can model the domain like this:
39+
///
40+
/// ```swift
41+
/// struct AppState {
42+
/// var activity: ActivityState
43+
/// var profile: ProfileState
44+
/// var search: SearchState
45+
/// }
46+
///
47+
/// enum AppAction {
48+
/// case activity(ActivityState)
49+
/// case profile(ProfileState)
50+
/// case search(SearchState)
51+
/// }
52+
/// ```
53+
///
54+
/// We can construct a view for each of these domains by applying ``scope(state:action:)-9iai9``
55+
/// to a store that holds onto the full app domain in order to transform it into a store for each
56+
/// sub-domain:
57+
///
58+
/// ```swift
59+
/// struct AppView: View {
60+
/// let store: Store<AppState, AppAction>
61+
///
62+
/// var body: some View {
63+
/// TabView {
64+
/// ActivityView(store: self.store.scope(state: \.activity, action: AppAction.activity))
65+
/// .tabItem { Text("Activity") }
66+
///
67+
/// SearchView(store: self.store.scope(state: \.search, action: AppAction.search))
68+
/// .tabItem { Text("Search") }
69+
///
70+
/// ProfileView(store: self.store.scope(state: \.profile, action: AppAction.profile))
71+
/// .tabItem { Text("Profile") }
72+
/// }
73+
/// }
74+
/// ```
75+
///
76+
/// ### Thread safety
77+
///
78+
/// The `Store` class is not thread-safe, and so all interactions with an instance of ``Store``
79+
/// (including all of its scopes and derived ``ViewStore``s) must be done on the same thread.
80+
/// Further, if the store is powering a SwiftUI or UIKit view, as is customary, then all
81+
/// interactions must be done on the _main_ thread.
82+
///
83+
/// The reason stores are not thread-safe is due to the fact that when an action is sent to a store,
84+
/// a reducer is run on the current state, and this process cannot be done from multiple threads.
85+
/// It is possible to make this process thread-safe by introducing locks or queues, but this
86+
/// introduces new complications:
87+
///
88+
/// * If done simply with `DispatchQueue.main.async` you will incur a thread hop even when you are
89+
/// already on the main thread. This can lead to unexpected behavior in UIKit and SwiftUI, where
90+
/// sometimes you are required to do work synchronously, such as in animation blocks.
91+
///
92+
/// * It is possible to create a scheduler that performs its work immediately when on the main
93+
/// thread and otherwise uses `DispatchQueue.main.async` (e.g. see CombineScheduler's [UIScheduler](https://github.com/pointfreeco/combine-schedulers/blob/main/Sources/CombineSchedulers/UIScheduler.swift)). This introduces a lot more complexity, and should probably not be adopted without having a very
94+
/// good reason.
95+
///
96+
/// This is why we require all actions be sent from the same thread. This requirement is in the same
97+
/// spirit of how `URLSession` and other Apple APIs are designed. Those APIs tend to deliver their
98+
/// outputs on whatever thread is most convenient for them, and then it is your responsibility to
99+
/// dispatch back to the main queue if that's what you need. The Composable Architecture makes you
100+
/// responsible for making sure to send actions on the main thread. If you are using an effect that
101+
/// may deliver its output on a non-main thread, you must explicitly perform `.receive(on:)` in
102+
/// order to force it back on the main thread.
103+
///
104+
/// This approach makes the fewest number of assumptions about how effects are created and
105+
/// transformed, and prevents unnecessary thread hops and re-dispatching. It also provides some
106+
/// testing benefits. If your effects are not responsible for their own scheduling, then in tests
107+
/// all of the effects would run synchronously and immediately. You would not be able to test how
108+
/// multiple in-flight effects interleave with each other and affect the state of your application.
109+
/// However, by leaving scheduling out of the ``Store`` we get to test these aspects of our effects
110+
/// if we so desire, or we can ignore if we prefer. We have that flexibility.
111+
///
112+
/// See also: ``ViewStore`` to understand how one observes changes to the state in a ``Store`` and
113+
/// sends user actions.
9114
public final class Store<State, Action> {
10115
@MutableProperty
11116
private(set) var state: State
@@ -36,26 +141,28 @@ public final class Store<State, Action> {
36141
///
37142
/// This can be useful for deriving new stores to hand to child views in an application. For
38143
/// example:
39-
/// ```swift
40-
/// // Application state made from local states.
41-
/// struct AppState { var login: LoginState, ... }
42-
/// struct AppAction { case login(LoginAction), ... }
43144
///
44-
/// // A store that runs the entire application.
45-
/// let store = Store(
46-
/// initialState: AppState(),
47-
/// reducer: appReducer,
48-
/// environment: AppEnvironment()
49-
/// )
145+
/// ```swift
146+
/// // Application state made from local states.
147+
/// struct AppState { var login: LoginState, ... }
148+
/// struct AppAction { case login(LoginAction), ... }
149+
///
150+
/// // A store that runs the entire application.
151+
/// let store = Store(
152+
/// initialState: AppState(),
153+
/// reducer: appReducer,
154+
/// environment: AppEnvironment()
155+
/// )
156+
///
157+
/// // Construct a login view by scoping the store to one that works with only login domain.
158+
/// LoginView(
159+
/// store: store.scope(
160+
/// state: \.login,
161+
/// action: AppAction.login
162+
/// )
163+
/// )
164+
/// ```
50165
///
51-
/// // Construct a login view by scoping the store to one that works with only login domain.
52-
/// LoginView(
53-
/// store: store.scope(
54-
/// state: { $0.login },
55-
/// action: { AppAction.login($0) }
56-
/// )
57-
/// )
58-
/// ```
59166
/// Scoping in this fashion allows you to better modularize your application. In this case,
60167
/// `LoginView` could be extracted to a module that has no access to `AppState` or `AppAction`.
61168
///
@@ -65,29 +172,32 @@ public final class Store<State, Action> {
65172
/// For example, the above login domain could model a two screen login flow: a login form followed
66173
/// by a two-factor authentication screen. The second screen's domain might be nested in the
67174
/// first:
68-
/// ```swift
69-
/// struct LoginState: Equatable {
70-
/// var email = ""
71-
/// var password = ""
72-
/// var twoFactorAuth: TwoFactorAuthState?
73-
/// }
74175
///
75-
/// enum LoginAction: Equatable {
76-
/// case emailChanged(String)
77-
/// case loginButtonTapped
78-
/// case loginResponse(Result<TwoFactorAuthState, LoginError>)
79-
/// case passwordChanged(String)
80-
/// case twoFactorAuth(TwoFactorAuthAction)
81-
/// }
82-
/// ```
176+
/// ```swift
177+
/// struct LoginState: Equatable {
178+
/// var email = ""
179+
/// var password = ""
180+
/// var twoFactorAuth: TwoFactorAuthState?
181+
/// }
182+
///
183+
/// enum LoginAction: Equatable {
184+
/// case emailChanged(String)
185+
/// case loginButtonTapped
186+
/// case loginResponse(Result<TwoFactorAuthState, LoginError>)
187+
/// case passwordChanged(String)
188+
/// case twoFactorAuth(TwoFactorAuthAction)
189+
/// }
190+
/// ```
191+
///
83192
/// The login view holds onto a store of this domain:
84-
/// ```swift
85-
/// struct LoginView: View {
86-
/// let store: Store<LoginState, LoginAction>
193+
/// ```swift
194+
/// struct LoginView: View {
195+
/// let store: Store<LoginState, LoginAction>
196+
///
197+
/// var body: some View { ... }
198+
/// }
199+
/// ```
87200
///
88-
/// var body: some View { ... }
89-
/// }
90-
/// ```
91201
/// If its body were to use a view store of the same domain, this would introduce a number of
92202
/// problems:
93203
///
@@ -106,54 +216,59 @@ public final class Store<State, Action> {
106216
///
107217
/// To avoid these issues, one can introduce a view-specific domain that slices off the subset of
108218
/// state and actions that a view cares about:
109-
/// ```swift
110-
/// extension LoginView {
111-
/// struct State: Equatable {
112-
/// var email: String
113-
/// var password: String
114-
/// }
115219
///
116-
/// enum Action: Equatable {
117-
/// case emailChanged(String)
118-
/// case loginButtonTapped
119-
/// case passwordChanged(String)
120-
/// }
121-
/// }
122-
/// ```
220+
/// ```swift
221+
/// extension LoginView {
222+
/// struct State: Equatable {
223+
/// var email: String
224+
/// var password: String
225+
/// }
226+
///
227+
/// enum Action: Equatable {
228+
/// case emailChanged(String)
229+
/// case loginButtonTapped
230+
/// case passwordChanged(String)
231+
/// }
232+
/// }
233+
/// ```
234+
///
123235
/// One can also introduce a couple helpers that transform feature state into view state and
124236
/// transform view actions into feature actions.
125-
/// ```swift
126-
/// extension LoginState {
127-
/// var view: LoginView.State {
128-
/// .init(email: self.email, password: self.password)
129-
/// }
130-
/// }
131237
///
132-
/// extension LoginView.Action {
133-
/// var feature: LoginAction {
134-
/// switch self {
135-
/// case let .emailChanged(email)
136-
/// return .emailChanged(email)
137-
/// case .loginButtonTapped:
138-
/// return .loginButtonTapped
139-
/// case let .passwordChanged(password)
140-
/// return .passwordChanged(password)
141-
/// }
142-
/// }
238+
/// ```swift
239+
/// extension LoginState {
240+
/// var view: LoginView.State {
241+
/// .init(email: self.email, password: self.password)
242+
/// }
243+
/// }
244+
///
245+
/// extension LoginView.Action {
246+
/// var feature: LoginAction {
247+
/// switch self {
248+
/// case let .emailChanged(email)
249+
/// return .emailChanged(email)
250+
/// case .loginButtonTapped:
251+
/// return .loginButtonTapped
252+
/// case let .passwordChanged(password)
253+
/// return .passwordChanged(password)
143254
/// }
144-
/// ```
255+
/// }
256+
/// }
257+
/// ```
145258
///
146259
/// With these helpers defined, `LoginView` can now scope its store's feature domain into its view
147260
/// domain:
148-
/// ```swift
149-
/// var body: some View {
150-
/// WithViewStore(
151-
/// self.store.scope(state: { $0.view }, action: { $0.feature })
152-
/// ) { viewStore in
153-
/// ...
154-
/// }
155-
/// }
156-
/// ```
261+
///
262+
/// ```swift
263+
/// var body: some View {
264+
/// WithViewStore(
265+
/// self.store.scope(state: \.view, action: \.feature)
266+
/// ) { viewStore in
267+
/// ...
268+
/// }
269+
/// }
270+
/// ```
271+
///
157272
/// This view store is now incapable of reading any state but view state (and will not recompute
158273
/// when non-view state changes), and is incapable of sending any actions but view actions.
159274
///

0 commit comments

Comments
 (0)