-
Notifications
You must be signed in to change notification settings - Fork 1.5k
/
Copy pathIdentifiedArray+Observation.swift
139 lines (133 loc) · 4.42 KB
/
IdentifiedArray+Observation.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
import OrderedCollections
import SwiftUI
extension Store where State: ObservableState {
/// Scopes the store of an identified collection to a collection of stores.
///
/// This operator is most often used with SwiftUI's `ForEach` view. For example, suppose you
/// have a feature that contains an `IdentifiedArray` of child features like so:
///
/// ```swift
/// @Reducer
/// struct Feature {
/// @ObservableState
/// struct State {
/// var rows: IdentifiedArrayOf<Child.State> = []
/// }
/// enum Action {
/// case rows(IdentifiedActionOf<Child>)
/// }
/// var body: some ReducerOf<Self> {
/// Reduce { state, action in
/// // Core feature logic
/// }
/// .forEach(\.rows, action: \.rows) {
/// Child()
/// }
/// }
/// }
/// ```
///
/// Then in the view you can use this operator, with `ForEach`, to derive a store for
/// each element in the identified collection:
///
/// ```swift
/// struct FeatureView: View {
/// let store: StoreOf<Feature>
///
/// var body: some View {
/// List {
/// ForEach(store.scope(state: \.rows, action: \.rows), id: \.state.id) { store in
/// ChildView(store: store)
/// }
/// }
/// }
/// }
/// ```
///
/// > Tip: If you do not depend on the identity of the state of each row (_e.g._, the state's
/// > `id` is not associated with a selection binding), you can omit the `id` parameter, as the
/// > `Store` type is identifiable by its object identity:
/// >
/// > ```diff
/// > ForEach(
/// > - store.scope(state: \.rows, action: \.rows),
/// > - id: \.state.id,
/// > + store.scope(state: \.rows, action: \.rows)
/// > ) { childStore in
/// > ChildView(store: childStore)
/// > }
/// > ```
///
/// - Parameters:
/// - state: A key path to an identified array of child state.
/// - action: A case key path to an identified child action.
/// - column: The column.
/// - fileID: The fileID.
/// - filePath: The filePath.
/// - line: The line.
/// - Returns: An collection of stores of child state.
@_disfavoredOverload
public func scope<ElementID, ElementState, ElementAction>(
state: KeyPath<State, IdentifiedArray<ElementID, ElementState>>,
action: CaseKeyPath<Action, IdentifiedAction<ElementID, ElementAction>>,
fileID: StaticString = #fileID,
filePath: StaticString = #filePath,
line: UInt = #line,
column: UInt = #column
) -> some RandomAccessCollection<Store<ElementState, ElementAction>> {
if !self.canCacheChildren {
reportIssue(
uncachedStoreWarning(self),
fileID: fileID,
filePath: filePath,
line: line,
column: column
)
}
return _StoreCollection(self.scope(state: state, action: action))
}
}
public struct _StoreCollection<ID: Hashable & Sendable, State, Action>: RandomAccessCollection {
private let store: Store<IdentifiedArray<ID, State>, IdentifiedAction<ID, Action>>
private let data: IdentifiedArray<ID, State>
#if swift(<5.10)
@MainActor(unsafe)
#else
@preconcurrency@MainActor
#endif
fileprivate init(_ store: Store<IdentifiedArray<ID, State>, IdentifiedAction<ID, Action>>) {
self.store = store
self.data = store.withState { $0 }
}
public var startIndex: Int { self.data.startIndex }
public var endIndex: Int { self.data.endIndex }
public subscript(position: Int) -> Store<State, Action> {
precondition(
Thread.isMainThread,
#"""
Store collections must be interacted with on the main actor.
When passing a scoped store to a 'ForEach' in a lazy view (for example, 'LazyVStack'), it \
must be eagerly transformed into a collection to avoid access off the main actor:
Array(store.scope(state: \.elements, action: \.elements))
"""#
)
return MainActor._assumeIsolated { [uncheckedSelf = UncheckedSendable(self)] in
let `self` = uncheckedSelf.wrappedValue
guard self.data.indices.contains(position)
else {
return Store()
}
let id = self.data.ids[position]
var element = self.data[position]
return self.store.scope(
id: self.store.id(state: \.[id: id]!, action: \.[id: id]),
state: ToState {
element = $0[id: id] ?? element
return element
},
action: { .element(id: id, action: $0) },
isInvalid: { !$0.ids.contains(id) }
)
}
}
}