Skip to content

Commit 67f2377

Browse files
Improve docs for stack-based navigation (#2967)
* Improve docs for stack based navigation. * wip * Improve docs for stack based navigation * wip * Update Sources/ComposableArchitecture/Documentation.docc/Articles/StackBasedNavigation.md Co-authored-by: Stephen Celis <[email protected]> --------- Co-authored-by: Stephen Celis <[email protected]>
1 parent 4254c84 commit 67f2377

File tree

1 file changed

+85
-54
lines changed

1 file changed

+85
-54
lines changed

Sources/ComposableArchitecture/Documentation.docc/Articles/StackBasedNavigation.md

+85-54
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ flat collection of data, handing it off to SwiftUI, and letting it take care of
1111
It also allows for complex and recursive navigation paths in your application.
1212

1313
* [Basics](#Basics)
14+
* [Pushing features onto the stack](#Pushing-features-onto-the-stack)
1415
* [Integration](#Integration)
1516
* [Dismissal](#Dismissal)
1617
* [Testing](#Testing)
@@ -36,35 +37,17 @@ struct RootFeature {
3637
// ...
3738

3839
@Reducer
39-
struct Path {
40-
@ObservableState
41-
enum State {
42-
case addItem(AddFeature.State)
43-
case detailItem(DetailFeature.State)
44-
case editItem(EditFeature.State)
45-
}
46-
enum Action {
47-
case addItem(AddFeature.Action)
48-
case detailItem(DetailFeature.Action)
49-
case editItem(EditFeature.Action)
50-
}
51-
var body: some ReducerOf<Self> {
52-
Scope(state: \.addItem, action: \.addItem) {
53-
AddFeature()
54-
}
55-
Scope(state: \.editItem, action: \.editItem) {
56-
EditFeature()
57-
}
58-
Scope(state: \.detailItem, action: \.detailItem) {
59-
DetailFeature()
60-
}
61-
}
40+
enum Path {
41+
case addItem(AddFeature)
42+
case detailItem(DetailFeature)
43+
case editItem(EditFeature)
6244
}
6345
}
6446
```
6547

66-
> Note: The `Path` reducer is identical to the `Destination` reducer that one creates for tree-based
67-
> navigation when using enums. See <doc:TreeBasedNavigation#Enum-state> for more information.
48+
> Note: The `Path` reducer is identical to the `Destination` reducer that one creates for
49+
> tree-based navigation when using enums. See <doc:TreeBasedNavigation#Enum-state> for more
50+
> information.
6851
6952
Once the `Path` reducer is defined we can then hold onto ``StackState`` and ``StackAction`` in the
7053
feature that manages the navigation stack:
@@ -78,18 +61,18 @@ struct RootFeature {
7861
// ...
7962
}
8063
enum Action {
81-
case path(StackAction<Path.State, Path.Action>)
64+
case path(StackActionOf<Path>)
8265
// ...
8366
}
8467
}
8568
```
8669

87-
> Note: ``StackAction`` is generic over both state and action of the `Path` domain. This is
88-
> different from ``PresentationAction``, which only has a single generic.
70+
> Tip: ``StackAction`` is generic over both state and action of the `Path` domain, and so you can
71+
> use the ``StackActionOf`` typealias to simplify the syntax a bit. This is different from
72+
> ``PresentationAction``, which only has a single generic of `Action`.
8973
90-
And then we must make use of the ``Reducer/forEach(_:action:destination:fileID:line:)-yz3v``
91-
method to integrate the domains of all the features that can be navigated to with the domain of the
92-
parent feature:
74+
And then we must make use of the ``Reducer/forEach(_:action:)`` method to integrate the domains of
75+
all the features that can be navigated to with the domain of the parent feature:
9376

9477
```swift
9578
@Reducer
@@ -100,13 +83,14 @@ struct RootFeature {
10083
Reduce { state, action in
10184
// Core logic for root feature
10285
}
103-
.forEach(\.path, action: \.path) {
104-
Path()
105-
}
86+
.forEach(\.path, action: \.path)
10687
}
10788
}
10889
```
10990

91+
> Note: You do not need to specify `Path()` in a trailing closure of `forEach` because it can be
92+
> automatically inferred from `@Reducer enum Path`.
93+
11094
That completes the steps to integrate the child and parent features together for a navigation stack.
11195

11296
Next we must integrate the child and parent views together. This is done by a
@@ -148,14 +132,16 @@ struct RootView: View {
148132
The root view can be anything you want, and would typically have some `NavigationLink`s or other
149133
buttons that push new data onto the ``StackState`` held in your domain.
150134

151-
And the last trailing closure is provided a store of `Path` domain so that you can switch on it:
135+
And the last trailing closure is provided a store of `Path` domain, and you can use the
136+
``Store/case`` computed property to destructure each case of the `Path` to obtain a store focused
137+
on just that case:
152138

153139
```swift
154140
} destination: { store in
155-
switch store.state {
156-
case .addItem:
157-
case .detailItem:
158-
case .editItem:
141+
switch store.case {
142+
case .addItem(let store):
143+
case .detailItem(let store):
144+
case .editItem(let store):
159145
}
160146
}
161147
```
@@ -168,19 +154,13 @@ scope the store down to a specific case of the `Path.State` enum:
168154

169155
```swift
170156
} destination: { store in
171-
switch store.state {
172-
case .addItem:
173-
if let store = store.scope(state: \.addItem, action: \.addItem) {
174-
AddView(store: store)
175-
}
176-
case .detailItem:
177-
if let store = store.scope(state: \.detailItem, action: \.detailItem) {
178-
DetailView(store: store)
179-
}
180-
case .editItem:
181-
if let store = store.scope(state: \.editItem, action: \.editItem) {
182-
EditView(store: store)
183-
}
157+
switch store.case {
158+
case .addItem(let store):
159+
AddView(store: store)
160+
case .detailItem(let store):
161+
DetailView(store: store)
162+
case .editItem(let store):
163+
EditView(store: store)
184164
}
185165
}
186166
```
@@ -191,6 +171,57 @@ additional features to the stack by adding a new case to the `Path` reducer stat
191171
and you get complete introspection into what is happening in each child feature from the parent.
192172
Continue reading into <doc:StackBasedNavigation#Integration> for more information on that.
193173

174+
## Pushing features onto the stack
175+
176+
There are two primary ways to push features onto the stack once you have their domains integrated
177+
and `NavigationStack` in the view, as described above. The simplest way is to use the
178+
``SwiftUI/NavigationLink/init(state:label:fileID:line:)`` initializer on `NavigationLink`, which
179+
requires you to specify the state of the feature you want to push onto the stack. You must specify
180+
the full state, going all the way back to the `Path` reducer's state:
181+
182+
```swift
183+
Form {
184+
NavigationLink(
185+
state: RootFeature.Path.State.detail(DetailFeature.State())
186+
) {
187+
Text("Detail")
188+
}
189+
}
190+
```
191+
192+
When the link is tapped a ``StackAction/push(id:state:)`` action will be sent, causing the `path`
193+
collection to be mutated and appending the `.detail` state to the stack.
194+
195+
This is by far the simplest way to navigate to a screen, but it also has its drawbacks. In
196+
particular, it makes modularity difficult since the view that holds onto the `NavigationLink` must
197+
have access to the `Path.State` type, which means it needs to build all of the `Path` reducer,
198+
including _every_ feature that can be navigated to.
199+
200+
This hurts modularity because it is no longer possible to build each feature that can be presented
201+
in the stack individually, in full isolation. You must build them all together. Technically you can
202+
move all features' `State` types (and only the `State` types) to a separate module, and then
203+
features can depend on only that module without needing to build every feature's reducer.
204+
205+
Another alternative is to forgo `NavigationLink` entirely and just use `Button` that sends an action
206+
in the child feature's domain:
207+
208+
```swift
209+
Form {
210+
Button("Detail") {
211+
store.send(.detailButtonTapped)
212+
}
213+
}
214+
```
215+
216+
Then the root feature can listen for that action and append to the `path` with new state in order
217+
to drive navigation:
218+
219+
```swift
220+
case .path(.element(id: _, action: .list(.detailButtonTapped))):
221+
state.path.append(.detail(DetailFeature.State()))
222+
return .none
223+
```
224+
194225
## Integration
195226

196227
Once your features are integrated together using the steps above, your parent feature gets instant
@@ -211,7 +242,7 @@ additional logic, such as popping the "edit" feature and saving the edited item
211242

212243
```swift
213244
case let .path(.element(id: id, action: .editItem(.saveButtonTapped))):
214-
guard case let .editItem(editItemState) = state.path[id: id]
245+
guard let editItemState = state.path[id: id]?.editItem
215246
else { return .none }
216247

217248
state.path.pop(from: id)
@@ -365,7 +396,7 @@ struct Feature {
365396
var path = StackState<Path.State>()
366397
}
367398
enum Action {
368-
case path(StackAction<Path.State, Path.Action>)
399+
case path(StackActionOf<Path>)
369400
}
370401

371402
@Reducer

0 commit comments

Comments
 (0)