Skip to content

Commit c2a27dc

Browse files
authored
Child store caching improvements (#2627)
* Use `ScopeID` for all cached child stores * wip * wip * wip * wip * wip * wip
1 parent 41f79c0 commit c2a27dc

File tree

9 files changed

+147
-123
lines changed

9 files changed

+147
-123
lines changed

Examples/Integration/IntegrationUITests/EnumTests.swift

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -84,13 +84,6 @@ final class EnumTests: BaseIntegrationTests {
8484
"""
8585
EnumView.body
8686
PresentationStoreOf<EnumView.Feature.Destination>.scope
87-
StoreOf<BasicsView.Feature>.scope
88-
StoreOf<BasicsView.Feature?>.scope
89-
StoreOf<BasicsView.Feature?>.scope
90-
StoreOf<BasicsView.Feature?>.scope
91-
StoreOf<EnumView.Feature.Destination>.scope
92-
StoreOf<EnumView.Feature.Destination>.scope
93-
StoreOf<EnumView.Feature.Destination?>.scope
9487
StoreOf<EnumView.Feature.Destination?>.scope
9588
StoreOf<EnumView.Feature.Destination?>.scope
9689
StoreOf<EnumView.Feature>.scope
@@ -118,7 +111,6 @@ final class EnumTests: BaseIntegrationTests {
118111
PresentationStoreOf<EnumView.Feature.Destination>.scope
119112
StoreOf<BasicsView.Feature>.init
120113
StoreOf<BasicsView.Feature>.init
121-
StoreOf<BasicsView.Feature>.scope
122114
StoreOf<BasicsView.Feature?>.deinit
123115
StoreOf<BasicsView.Feature?>.init
124116
StoreOf<BasicsView.Feature?>.init
@@ -128,7 +120,6 @@ final class EnumTests: BaseIntegrationTests {
128120
StoreOf<BasicsView.Feature?>.init
129121
StoreOf<BasicsView.Feature?>.scope
130122
StoreOf<BasicsView.Feature?>.scope
131-
StoreOf<BasicsView.Feature?>.scope
132123
StoreOf<EnumView.Feature.Destination>.scope
133124
StoreOf<EnumView.Feature.Destination>.scope
134125
StoreOf<EnumView.Feature.Destination?>.scope
@@ -237,17 +228,10 @@ final class EnumTests: BaseIntegrationTests {
237228
PresentationStoreOf<EnumView.Feature.Destination>.scope
238229
PresentationStoreOf<EnumView.Feature.Destination>.scope
239230
StoreOf<BasicsView.Feature>.scope
240-
StoreOf<BasicsView.Feature>.scope
241-
StoreOf<BasicsView.Feature?>.scope
242-
StoreOf<BasicsView.Feature?>.scope
243231
StoreOf<BasicsView.Feature?>.scope
244232
StoreOf<BasicsView.Feature?>.scope
245-
StoreOf<BasicsView.Feature?>.scope
246-
StoreOf<EnumView.Feature.Destination>.scope
247233
StoreOf<EnumView.Feature.Destination>.scope
248234
StoreOf<EnumView.Feature.Destination>.scope
249-
StoreOf<EnumView.Feature.Destination>.scope
250-
StoreOf<EnumView.Feature.Destination?>.scope
251235
StoreOf<EnumView.Feature.Destination?>.scope
252236
StoreOf<EnumView.Feature.Destination?>.scope
253237
StoreOf<EnumView.Feature.Destination?>.scope

Examples/Integration/IntegrationUITests/PresentationTests.swift

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ final class PresentationTests: BaseIntegrationTests {
99
self.app.buttons["iOS 16"].tap()
1010
self.app.buttons["Presentation"].tap()
1111
self.clearLogs()
12-
//SnapshotTesting.isRecording = true
12+
// SnapshotTesting.isRecording = true
1313
}
1414

1515
func testOptional() throws {
@@ -27,8 +27,6 @@ final class PresentationTests: BaseIntegrationTests {
2727
PresentationStoreOf<BasicsView.Feature>.scope
2828
StoreOf<BasicsView.Feature>.init
2929
StoreOf<BasicsView.Feature>.init
30-
StoreOf<BasicsView.Feature?>.deinit
31-
StoreOf<BasicsView.Feature?>.init
3230
StoreOf<BasicsView.Feature?>.init
3331
StoreOf<BasicsView.Feature?>.init
3432
StoreOf<BasicsView.Feature?>.init
@@ -80,10 +78,8 @@ final class PresentationTests: BaseIntegrationTests {
8078
PresentationStoreOf<BasicsView.Feature>.scope
8179
StoreOf<BasicsView.Feature>.deinit
8280
StoreOf<BasicsView.Feature>.deinit
83-
StoreOf<BasicsView.Feature>.scope
8481
StoreOf<BasicsView.Feature?>.deinit
85-
StoreOf<BasicsView.Feature?>.init
86-
StoreOf<BasicsView.Feature?>.scope
82+
StoreOf<BasicsView.Feature?>.deinit
8783
StoreOf<BasicsView.Feature?>.scope
8884
StoreOf<BasicsView.Feature?>.scope
8985
StoreOf<BasicsView.Feature?>.scope
@@ -110,8 +106,6 @@ final class PresentationTests: BaseIntegrationTests {
110106
PresentationStoreOf<BasicsView.Feature>.scope
111107
StoreOf<BasicsView.Feature>.init
112108
StoreOf<BasicsView.Feature>.init
113-
StoreOf<BasicsView.Feature?>.deinit
114-
StoreOf<BasicsView.Feature?>.init
115109
StoreOf<BasicsView.Feature?>.init
116110
StoreOf<BasicsView.Feature?>.init
117111
StoreOf<BasicsView.Feature?>.init
@@ -193,10 +187,8 @@ final class PresentationTests: BaseIntegrationTests {
193187
PresentationView.body
194188
StoreOf<BasicsView.Feature>.deinit
195189
StoreOf<BasicsView.Feature>.deinit
196-
StoreOf<BasicsView.Feature>.scope
197190
StoreOf<BasicsView.Feature?>.deinit
198-
StoreOf<BasicsView.Feature?>.init
199-
StoreOf<BasicsView.Feature?>.scope
191+
StoreOf<BasicsView.Feature?>.deinit
200192
StoreOf<BasicsView.Feature?>.scope
201193
StoreOf<BasicsView.Feature?>.scope
202194
StoreOf<BasicsView.Feature?>.scope

Sources/ComposableArchitecture/Store.swift

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ import SwiftUI
134134
public final class Store<State, Action> {
135135
private var bufferedActions: [Action] = []
136136
fileprivate var canCacheChildren = true
137-
fileprivate var children: [AnyHashable: AnyObject] = [:]
137+
fileprivate var children: [ScopeID<State, Action>: AnyObject] = [:]
138138
@_spi(Internals) public var effectCancellables: [UUID: AnyCancellable] = [:]
139139
var _isInvalidated = { false }
140140
private var isSending = false
@@ -389,7 +389,7 @@ public final class Store<State, Action> {
389389
) -> Store<ChildState, ChildAction> {
390390
self.scope(
391391
state: { $0[keyPath: state] },
392-
id: { _ in Scope(state: state, action: action) },
392+
id: self.id(state: state, action: action),
393393
action: { action($0) },
394394
isInvalid: nil,
395395
removeDuplicates: nil
@@ -465,7 +465,7 @@ public final class Store<State, Action> {
465465

466466
func scope<ChildState, ChildAction>(
467467
state toChildState: @escaping (State) -> ChildState,
468-
id: ((State) -> AnyHashable)?,
468+
id: ScopeID<State, Action>?,
469469
action fromChildAction: @escaping (ChildAction) -> Action,
470470
isInvalid: ((State) -> Bool)?,
471471
removeDuplicates isDuplicate: ((ChildState, ChildState) -> Bool)?
@@ -487,7 +487,7 @@ public final class Store<State, Action> {
487487
}
488488
}
489489

490-
fileprivate func invalidateChild(id: AnyHashable) {
490+
fileprivate func invalidateChild(id: ScopeID<State, Action>) {
491491
guard self.children.keys.contains(id) else { return }
492492
(self.children[id] as? any AnyStore)?.invalidate()
493493
self.children[id] = nil
@@ -745,12 +745,19 @@ public final class Store<State, Action> {
745745
StorePublisher(store: self, upstream: self.stateSubject)
746746
}
747747

748-
private struct Scope<ChildState, ChildAction>: Hashable {
749-
let state: KeyPath<State, ChildState>
750-
let action: CaseKeyPath<Action, ChildAction>
748+
func id<ChildState, ChildAction>(
749+
state: KeyPath<State, ChildState>,
750+
action: CaseKeyPath<Action, ChildAction>
751+
) -> ScopeID<State, Action> {
752+
ScopeID(state: state, action: action)
751753
}
752754
}
753755

756+
struct ScopeID<State, Action>: Hashable {
757+
let state: PartialKeyPath<State>
758+
let action: PartialCaseKeyPath<Action>
759+
}
760+
754761
extension Store: CustomDebugStringConvertible {
755762
public var debugDescription: String {
756763
storeTypeName(of: self)
@@ -944,7 +951,7 @@ extension Reducer {
944951
fileprivate func scope<ChildState, ChildAction>(
945952
store: Store<State, Action>,
946953
state toChildState: @escaping (State) -> ChildState,
947-
id: ((State) -> AnyHashable)?,
954+
id: ScopeID<State, Action>?,
948955
action fromChildAction: @escaping (ChildAction) -> Action,
949956
isInvalid: ((State) -> Bool)?,
950957
removeDuplicates isDuplicate: ((ChildState, ChildState) -> Bool)?
@@ -1017,7 +1024,7 @@ private protocol AnyScopedStoreReducer {
10171024
func scope<S, A, ChildState, ChildAction>(
10181025
store: Store<S, A>,
10191026
state toChildState: @escaping (S) -> ChildState,
1020-
id: ((S) -> AnyHashable)?,
1027+
id: ScopeID<S, A>?,
10211028
action fromChildAction: @escaping (ChildAction) -> A,
10221029
isInvalid: ((S) -> Bool)?,
10231030
removeDuplicates isDuplicate: ((ChildState, ChildState) -> Bool)?
@@ -1028,12 +1035,11 @@ extension ScopedStoreReducer: AnyScopedStoreReducer {
10281035
func scope<S, A, ChildState, ChildAction>(
10291036
store: Store<S, A>,
10301037
state toChildState: @escaping (S) -> ChildState,
1031-
id: ((S) -> AnyHashable)?,
1038+
id: ScopeID<S, A>?,
10321039
action fromChildAction: @escaping (ChildAction) -> A,
10331040
isInvalid: ((S) -> Bool)?,
10341041
removeDuplicates isDuplicate: ((ChildState, ChildState) -> Bool)?
10351042
) -> Store<ChildState, ChildAction> {
1036-
let id = id?(store.stateSubject.value)
10371043
if store.canCacheChildren,
10381044
let id = id,
10391045
let childStore = store.children[id] as? Store<ChildState, ChildAction>

Sources/ComposableArchitecture/SwiftUI/ForEachStore.swift

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -113,15 +113,11 @@ public struct ForEachStore<
113113
removeDuplicates: { areOrderedSetsDuplicates($0.ids, $1.ids) }
114114
) { viewStore in
115115
ForEach(viewStore.state, id: viewStore.state.id) { element in
116-
var element = element
117116
let id = element[keyPath: viewStore.state.id]
118117
content(
119118
store.scope(
120-
state: {
121-
element = $0[id: id] ?? element
122-
return element
123-
},
124-
id: { _ in id },
119+
state: { $0[id: id]! },
120+
id: store.id(state: \.[id: id]!, action: \.[id: id]),
125121
action: { .element(id: id, action: $0) },
126122
isInvalid: { !$0.ids.contains(id) },
127123
removeDuplicates: nil
@@ -173,15 +169,11 @@ public struct ForEachStore<
173169
removeDuplicates: { areOrderedSetsDuplicates($0.ids, $1.ids) }
174170
) { viewStore in
175171
ForEach(viewStore.state, id: viewStore.state.id) { element in
176-
var element = element
177172
let id = element[keyPath: viewStore.state.id]
178173
content(
179174
store.scope(
180-
state: {
181-
element = $0[id: id] ?? element
182-
return element
183-
},
184-
id: { _ in id },
175+
state: { $0[id: id]! },
176+
id: store.id(state: \.[id: id]!, action: \.[id: id]),
185177
action: { (id, $0) },
186178
isInvalid: { !$0.ids.contains(id) },
187179
removeDuplicates: nil
@@ -195,3 +187,13 @@ public struct ForEachStore<
195187
self.content
196188
}
197189
}
190+
191+
extension Case {
192+
fileprivate subscript<ID: Hashable, Action>(id id: ID) -> Case<Action>
193+
where Value == (id: ID, action: Action) {
194+
Case<Action>(
195+
embed: { (id: id, action: $0) },
196+
extract: { $0.id == id ? $0.action : nil }
197+
)
198+
}
199+
}

Sources/ComposableArchitecture/SwiftUI/IfLetStore.swift

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -36,23 +36,23 @@ public struct IfLetStore<State, Action, Content: View>: View {
3636
) where Content == _ConditionalContent<IfContent, ElseContent> {
3737
let store = store.scope(
3838
state: { $0 },
39-
id: nil,
39+
id: store.id(state: \.self, action: \.self),
4040
action: { $0 },
4141
isInvalid: { $0 == nil },
4242
removeDuplicates: nil
4343
)
4444
self.store = store
4545
let elseContent = elseContent()
4646
self.content = { viewStore in
47-
if var state = viewStore.state {
47+
if viewStore.state != nil {
4848
return ViewBuilder.buildEither(
4949
first: ifContent(
5050
store.scope(
51-
state: {
52-
state = $0 ?? state
53-
return state
54-
},
55-
action: { $0 }
51+
state: { $0! },
52+
id: store.id(state: \.!, action: \.self),
53+
action: { $0 },
54+
isInvalid: { $0 == nil },
55+
removeDuplicates: nil
5656
)
5757
)
5858
)
@@ -75,21 +75,21 @@ public struct IfLetStore<State, Action, Content: View>: View {
7575
) where Content == IfContent? {
7676
let store = store.scope(
7777
state: { $0 },
78-
id: nil,
78+
id: store.id(state: \.self, action: \.self),
7979
action: { $0 },
8080
isInvalid: { $0 == nil },
8181
removeDuplicates: nil
8282
)
8383
self.store = store
8484
self.content = { viewStore in
85-
if var state = viewStore.state {
85+
if viewStore.state != nil {
8686
return ifContent(
8787
store.scope(
88-
state: {
89-
state = $0 ?? state
90-
return state
91-
},
92-
action: { $0 }
88+
state: { $0! },
89+
id: store.id(state: \.!, action: \.self),
90+
action: { $0 },
91+
isInvalid: { $0 == nil },
92+
removeDuplicates: nil
9393
)
9494
)
9595
} else {
@@ -132,7 +132,7 @@ public struct IfLetStore<State, Action, Content: View>: View {
132132
@ViewBuilder else elseContent: @escaping () -> ElseContent
133133
) where Content == _ConditionalContent<IfContent, ElseContent> {
134134
self.init(
135-
store.scope(state: { $0.wrappedValue }, action: PresentationAction.presented),
135+
store.scope(state: \.wrappedValue, action: \.presented),
136136
then: ifContent,
137137
else: elseContent
138138
)
@@ -170,7 +170,7 @@ public struct IfLetStore<State, Action, Content: View>: View {
170170
@ViewBuilder then ifContent: @escaping (_ store: Store<State, Action>) -> IfContent
171171
) where Content == IfContent? {
172172
self.init(
173-
store.scope(state: { $0.wrappedValue }, action: PresentationAction.presented),
173+
store.scope(state: \.wrappedValue, action: \.presented),
174174
then: ifContent
175175
)
176176
}

Sources/ComposableArchitecture/SwiftUI/NavigationDestination.swift

Lines changed: 7 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,14 @@ extension View {
2020
store: Store<PresentationState<State>, PresentationAction<Action>>,
2121
@ViewBuilder destination: @escaping (_ store: Store<State, Action>) -> Destination
2222
) -> some View {
23-
self._navigationDestination(
23+
self.presentation(
2424
store: store,
25-
state: { $0 },
26-
action: { $0 },
27-
destination: destination
28-
)
25+
id: { $0.wrappedValue.map(NavigationDestinationID.init) }
26+
) { `self`, $item, destinationContent in
27+
self.navigationDestination(isPresented: $item.isPresent()) {
28+
destinationContent(destination)
29+
}
30+
}
2931
}
3032

3133
/// Associates a destination view with a store that can be used to push the view onto a
@@ -73,23 +75,6 @@ extension View {
7375
action fromDestinationAction: @escaping (_ destinationAction: DestinationAction) -> Action,
7476
@ViewBuilder destination: @escaping (_ store: Store<DestinationState, DestinationAction>) ->
7577
Destination
76-
) -> some View {
77-
self._navigationDestination(
78-
store: store,
79-
state: toDestinationState,
80-
action: fromDestinationAction,
81-
destination: destination
82-
)
83-
}
84-
85-
private func _navigationDestination<
86-
State, Action, DestinationState, DestinationAction, Destination: View
87-
>(
88-
store: Store<PresentationState<State>, PresentationAction<Action>>,
89-
state toDestinationState: @escaping (_ state: State) -> DestinationState?,
90-
action fromDestinationAction: @escaping (_ destinationAction: DestinationAction) -> Action,
91-
@ViewBuilder destination: @escaping (_ store: Store<DestinationState, DestinationAction>) ->
92-
Destination
9378
) -> some View {
9479
self.presentation(
9580
store: store,

Sources/ComposableArchitecture/SwiftUI/NavigationStackStore.swift

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -29,15 +29,11 @@ public struct NavigationStackStore<State, Action, Root: View, Destination: View>
2929
) {
3030
self.root = root()
3131
self.destination = { component in
32-
var state = component.element
33-
return destination(
32+
destination(
3433
store
3534
.scope(
36-
state: {
37-
state = $0[id: component.id] ?? state
38-
return state
39-
},
40-
id: { _ in component.id },
35+
state: { $0[id: component.id]! },
36+
id: store.id(state: \.[id: component.id]!, action: \.[id: component.id]),
4137
action: { .element(id: component.id, action: $0) },
4238
isInvalid: { !$0.ids.contains(component.id) },
4339
removeDuplicates: nil
@@ -69,15 +65,11 @@ public struct NavigationStackStore<State, Action, Root: View, Destination: View>
6965
) where Destination == SwitchStore<State, Action, D> {
7066
self.root = root()
7167
self.destination = { component in
72-
var state = component.element
73-
return SwitchStore(
68+
SwitchStore(
7469
store
7570
.scope(
76-
state: {
77-
state = $0[id: component.id] ?? state
78-
return state
79-
},
80-
id: { _ in component.id },
71+
state: { $0[id: component.id]! },
72+
id: store.id(state: \.[id: component.id]!, action: \.[id: component.id]),
8173
action: { .element(id: component.id, action: $0) },
8274
isInvalid: { !$0.ids.contains(component.id) },
8375
removeDuplicates: nil

0 commit comments

Comments
 (0)