@@ -42,16 +42,120 @@ public final class Store<State, Action> {
4242 /// struct AppAction { case login(LoginAction), ... }
4343 ///
4444 /// // A store that runs the entire application.
45- /// let store = Store(initialState: AppState(), reducer: appReducer, environment: ())
45+ /// let store = Store(
46+ /// initialState: AppState(),
47+ /// reducer: appReducer,
48+ /// environment: AppEnvironment()
49+ /// )
4650 ///
4751 /// // Construct a login view by scoping the store to one that works with only login domain.
48- /// let loginView = LoginView(
52+ /// LoginView(
4953 /// store: store.scope(
5054 /// state: { $0.login },
5155 /// action: { AppAction.login($0) }
5256 /// )
5357 /// )
5458 ///
59+ /// Scoping in this fashion allows you to better modularize your application. In this case,
60+ /// `LoginView` could be extracted to a module that has no access to `AppState` or `AppAction`.
61+ ///
62+ /// Scoping also gives a view the opportunity to focus on just the state and actions it cares
63+ /// about, even if its feature domain is larger.
64+ ///
65+ /// For example, the above login domain could model a two screen login flow: a login form followed
66+ /// by a two-factor authentication screen. The second screen's domain might be nested in the
67+ /// first:
68+ ///
69+ /// struct LoginState: Equatable {
70+ /// var email = ""
71+ /// var password = ""
72+ /// var twoFactorAuth: TwoFactorAuthState?
73+ /// }
74+ ///
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+ ///
83+ /// The login view holds onto a store of this domain:
84+ ///
85+ /// struct LoginView: View {
86+ /// let store: Store<LoginState, LoginAction>
87+ ///
88+ /// var body: some View { ... }
89+ /// }
90+ ///
91+ /// If its body were to use a view store of the same domain, this would introduce a number of
92+ /// problems:
93+ ///
94+ /// * The login view would be able to read from `twoFactorAuth` state. This state is only intended
95+ /// to be read from the two-factor auth screen.
96+ ///
97+ /// * Even worse, changes to `twoFactorAuth` state would now cause SwiftUI to recompute
98+ /// `LoginView`'s body unnecessarily.
99+ ///
100+ /// * The login view would be able to send `twoFactorAuth` actions. These actions are only
101+ /// intended to be sent from the two-factor auth screen (and reducer).
102+ ///
103+ /// * The login view would be able to send non user-facing login actions, like `loginResponse`.
104+ /// These actions are only intended to be used in the login reducer to feed the results of
105+ /// effects back into the store.
106+ ///
107+ /// To avoid these issues, one can introduce a view-specific domain that slices off the subset of
108+ /// state and actions that a view cares about:
109+ ///
110+ /// extension LoginView {
111+ /// struct State: Equatable {
112+ /// var email: String
113+ /// var password: String
114+ /// }
115+ ///
116+ /// enum Action: Equatable {
117+ /// case emailChanged(String)
118+ /// case loginButtonTapped
119+ /// case passwordChanged(String)
120+ /// }
121+ /// }
122+ ///
123+ /// One can also introduce a couple helpers that transform feature state into view state and
124+ /// transform view actions into feature actions.
125+ ///
126+ /// extension LoginState {
127+ /// var view: LoginView.State {
128+ /// .init(email: self.email, password: self.password)
129+ /// }
130+ /// }
131+ ///
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+ /// }
143+ /// }
144+ ///
145+ /// With these helpers defined, `LoginView` can now scope its store's feature domain into its view
146+ /// domain:
147+ ///
148+ /// var body: some View {
149+ /// WithViewStore(
150+ /// self.store.scope(state: { $0.view }, action: { $0.feature })
151+ /// ) { viewStore in
152+ /// ...
153+ /// }
154+ /// }
155+ ///
156+ /// This view store is now incapable of reading any state but view state (and will not recompute
157+ /// when non-view state changes), and is incapable of sending any actions but view actions.
158+ ///
55159 /// - Parameters:
56160 /// - toLocalState: A function that transforms `State` into `LocalState`.
57161 /// - fromLocalAction: A function that transforms `LocalAction` into `Action`.
0 commit comments