Skip to content

Commit 764de87

Browse files
More docs for WithViewStore and new observe argument (#1368)
* wip * wip * Update Performance.md * Update Performance.md * wip * wip * Update Performance.md Co-authored-by: Stephen Celis <[email protected]>
1 parent a777780 commit 764de87

File tree

3 files changed

+362
-21
lines changed

3 files changed

+362
-21
lines changed

Sources/ComposableArchitecture/Documentation.docc/Articles/Performance.md

Lines changed: 94 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -74,18 +74,102 @@ This means `AppView` does not actually need to observe any state changes. This v
7474
created a single time, whereas if we observed the store then it would re-compute every time a single
7575
thing changed in either the activity, search or profile child features.
7676

77-
If sometime in the future we do actually need some state from the store, we can create a localized
78-
"view state" struct that holds only the bare essentials of state that the view needs to do its
79-
job. For example, suppose the activity state holds an integer that represents the number of
80-
unread activities. Then we could observe changes to only that piece of state like so:
77+
If sometime in the future we do actually need some state from the store, we can start to observe
78+
only the bare essentials of state necessary for the view to do its job. For example, suppose that
79+
we need access to the currently selected tab in state:
80+
81+
```swift
82+
struct AppState {
83+
var activity: ActivityState
84+
var search: SearchState
85+
var profile: ProfileState
86+
var selectedTab: Tab
87+
enum Tab { case activity, search, profile }
88+
}
89+
```
90+
91+
Then we can observe this state so that we can construct a binding to `selectedTab` for the tab view:
92+
93+
```swift
94+
struct AppView: View {
95+
let store: Store<AppState, AppAction>
96+
97+
var body: some View {
98+
WithViewStore(self.store, observe: { $0 }) { viewStore in
99+
TabView(selection: viewStore.binding(send: AppAction.tabSelected) {
100+
ActivityView(
101+
store: self.store.scope(state: \.activity, action: AppAction.activity)
102+
)
103+
.tag(AppState.Tab.activity)
104+
SearchView(
105+
store: self.store.scope(state: \.search, action: AppAction.search)
106+
)
107+
.tag(AppState.Tab.search)
108+
ProfileView(
109+
store: self.store.scope(state: \.profile, action: AppAction.profile)
110+
)
111+
.tag(AppState.Tab.profile)
112+
}
113+
}
114+
}
115+
}
116+
```
117+
118+
However, this style of state observation is terribly inefficient since _every_ change to `AppState`
119+
will cause the view to re-compute even though the only piece of state we actually care about is
120+
the `selectedTab`. The reason we are observing too much state is because we use `observe: { $0 }`
121+
in the construction of the ``WithViewStore``, which means the view store will observe all of state.
122+
123+
To chisel away at the observed state you can provide a closure for that argument that plucks out
124+
the state the view needs. In this case the view only needs a single field:
125+
126+
```swift
127+
WithViewStore(self.store, observe: \.selectedTab) { viewStore in
128+
TabView(selection: viewStore.binding(send: AppAction.tabSelected) {
129+
// ...
130+
}
131+
}
132+
```
133+
134+
In the future, the view may need access to more state. For example, suppose `ActivityState` holds
135+
onto an `unreadCount` integer to represent how many new activities you have. There's no need to
136+
observe _all_ of `ActivityState` to get access to this one field. You can observe just the one
137+
field.
138+
139+
Technically you can do this by mapping your state into a tuple, but because tuples are not
140+
`Equatable` you will need to provide an explicit `removeDuplicates` argument:
141+
142+
```swift
143+
WithViewStore(
144+
self.store,
145+
observe: { (selectedTab: $0.selectedTab, unreadActivityCount: $0.activity.unreadCount) },
146+
removeDuplicates: ==
147+
) { viewStore in
148+
TabView(selection: viewStore.binding(\.selectedTab, send: AppAction.tabSelected) {
149+
ActivityView(
150+
store: self.store.scope(state: \.activity, action: AppAction.activity)
151+
)
152+
.tag(AppState.Tab.activity)
153+
.badge("\(viewStore.state)")
154+
155+
// ...
156+
}
157+
}
158+
```
159+
160+
Alternatively, and recommended, you can introduce a lightweight, equatable `ViewState` struct
161+
nested inside your view whose purpose is to transform the `Store`'s full state into the bare
162+
essentials of what the view needs:
81163

82164
```swift
83165
struct AppView: View {
84166
let store: Store<AppState, AppAction>
85167

86-
struct ViewState {
168+
struct ViewState: Equatable {
169+
let selectedTab: AppState.Tab
87170
let unreadActivityCount: Int
88171
init(state: AppState) {
172+
self.selectedTab = state.selectedTab
89173
self.unreadActivityCount = state.activity.unreadCount
90174
}
91175
}
@@ -106,15 +190,16 @@ struct AppView: View {
106190
}
107191
```
108192

109-
Now the `AppView` will re-compute its body only when `activity.unreadCount` changes. In particular,
110-
no changes to the search or profile features will cause the view to re-compute, and that greatly
111-
reduces how often the view must re-compute.
193+
This gives you maximum flexibilty in the future for adding new fields to `ViewState` without making
194+
your view convoluated.
112195

113196
This technique for reducing view re-computations is most effective towards the root of your app
114197
hierarchy and least effective towards the leaf nodes of your app. Root features tend to hold lots
115198
of state that its view does not need, such as child features, and leaf features tend to only hold
116199
what's necessary. If you are going to employ this technique you will get the most benefit by
117-
applying it to views closer to the root.
200+
applying it to views closer to the root. At leaf features and views that need access to most
201+
of the state, it is fine to continue using `observe: { $0 }` to observe all of the state in the
202+
store.
118203

119204
### CPU intensive calculations
120205

Sources/ComposableArchitecture/Documentation.docc/Articles/SwiftUIDeprecations.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,26 @@ Avoid using deprecated APIs in your app. Select a method to see the replacement
88

99
## Topics
1010

11+
### `WithViewStore`
12+
13+
- ``WithViewStore/init(_:content:file:line:)-1gjbi``
14+
- ``WithViewStore/init(_:content:file:line:)-2uj44``
15+
- ``WithViewStore/init(_:content:file:line:)-5vj3w``
16+
- ``WithViewStore/init(_:content:file:line:)-5zsmz``
17+
- ``WithViewStore/init(_:content:file:line:)-7kai``
18+
- ``WithViewStore/init(_:file:line:content:)-4xog0``
19+
- ``WithViewStore/init(_:file:line:content:)-55smh``
20+
- ``WithViewStore/init(_:file:line:content:)-7qkc1``
21+
- ``WithViewStore/init(_:file:line:content:)-8b21b``
22+
- ``WithViewStore/init(_:file:line:content:)-9b6e2``
23+
- ``WithViewStore/init(_:removeDuplicates:content:file:line:)-1lyhl``
24+
- ``WithViewStore/init(_:removeDuplicates:content:file:line:)-35xje``
25+
- ``WithViewStore/init(_:removeDuplicates:content:file:line:)-8zzun``
26+
- ``WithViewStore/init(_:removeDuplicates:content:file:line:)-9atby``
27+
- ``WithViewStore/init(_:removeDuplicates:file:line:content:)``
28+
- ``WithViewStore/Action``
29+
- ``WithViewStore/State``
30+
1131
### View State
1232

1333
- ``ActionSheetState``

0 commit comments

Comments
 (0)