From 7194885749257b449ed23f6ae4df1e4bab4a556c Mon Sep 17 00:00:00 2001 From: Stephen Celis Date: Fri, 22 Aug 2025 12:59:36 -0700 Subject: [PATCH] Fix retain cycle regression from 1.21 --- Sources/ComposableArchitecture/Store.swift | 2 +- .../StoreTests.swift | 63 +++++++++++++++++++ 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/Sources/ComposableArchitecture/Store.swift b/Sources/ComposableArchitecture/Store.swift index 6ca64ec00239..9e15b0060418 100644 --- a/Sources/ComposableArchitecture/Store.swift +++ b/Sources/ComposableArchitecture/Store.swift @@ -339,7 +339,7 @@ public final class Store: _Store { .compactMap { [weak self] in (self?.currentState as? T)?._$id } .removeDuplicates() .dropFirst() - .sink { [weak self] _ in + .sink { [weak self, weak parent] _ in guard let scopeID = self?.scopeID else { return } parent?.removeChild(scopeID: scopeID) diff --git a/Tests/ComposableArchitectureTests/StoreTests.swift b/Tests/ComposableArchitectureTests/StoreTests.swift index aec017b7b7dd..6151fa700ebc 100644 --- a/Tests/ComposableArchitectureTests/StoreTests.swift +++ b/Tests/ComposableArchitectureTests/StoreTests.swift @@ -1338,6 +1338,69 @@ final class StoreTests: BaseTCATestCase { #expect(store.count == 1729) } } + + @Suite + struct ParentChildLifecycle { + @available(iOS 16, macOS 13, tvOS 16, watchOS 9, *) + @MainActor + @Test + func parentChildLifecycle() async throws { + weak var parentStore: StoreOf? + do { + let store = Store(initialState: Parent.State()) { + Parent() + } + parentStore = store + store.send(.presentButtonTapped) + guard let _ = store.scope(state: \.child, action: \.child) else { + Issue.record("Child is 'nil'") + return + } + } + #expect(parentStore == nil) + } + + @Reducer struct Child { + @ObservableState struct State { + var count = 0 + } + enum Action { + case incrementButtonTapped + } + var body: some Reducer { + Reduce { state, action in + switch action { + case .incrementButtonTapped: + state.count += 1 + return .none + } + } + } + } + @Reducer struct Parent { + @ObservableState struct State { + @Presents var child: Child.State? + } + enum Action { + case child(PresentationAction) + case presentButtonTapped + } + var body: some Reducer { + Reduce { state, action in + switch action { + case .child: + return .none + case .presentButtonTapped: + state.child = Child.State() + return .none + } + } + .ifLet(\.$child, action: \.child) { + Child() + } + } + } + } } #endif