Skip to content

Commit 341b09b

Browse files
committed
wip
1 parent c1218e9 commit 341b09b

File tree

2 files changed

+85
-7
lines changed

2 files changed

+85
-7
lines changed

Sources/ComposableArchitecture/Reducer/Reducers/PresentationReducer.swift

+8-4
Original file line numberDiff line numberDiff line change
@@ -194,10 +194,14 @@ extension ReducerProtocol {
194194
/// The `ifLet` operator does a number of things to make integrating parent and child features
195195
/// ergonomic and enforce correctness:
196196
///
197-
/// * It forces a specific order of operations for the child and parent features. It runs the
198-
/// child first, and then the parent. If the order was reversed, then it would be possible for
199-
/// the parent feature to `nil` out the child state, in which case the child feature would not
200-
/// be able to react to that action. That can cause subtle bugs.
197+
/// * It forces a specific order of operations for the child and parent features:
198+
/// * When a ``PresentationAction/dismiss`` action is sent, it runs the parent feature
199+
/// before the child state is `nil`'d out. This gives the parent feature an opportunity to
200+
/// inspect the child state one last time before the state is cleared.
201+
/// * When a ``PresentationAction/presented(_:)`` action is sent it runs runs the
202+
/// child first, and then the parent. If the order was reversed, then it would be possible
203+
/// for the parent feature to `nil` out the child state, in which case the child feature
204+
/// would not be able to react to that action. That can cause subtle bugs.
201205
///
202206
/// * It automatically cancels all child effects when it detects the child's state is `nil`'d
203207
/// out.

Sources/ComposableArchitecture/Reducer/Reducers/StackReducer.swift

+77-3
Original file line numberDiff line numberDiff line change
@@ -178,8 +178,54 @@ extension ReducerProtocol {
178178
/// Embeds a child reducer in a parent domain that works on elements of a navigation stack in
179179
/// parent state.
180180
///
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.
183229
///
184230
/// - Parameters:
185231
/// - toStackState: A writable key path from parent state to a stack of destination state.
@@ -373,6 +419,34 @@ public struct _StackReducer<
373419
}
374420

375421
/// 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.
376450
public struct StackElementID: Hashable, Sendable {
377451
@_spi(Internals) public var generation: Int
378452
@_spi(Internals) public var rawValue: AnyHashableSendable
@@ -382,7 +456,7 @@ public struct StackElementID: Hashable, Sendable {
382456
self.rawValue = AnyHashableSendable(rawValue)
383457
}
384458

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 && ?
386460
public static func == (lhs: Self, rhs: Self) -> Bool {
387461
lhs.rawValue == rhs.rawValue || lhs.generation == rhs.generation
388462
}

0 commit comments

Comments
 (0)