Skip to content

Commit 7e3cd58

Browse files
committed
Clear child store from parent when it is invalidated.
1 parent b914abb commit 7e3cd58

File tree

2 files changed

+209
-6
lines changed

2 files changed

+209
-6
lines changed

Examples/CaseStudies/SwiftUICaseStudies/CaseStudiesApp.swift

Lines changed: 174 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,180 @@ import SwiftUI
55
struct CaseStudiesApp: App {
66
var body: some Scene {
77
WindowGroup {
8-
RootView()
8+
//RootView()
9+
ContentView()
910
}
1011
}
1112
}
13+
14+
15+
import SwiftUI
16+
import ComposableArchitecture
17+
18+
19+
//@inlinable
20+
//public func _$isIdentityEqual<ID, T: ObservableState>(
21+
// _ lhs: IdentifiedArray<ID, T>,
22+
// _ rhs: IdentifiedArray<ID, T>
23+
//) -> Bool {
24+
// false
25+
//}
26+
27+
@Reducer
28+
struct MainFeature {
29+
@ObservableState
30+
struct State {
31+
var childArray: IdentifiedArrayOf<ChildFeature.State>
32+
var childSolo: ChildFeature.State?
33+
34+
init() {
35+
childSolo = ChildFeature.State(id: "1", count: 0)
36+
// It's important to start with non empty array.
37+
childArray = [ChildFeature.State(id: "1", count: 0)]
38+
}
39+
}
40+
41+
enum Action {
42+
case setAnotherIDs
43+
case getBackToInitialIDsWithAnotherCount
44+
case resetToInitialIDs
45+
case childArray(IdentifiedActionOf<ChildFeature>)
46+
case childSolo(ChildFeature.Action)
47+
}
48+
49+
var body: some ReducerOf<Self> {
50+
Reduce<State, Action> { state, action in
51+
switch action {
52+
case .setAnotherIDs:
53+
// Update ID
54+
state.childSolo = ChildFeature.State(id: "2", count: 0)
55+
state.childArray = [ChildFeature.State(id: "2", count: 0)]
56+
return .none
57+
58+
case .getBackToInitialIDsWithAnotherCount:
59+
// Go back to the initial ID with a different count
60+
state.childSolo = ChildFeature.State(id: "1", count: 10)
61+
state.childArray = [ChildFeature.State(id: "1", count: 10)]
62+
return .none
63+
64+
case .resetToInitialIDs:
65+
// Set the same ID as in the previous action, but with a count of 0
66+
// After this step, the child stores in childArray stopped working.
67+
state.childSolo = ChildFeature.State(id: "1", count: 0)
68+
state.childArray = [ChildFeature.State(id: "1", count: 0)]
69+
return .none
70+
71+
case .childArray:
72+
return .none
73+
74+
case .childSolo:
75+
return .none
76+
}
77+
}
78+
.ifLet(\.childSolo, action: \.childSolo) {
79+
ChildFeature()
80+
}
81+
.forEach(\.childArray, action: \.childArray) {
82+
ChildFeature()
83+
}
84+
}
85+
}
86+
87+
@Reducer
88+
struct ChildFeature {
89+
@ObservableState
90+
struct State: Identifiable {
91+
let id: String
92+
var count: Int
93+
}
94+
95+
enum Action {
96+
case plus
97+
case minus
98+
}
99+
100+
var body: some ReducerOf<Self> {
101+
Reduce { state, action in
102+
switch action {
103+
case .plus:
104+
state.count += 1
105+
return .none
106+
107+
case .minus:
108+
state.count -= 1
109+
return .none
110+
}
111+
}
112+
}
113+
}
114+
115+
struct ChildView: View {
116+
let store: StoreOf<ChildFeature>
117+
118+
var body: some View {
119+
VStack {
120+
Text("id: " + store.id)
121+
122+
HStack {
123+
Button("Minus") {
124+
store.send(.minus)
125+
}
126+
127+
Text(store.count.description)
128+
129+
Button("Plus") {
130+
store.send(.plus)
131+
}
132+
}
133+
.frame(height: 50)
134+
.buttonStyle(.borderedProminent)
135+
}
136+
}
137+
}
138+
139+
struct MainView: View {
140+
let store: StoreOf<MainFeature>
141+
142+
var body: some View {
143+
ScrollView {
144+
VStack {
145+
Button("setAnotherIDs") {
146+
store.send(.setAnotherIDs)
147+
}
148+
149+
Button("getBackToInitialIDsWithAnotherCount") {
150+
store.send(.getBackToInitialIDsWithAnotherCount)
151+
}
152+
153+
Button("resetToInitialIDs (not working)") {
154+
store.send(.resetToInitialIDs)
155+
}
156+
157+
Color.red.frame(height: 10)
158+
159+
// Text("Child solo:")
160+
// if let childStore = store.scope(state: \.childSolo, action: \.childSolo) {
161+
// ChildView(store: childStore)
162+
// }
163+
164+
Color.red.frame(height: 10)
165+
166+
Text("Child array:")
167+
ForEach(
168+
store.scope(state: \.childArray, action: \.childArray)
169+
) { childStore in
170+
ChildView(store: childStore)
171+
}
172+
}
173+
}
174+
}
175+
}
176+
177+
struct ContentView: View {
178+
@State var mainStore: StoreOf<MainFeature> = Store(initialState: MainFeature.State()) {
179+
MainFeature()._printChanges()
180+
}
181+
var body: some View {
182+
MainView(store: mainStore)
183+
}
184+
}

