Skip to content

Commit 873d1da

Browse files
mbrandonwmluisbrown
authored andcommitted
Use @StateObject for WithViewStore in iOS 14 (#1325)
* wip * Deprecate WithViewStore overloads * wip * wip * wip * wip * draft of state object for withviewstore * Revert "draft of state object for withviewstore" This reverts commit bf00da23fd1a56e1bdd4c4d58260f5251254c8e6. * wip * wip * wip * wip * wip * wip * wip * wip * fix availabililty * tryting to get debug * fix printing * wip * wip * fix flakey test * release builds * wip * wip * merge conflicts * wip Co-authored-by: Stephen Celis <[email protected]> (cherry picked from commit 1a53648704c8084370b7951c33de05cec3e3e7b8) # Conflicts: # Sources/ComposableArchitecture/SwiftUI/ForEachStore.swift # Sources/ComposableArchitecture/SwiftUI/WithViewStore.swift
1 parent cf29124 commit 873d1da

File tree

5 files changed

+1283
-908
lines changed

5 files changed

+1283
-908
lines changed

Sources/ComposableArchitecture/Internal/Deprecations.swift

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -708,7 +708,13 @@ extension Reducer {
708708
where
709709
Data == [EachState],
710710
Content == WithViewStore<
711-
[ID], (Data.Index, EachAction), ForEach<[(offset: Int, element: ID)], ID, EachContent>
711+
[ID], (Data.Index, EachAction),
712+
_ConditionalContent<
713+
AnyView,
714+
_ObservedObjectViewStore<
715+
[ID], (Int, EachAction), ForEach<[(offset: Int, element: ID)], ID, EachContent>
716+
>
717+
>
712718
>
713719
{
714720
let data = store.state
@@ -735,7 +741,13 @@ extension Reducer {
735741
where
736742
Data == [EachState],
737743
Content == WithViewStore<
738-
[ID], (Data.Index, EachAction), ForEach<[(offset: Int, element: ID)], ID, EachContent>
744+
[ID], (Data.Index, EachAction),
745+
_ConditionalContent<
746+
AnyView,
747+
_ObservedObjectViewStore<
748+
[ID], (Int, EachAction), ForEach<[(offset: Int, element: ID)], ID, EachContent>
749+
>
750+
>
739751
>,
740752
EachState: Identifiable,
741753
EachState.ID == ID
Lines changed: 128 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -1,137 +1,143 @@
11
#if canImport(SwiftUI)
2-
import OrderedCollections
3-
import SwiftUI
2+
import OrderedCollections
3+
import SwiftUI
44

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

80-
/// Initializes a structure that computes views on demand from a store on a collection of data and
81-
/// an identified action.
82-
///
83-
/// - Parameters:
84-
/// - store: A store on an identified array of data and an identified action.
85-
/// - content: A function that can generate content given a store of an element.
86-
public init<EachContent>(
87-
_ store: Store<IdentifiedArray<ID, EachState>, (ID, EachAction)>,
88-
@ViewBuilder content: @escaping (Store<EachState, EachAction>) -> EachContent
89-
)
90-
where
91-
Data == IdentifiedArray<ID, EachState>,
92-
Content == WithViewStore<
93-
OrderedSet<ID>, (ID, EachAction), ForEach<OrderedSet<ID>, ID, EachContent>
80+
/// Initializes a structure that computes views on demand from a store on a collection of data and
81+
/// an identified action.
82+
///
83+
/// - Parameters:
84+
/// - store: A store on an identified array of data and an identified action.
85+
/// - content: A function that can generate content given a store of an element.
86+
public init<EachContent>(
87+
_ store: Store<IdentifiedArray<ID, EachState>, (ID, EachAction)>,
88+
@ViewBuilder content: @escaping (Store<EachState, EachAction>) -> EachContent
89+
)
90+
where
91+
Data == IdentifiedArray<ID, EachState>,
92+
Content == WithViewStore<
93+
OrderedSet<ID>, (ID, EachAction),
94+
_ConditionalContent<
95+
AnyView,
96+
_ObservedObjectViewStore<
97+
OrderedSet<ID>, (ID, EachAction), ForEach<OrderedSet<ID>, ID, EachContent>
98+
>
9499
>
95-
{
100+
>
101+
{
96102
self.data = store.state
97-
self.content = {
98-
WithViewStore(
99-
store.scope(state: { $0.ids }),
100-
removeDuplicates: areOrderedSetsDuplicates
101-
) { viewStore in
102-
ForEach(viewStore.state, id: \.self) { id -> EachContent in
103-
// NB: We cache elements here to avoid a potential crash where SwiftUI may re-evaluate
104-
// views for elements no longer in the collection.
105-
//
106-
// Feedback filed: https://gist.github.com/stephencelis/cdf85ae8dab437adc998fb0204ed9a6b
103+
self.content = {
104+
WithViewStore(
105+
store.scope(state: { $0.ids }),
106+
removeDuplicates: areOrderedSetsDuplicates
107+
) { viewStore in
108+
ForEach(viewStore.state, id: \.self) { id -> EachContent in
109+
// NB: We cache elements here to avoid a potential crash where SwiftUI may re-evaluate
110+
// views for elements no longer in the collection.
111+
//
112+
// Feedback filed: https://gist.github.com/stephencelis/cdf85ae8dab437adc998fb0204ed9a6b
107113
var element = store.state[id: id]!
108-
return content(
109-
store.scope(
110-
state: {
111-
element = $0[id: id] ?? element
112-
return element
113-
},
114-
action: { (id, $0) }
115-
)
114+
return content(
115+
store.scope(
116+
state: {
117+
element = $0[id: id] ?? element
118+
return element
119+
},
120+
action: { (id, $0) }
116121
)
117-
}
122+
)
118123
}
119124
}
120125
}
126+
}
121127

122-
public var body: some View {
123-
self.content()
124-
}
128+
public var body: some View {
129+
self.content()
125130
}
131+
}
126132

127-
private func areOrderedSetsDuplicates<ID: Hashable>(lhs: OrderedSet<ID>, rhs: OrderedSet<ID>)
128-
-> Bool
129-
{
130-
var lhs = lhs
131-
var rhs = rhs
132-
if memcmp(&lhs, &rhs, MemoryLayout<OrderedSet<ID>>.size) == 0 {
133-
return true
134-
}
135-
return lhs == rhs
133+
private func areOrderedSetsDuplicates<ID: Hashable>(lhs: OrderedSet<ID>, rhs: OrderedSet<ID>)
134+
-> Bool
135+
{
136+
var lhs = lhs
137+
var rhs = rhs
138+
if memcmp(&lhs, &rhs, MemoryLayout<OrderedSet<ID>>.size) == 0 {
139+
return true
136140
}
141+
return lhs == rhs
142+
}
137143
#endif

0 commit comments

Comments
 (0)