Skip to content

Commit fabd1b6

Browse files
committed
Safely unwrap ForEachStore rows (#471)
* Safely unwrap ForEachStore rows * Add feedback link
1 parent 5dd2eeb commit fabd1b6

File tree

1 file changed

+142
-141
lines changed

1 file changed

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

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-
/// }
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+
/// }
6060
@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
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
6565

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-
{
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+
{
8585
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) }
94-
)
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) }
9594
)
96-
}
95+
)
9796
}
9897
}
9998
}
99+
}
100100

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-
}
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+
}
122122

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) }
147-
)
148-
)
149-
}
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, IfLetStore<EachState, EachAction, EachContent?>>>
137+
{
138+
self.data = store.state
139+
self.content = {
140+
WithViewStore(store.scope(state: { $0.ids })) { viewStore in
141+
ForEach(viewStore.state, id: \.self) { id in
142+
// NB: We safely unwrap state here to avoid a potential crash where SwiftUI may
143+
// re-evaluate views for elements no longer in the collection.
144+
//
145+
// Feedback filed: https://gist.github.com/stephencelis/cdf85ae8dab437adc998fb0204ed9a6b
146+
IfLetStore(
147+
store.scope(state: { $0[id: id] }, action: { (id, $0) }),
148+
then: content
149+
)
150150
}
151151
}
152152
}
153+
}
153154

154-
public var body: some View {
155-
self.content()
156-
}
155+
public var body: some View {
156+
self.content()
157157
}
158+
}
158159
#endif

0 commit comments

Comments
 (0)