@@ -178,8 +178,54 @@ extension ReducerProtocol {
178
178
/// Embeds a child reducer in a parent domain that works on elements of a navigation stack in
179
179
/// parent state.
180
180
///
181
- /// For example, if a parent feature holds onto a ``StackState`` of destination states, then it
182
- /// can perform its core logic _and_ the destination's logic by using the `forEach` operator:
181
+ /// This version of `forEach` works when the parent domain holds onto the child domain using
182
+ /// ``StackState`` and ``StackAction``.
183
+ ///
184
+ /// For example, if a parent feature models a navigation stack of child features using the
185
+ /// ``StackState`` and ``StackAction`` types, then it can perform its core logic _and_ the logic
186
+ /// of each child feature using the `forEach` operator:
187
+ ///
188
+ /// ```swift
189
+ /// struct ParentFeature: ReducerProtocol {
190
+ /// struct State {
191
+ /// var path = StackState<Path.State>()
192
+ /// // ...
193
+ /// }
194
+ /// enum Action {
195
+ /// case path(StackAction<Path.State, Path.Action>)
196
+ /// // ...
197
+ /// }
198
+ /// var body: some ReducerProtocolOf<Self> {
199
+ /// Reduce { state, action in
200
+ /// // Core parent logic
201
+ /// }
202
+ /// .forEach(\.path, action: /Action.path) {
203
+ /// Path()
204
+ /// }
205
+ /// }
206
+ /// }
207
+ /// ```
208
+ ///
209
+ /// The `forEach` operator does a number of things to make integrating parent and child features
210
+ /// ergonomic and enforce correctness:
211
+ ///
212
+ /// * It forces a specific order of operations for the child and parent features:
213
+ /// * When a ``StackAction/element(id:action:)`` action is sent it runs runs the
214
+ /// child first, and then the parent. If the order was reversed, then it would be possible
215
+ /// for the parent feature to `nil` out the child state, in which case the child feature
216
+ /// would not be able to react to that action. That can cause subtle bugs.
217
+ /// * When a ``StackAction/popFrom(id:)`` action is sent it runs the parent feature
218
+ /// before the child state is popped off the stack. This gives the parent feature an
219
+ /// opportunity to inspect the child state one last time before the state is removed.
220
+ /// * When a ``StackAction/push(id:state:)`` action is sent it runs the parent feature
221
+ /// after the child state is appended to the stack. This gives the parent feature an
222
+ /// opportunity to make extra mutations to the state after it has been added.
223
+ ///
224
+ /// * It automatically cancels all child effects when it detects the child's state is removed
225
+ /// from the stack
226
+ ///
227
+ /// * It gives the child feature access to the ``DismissEffect`` dependency, which allows the
228
+ /// child feature to dismiss itself without communicating with the parent.
183
229
///
184
230
/// - Parameters:
185
231
/// - toStackState: A writable key path from parent state to a stack of destination state.
@@ -373,6 +419,34 @@ public struct _StackReducer<
373
419
}
374
420
375
421
/// An opaque type that identifies an element of ``StackState``.
422
+ ///
423
+ /// The ``StackState`` type creates instances of this identifier when new elements are added to
424
+ /// the stack. This makes it possible to easily look up specific elements in the stack without
425
+ /// resorting to positional indices, which can be error prone, especially when dealing with async
426
+ /// effects.
427
+ ///
428
+ /// In production environments (e.g. in Xcode previews, simulators and on devices) the identifier
429
+ /// is backed by a randomly generated UUID, but in tests a deterministic, generational ID is used.
430
+ /// This allows you to predict how IDs will be created and allows you to write tests for how
431
+ /// features behave in the stack.
432
+ ///
433
+ /// ```swift
434
+ /// func testBasics() {
435
+ /// var path = StackState<Int>()
436
+ /// path.append(42)
437
+ /// XCTAssertEqual(path[id: 0], 42)
438
+ /// path.append(1729)
439
+ /// XCTAssertEqual(path[id: 1], 1729)
440
+ ///
441
+ /// path.removeAll()
442
+ /// path.append(-1)
443
+ /// XCTAssertEqual(path[id: 2], -1)
444
+ /// }
445
+ /// ```
446
+ ///
447
+ /// Notice that after removing all elements and appending a new element, the ID generated was 2 and
448
+ /// did not go back to 0. This is because in tests the IDs are _generational_, which means they
449
+ /// keep counting up, even if you remove elements from the stack.
376
450
public struct StackElementID : Hashable , Sendable {
377
451
@_spi ( Internals) public var generation : Int
378
452
@_spi ( Internals) public var rawValue : AnyHashableSendable
@@ -382,7 +456,7 @@ public struct StackElementID: Hashable, Sendable {
382
456
self . rawValue = AnyHashableSendable ( rawValue)
383
457
}
384
458
385
- // TODO: is this still correct? can we get test coverage that breaks when || is changed to && ?
459
+ // TODO: is this still correct? can we get a test that fails when || is changed to && ?
386
460
public static func == ( lhs: Self , rhs: Self ) -> Bool {
387
461
lhs. rawValue == rhs. rawValue || lhs. generation == rhs. generation
388
462
}
0 commit comments