Skip to content

Commit 192d675

Browse files
committed
Improve performance of ForEachStore (#386)
* Improve performance of ForEachStore * fix * Fix * Cleanup * Basic docs * Update ForEachStore.swift
1 parent 4ff1b56 commit 192d675

File tree

1 file changed

+142
-107
lines changed

1 file changed

+142
-107
lines changed
Lines changed: 142 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -1,123 +1,158 @@
11
#if canImport(SwiftUI)
2-
import SwiftUI
2+
import SwiftUI
33

4-
/// A structure that computes views on demand from a store on a collection of data.
5-
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
6-
public struct ForEachStore<EachState, EachAction, Data, ID, Content>: DynamicViewContent
7-
where Data: Collection, ID: Hashable, Content: View {
8-
public let data: Data
9-
private let content: () -> Content
4+
/// A Composable Architecture-friendly wrapper around `ForEach` that simplifies working with
5+
/// collections of state.
6+
///
7+
/// `ForEachStore` loops over a store's collection with a store scoped to the domain of each
8+
/// element. This allows you to extract and modularize an element's view and avoid concerns around
9+
/// collection index math and parent-child store communication.
10+
///
11+
/// For example, a todos app may define the domain and logic associated with an individual todo:
12+
///
13+
/// struct TodoState: Equatable, Identifiable {
14+
/// let id: UUID
15+
/// var description = ""
16+
/// var isComplete = false
17+
/// }
18+
/// enum TodoAction {
19+
/// case isCompleteToggled(Bool)
20+
/// case descriptionChanged(String)
21+
/// }
22+
/// struct TodoEnvironment {}
23+
/// let todoReducer = Reducer<TodoState, TodoAction, TodoEnvironment { ... }
24+
///
25+
/// As well as a view with a domain-specific store:
26+
///
27+
/// struct TodoView: View {
28+
/// let store: Store<TodoState, TodoAction>
29+
/// var body: some View { ... }
30+
/// }
31+
///
32+
/// For a parent domain to work with a collection of todos, it can hold onto this collection in
33+
/// state:
34+
///
35+
/// struct AppState: Equatable {
36+
/// var todos: IdentifiedArrayOf<TodoState> = []
37+
/// }
38+
///
39+
/// Define a case to handle actions sent to the child domain:
40+
///
41+
/// enum AppAction {
42+
/// case todo(id: TodoState.ID, action: TodoAction)
43+
/// }
44+
///
45+
/// Enhance its reducer using `forEach`:
46+
///
47+
/// let appReducer = todoReducer.forEach(
48+
/// state: \.todos,
49+
/// action: /AppAction.todo(id:action:),
50+
/// environment: { _ in TodoEnvironment() }
51+
/// )
52+
///
53+
/// And finally render a list of `TodoView`s using `ForEachStore`:
54+
///
55+
/// ForEachStore(
56+
/// self.store.scope(state: \.todos, AppAction.todo(id:action:))
57+
/// ) { todoStore in
58+
/// TodoView(store: todoStore)
59+
/// }
60+
@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
61+
public struct ForEachStore<EachState, EachAction, Data, ID, Content>: DynamicViewContent
62+
where Data: Collection, ID: Hashable, Content: View {
63+
public let data: Data
64+
private let content: () -> Content
1065

11-
/// Initializes a structure that computes views on demand from a store on an array of data and an
12-
/// indexed action.
13-
///
14-
/// - Parameters:
15-
/// - store: A store on an array of data and an indexed action.
16-
/// - id: A key path identifying an element.
17-
/// - content: A function that can generate content given a store of an element.
18-
public init<EachContent>(
19-
_ store: Store<Data, (Data.Index, EachAction)>,
20-
id: KeyPath<EachState, ID>,
21-
content: @escaping (Store<EachState, EachAction>) -> EachContent
22-
)
23-
where
24-
Data == [EachState],
25-
EachContent: View,
26-
Content == WithViewStore<
27-
Data, (Data.Index, EachAction),
28-
ForEach<ContiguousArray<(Data.Index, EachState)>, ID, EachContent>
29-
>
30-
{
31-
self.data = ViewStore(store, removeDuplicates: { _, _ in false }).state
32-
self.content = {
33-
WithViewStore(
34-
store,
35-
removeDuplicates: { lhs, rhs in
36-
guard lhs.count == rhs.count else { return false }
37-
return zip(lhs, rhs).allSatisfy { $0[keyPath: id] == $1[keyPath: id] }
38-
}
39-
) { viewStore in
40-
ForEach(
41-
ContiguousArray(zip(viewStore.indices, viewStore.state)),
42-
id: (\(Data.Index, EachState).1).appending(path: id)
43-
) { index, element in
44-
content(
45-
store.scope(
46-
state: { index < $0.endIndex ? $0[index] : element },
47-
action: { (index, $0) }
48-
)
66+
/// Initializes a structure that computes views on demand from a store on an array of data and an
67+
/// indexed action.
68+
///
69+
/// - Parameters:
70+
/// - store: A store on an array of data and an indexed action.
71+
/// - id: A key path identifying an element.
72+
/// - content: A function that can generate content given a store of an element.
73+
public init<EachContent>(
74+
_ store: Store<Data, (Data.Index, EachAction)>,
75+
id: KeyPath<EachState, ID>,
76+
content: @escaping (Store<EachState, EachAction>) -> EachContent
77+
)
78+
where
79+
Data == [EachState],
80+
EachContent: View,
81+
Content == WithViewStore<
82+
[ID], (Data.Index, EachAction), ForEach<[(offset: Int, element: ID)], ID, EachContent>
83+
>
84+
{
85+
let data = store.state
86+
self.data = data
87+
self.content = {
88+
WithViewStore(store.scope(state: { $0.map { $0[keyPath: id] } })) { viewStore in
89+
ForEach(Array(viewStore.state.enumerated()), id: \.element) { index, _ in
90+
content(
91+
store.scope(
92+
state: { index < $0.endIndex ? $0[index] : data[index] },
93+
action: { (index, $0) }
4994
)
50-
}
95+
)
5196
}
5297
}
5398
}
99+
}
54100

55-
/// Initializes a structure that computes views on demand from a store on an array of data and an
56-
/// indexed action.
57-
///
58-
/// - Parameters:
59-
/// - store: A store on an array of data and an indexed action.
60-
/// - content: A function that can generate content given a store of an element.
61-
public init<EachContent>(
62-
_ store: Store<Data, (Data.Index, EachAction)>,
63-
content: @escaping (Store<EachState, EachAction>) -> EachContent
64-
)
65-
where
66-
Data == [EachState],
67-
EachContent: View,
68-
Content == WithViewStore<
69-
Data, (Data.Index, EachAction),
70-
ForEach<ContiguousArray<(Data.Index, EachState)>, ID, EachContent>
71-
>,
72-
EachState: Identifiable,
73-
EachState.ID == ID
74-
{
75-
self.init(store, id: \.id, content: content)
76-
}
77-
78-
/// Initializes a structure that computes views on demand from a store on a collection of data and
79-
/// an identified action.
80-
///
81-
/// - Parameters:
82-
/// - store: A store on an identified array of data and an identified action.
83-
/// - content: A function that can generate content given a store of an element.
84-
public init<EachContent: View>(
85-
_ store: Store<IdentifiedArray<ID, EachState>, (ID, EachAction)>,
86-
content: @escaping (Store<EachState, EachAction>) -> EachContent
87-
)
88-
where
89-
EachContent: View,
90-
Data == IdentifiedArray<ID, EachState>,
91-
Content == WithViewStore<
92-
IdentifiedArray<ID, EachState>, (ID, EachAction),
93-
ForEach<IdentifiedArray<ID, EachState>, ID, EachContent>
94-
>
95-
{
101+
/// Initializes a structure that computes views on demand from a store on an array of data and an
102+
/// indexed action.
103+
///
104+
/// - Parameters:
105+
/// - store: A store on an array of data and an indexed action.
106+
/// - content: A function that can generate content given a store of an element.
107+
public init<EachContent>(
108+
_ store: Store<Data, (Data.Index, EachAction)>,
109+
content: @escaping (Store<EachState, EachAction>) -> EachContent
110+
)
111+
where
112+
Data == [EachState],
113+
EachContent: View,
114+
Content == WithViewStore<
115+
[ID], (Data.Index, EachAction), ForEach<[(offset: Int, element: ID)], ID, EachContent>
116+
>,
117+
EachState: Identifiable,
118+
EachState.ID == ID
119+
{
120+
self.init(store, id: \.id, content: content)
121+
}
96122

97-
self.data = ViewStore(store, removeDuplicates: { _, _ in false }).state
98-
self.content = {
99-
WithViewStore(
100-
store,
101-
removeDuplicates: { lhs, rhs in
102-
guard lhs.id == rhs.id else { return false }
103-
guard lhs.count == rhs.count else { return false }
104-
return zip(lhs, rhs).allSatisfy { $0[keyPath: lhs.id] == $1[keyPath: rhs.id] }
105-
}
106-
) { viewStore in
107-
ForEach(viewStore.state, id: viewStore.id) { element in
108-
content(
109-
store.scope(
110-
state: { $0[id: element[keyPath: viewStore.id]] ?? element },
111-
action: { (element[keyPath: viewStore.id], $0) }
112-
)
123+
/// Initializes a structure that computes views on demand from a store on a collection of data and
124+
/// an identified action.
125+
///
126+
/// - Parameters:
127+
/// - store: A store on an identified array of data and an identified action.
128+
/// - content: A function that can generate content given a store of an element.
129+
public init<EachContent: View>(
130+
_ store: Store<IdentifiedArray<ID, EachState>, (ID, EachAction)>,
131+
content: @escaping (Store<EachState, EachAction>) -> EachContent
132+
)
133+
where
134+
EachContent: View,
135+
Data == IdentifiedArray<ID, EachState>,
136+
Content == WithViewStore<[ID], (ID, EachAction), ForEach<[ID], ID, EachContent>>
137+
{
138+
let data = store.state
139+
self.data = data
140+
self.content = {
141+
WithViewStore(store.scope(state: { $0.ids })) { viewStore in
142+
ForEach(viewStore.state, id: \.self) { id in
143+
content(
144+
store.scope(
145+
state: { $0[id: id] ?? data[id: id]! },
146+
action: { (id, $0) }
113147
)
114-
}
148+
)
115149
}
116150
}
117151
}
152+
}
118153

119-
public var body: some View {
120-
self.content()
121-
}
154+
public var body: some View {
155+
self.content()
122156
}
157+
}
123158
#endif

0 commit comments

Comments
 (0)