diff --git a/stdlib/public/Observation/Sources/Observation/ObservationRegistrar.swift b/stdlib/public/Observation/Sources/Observation/ObservationRegistrar.swift index 3c356381bee63..58fd1ff827bf4 100644 --- a/stdlib/public/Observation/Sources/Observation/ObservationRegistrar.swift +++ b/stdlib/public/Observation/Sources/Observation/ObservationRegistrar.swift @@ -110,9 +110,27 @@ public struct ObservationRegistrar: Sendable { } } - internal mutating func cancelAll() { + internal mutating func deinitialize() -> (@Sendable () -> Void)? { + func extractSelf(_ ty: T.Type) -> AnyKeyPath { + return \T.self + } + + var tracker: (@Sendable () -> Void)? + lookupIteration: for (keyPath, ids) in lookups { + for id in ids { + if let found = observations[id]?.willSetTracker { + // convert the keyPath into its \Self.self version + let selfKp = _openExistential(type(of: keyPath).rootType, do: extractSelf) + tracker = { + found(selfKp) + } + break lookupIteration + } + } + } observations.removeAll() lookups.removeAll() + return tracker } internal mutating func willSet(keyPath: AnyKeyPath) -> [@Sendable (AnyKeyPath) -> Void] { @@ -157,8 +175,8 @@ public struct ObservationRegistrar: Sendable { state.withCriticalRegion { $0.cancel(id) } } - internal func cancelAll() { - state.withCriticalRegion { $0.cancelAll() } + internal func deinitialize() { + state.withCriticalRegion { $0.deinitialize() }?() } internal func willSet( @@ -189,7 +207,7 @@ public struct ObservationRegistrar: Sendable { } deinit { - context.cancelAll() + context.deinitialize() } } diff --git a/test/stdlib/Observation/Observable.swift b/test/stdlib/Observation/Observable.swift index 9fa23325805fc..81b3f7bf1cbc6 100644 --- a/test/stdlib/Observation/Observable.swift +++ b/test/stdlib/Observation/Observable.swift @@ -287,6 +287,22 @@ final class CowTest { var container = CowContainer() } +@Observable +final class DeinitTriggeredObserver { + var property: Int = 3 + var property2: Int = 4 + let deinitTrigger: () -> Void + + init(_ deinitTrigger: @escaping () -> Void) { + self.deinitTrigger = deinitTrigger + } + + deinit { + deinitTrigger() + } +} + + @main struct Validator { @MainActor @@ -511,6 +527,23 @@ struct Validator { expectEqual(subject.container.id, startId) } + suite.test("weak container observation") { + let changed = CapturedState(state: false) + let deinitialized = CapturedState(state: 0) + var test = DeinitTriggeredObserver { + deinitialized.state += 1 + } + withObservationTracking { [weak test] in + _blackHole(test?.property) + _blackHole(test?.property2) + } onChange: { + changed.state = true + } + test = DeinitTriggeredObserver { } + expectEqual(deinitialized.state, 1) // ensure only one invocation is done per deinitialization + expectEqual(changed.state, true) + } + runAllTests() } }