Sources/ComposableArchitecture/Store.swift

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -92,15 +92,35 @@ import SwiftUI
9292
/// Instead, stores should be observed through Swift's Observation framework (or the Perception
9393
/// package when targeting iOS <17) by applying the ``ObservableState()`` macro to your feature's
9494
/// state.
95+
96+
97+
@MainActor
98+
protocol _Store: AnyObject {
99+
var allChildren: [AnyObject] { get }
100+
func removeChild(_ store: AnyObject)
101+
}
102+
103+
95104
@dynamicMemberLookup
96105
#if swift(<5.10)
97106
@MainActor(unsafe)
98107
#else
99108
@preconcurrency@MainActor
100109
#endif
101-
public final class Store<State, Action> {
110+
public final class Store<State, Action>: _Store {
102111
var children: [ScopeID<State, Action>: AnyObject] = [:]
103112

113+
var allChildren: [AnyObject] {
114+
Array(children.values)
115+
}
116+
func removeChild(_ store: AnyObject) {
117+
for (key, child) in children {
118+
guard child === store
119+
else { continue }
120+
children.removeValue(forKey: key)
121+
}
122+
}
123+
104124
let core: any Core<State, Action>
105125
@_spi(Internals) public var effectCancellables: [UUID: AnyCancellable] { core.effectCancellables }
106126

@@ -266,7 +286,7 @@ public final class Store<State, Action> {
266286
let id,
267287
let child = children[id] as? Store<ChildState, ChildAction>
268288
else {
269-
let child = Store<ChildState, ChildAction>(core: childCore())
289+
let child = Store<ChildState, ChildAction>(core: childCore(), parent: self)
270290
if core.canStoreCacheChildren, let id {
271291
children[id] = child
272292
}
@@ -313,14 +333,24 @@ public final class Store<State, Action> {
313333
core.send(action)
314334
}
315335

316-
private init(core: some Core<State, Action>) {
336+
private weak var parent: (any _Store)?
337+
338+
private init(core: some Core<State, Action>, parent: (any _Store)?) {
317339
defer { Logger.shared.log("\(storeTypeName(of: self)).init") }
318340
self.core = core
341+
self.parent = parent
319342

320343
if let stateType = State.self as? any ObservableState.Type {
321344
func subscribeToDidSet<T: ObservableState>(_ type: T.Type) -> AnyCancellable {
322345
return core.didSet
323-
.prefix { [weak self] _ in self?.core.isInvalid != true }
346+
.prefix { [weak self] _ in
347+
guard let self else { return false }
348+
let isInvalid = self.core.isInvalid
349+
if isInvalid {
350+
self.parent?.removeChild(self)
351+
}
352+
return !isInvalid
353+
}
324354
.compactMap { [weak self] in (self?.currentState as? T)?._$id }
325355
.removeDuplicates()
326356
.dropFirst()
@@ -337,7 +367,7 @@ public final class Store<State, Action> {
337367
initialState: R.State,
338368
reducer: R
339369
) {
340-
self.init(core: RootCore(initialState: initialState, reducer: reducer))
370+
self.init(core: RootCore(initialState: initialState, reducer: reducer), parent: nil)
341371
}
342372

343373
/// A publisher that emits when state changes.

0 commit comments

Comments
 (0)