From 9bbefad99356d7d9a5a658aaf594da784196d0e3 Mon Sep 17 00:00:00 2001 From: Dark-detsixE Date: Wed, 1 Jul 2026 23:19:09 +0800 Subject: [PATCH] Add View.onPreferenceChange --- .../PreferenceActionModifierExample.swift | 29 ++++ .../PreferenceSceneModifier.swift | 6 +- .../Preference/PreferenceActionModifier.swift | 124 ++++++++++++++++++ .../PreferenceWritingModifier.swift | 6 +- 4 files changed, 159 insertions(+), 6 deletions(-) create mode 100644 Example/Shared/ViewModifier/PreferenceActionModifierExample.swift create mode 100644 Sources/OpenSwiftUICore/Data/Preference/PreferenceActionModifier.swift diff --git a/Example/Shared/ViewModifier/PreferenceActionModifierExample.swift b/Example/Shared/ViewModifier/PreferenceActionModifierExample.swift new file mode 100644 index 000000000..3ad335fef --- /dev/null +++ b/Example/Shared/ViewModifier/PreferenceActionModifierExample.swift @@ -0,0 +1,29 @@ +// +// PreferenceActionModifierExample.swift +// Shared + +#if OPENSWIFTUI +import OpenSwiftUI +#else +import SwiftUI +#endif + +struct MyKey: PreferenceKey { + static let defaultValue = "" + + static func reduce(value: inout String, nextValue: () -> String) { + value = nextValue() + } +} + +struct PreferenceActionModifierExample: View { + var body: some View { + VStack { + Color.red + .preference(key: MyKey.self, value: "changed") + } + .onPreferenceChange(MyKey.self) { + print("onPreferenceChange: \($0)") + } + } +} diff --git a/Sources/OpenSwiftUI/Modifier/SceneModifier/PreferenceSceneModifier.swift b/Sources/OpenSwiftUI/Modifier/SceneModifier/PreferenceSceneModifier.swift index de6a908a4..e1559ccc7 100644 --- a/Sources/OpenSwiftUI/Modifier/SceneModifier/PreferenceSceneModifier.swift +++ b/Sources/OpenSwiftUI/Modifier/SceneModifier/PreferenceSceneModifier.swift @@ -16,9 +16,9 @@ extension _PreferenceWritingModifier: _SceneModifier { inputs: _SceneInputs, body: @escaping (_Graph, _SceneInputs) -> _SceneOutputs ) -> _SceneOutputs { - var inputs = inputs - inputs.preferences.remove(Key.self) - var outputs = body(_Graph(), inputs) + var newInputs = inputs + newInputs.preferences.remove(Key.self) + var outputs = body(_Graph(), newInputs) outputs.preferences .makePreferenceWriter( inputs: inputs.preferences, diff --git a/Sources/OpenSwiftUICore/Data/Preference/PreferenceActionModifier.swift b/Sources/OpenSwiftUICore/Data/Preference/PreferenceActionModifier.swift new file mode 100644 index 000000000..044872458 --- /dev/null +++ b/Sources/OpenSwiftUICore/Data/Preference/PreferenceActionModifier.swift @@ -0,0 +1,124 @@ +// +// PreferenceActionModifier.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete +// ID: 264234112339315C9A664F0B7F8B50C1 (SwiftUICore) + +import OpenAttributeGraphShims + +extension View { + /// Adds an action to perform when the specified preference key's value + /// changes. + /// + /// - Parameters: + /// - key: The key to monitor for value changes. + /// - action: The action to perform when the value for `key` changes. The + /// `action` closure passes the new value as its parameter. + /// + /// - Returns: A view that triggers `action` when the value for `key` + /// changes. + @inlinable + nonisolated public func onPreferenceChange( + _ key: K.Type = K.self, + perform action: @escaping (K.Value) -> Void + ) -> some View where K: PreferenceKey, K.Value: Equatable { + return modifier(_PreferenceActionModifier(action: action)) + } +} + +// MARK: - PreferenceActionModifier + +@frozen +public struct _PreferenceActionModifier: MultiViewModifier, PrimitiveViewModifier where K: PreferenceKey, K.Value: Equatable { + public var action: (_ value: K.Value) -> Void + + @inlinable + public init(action: @escaping (_ value: K.Value) -> Void) { + self.action = action + } + + nonisolated public static func _makeView( + modifier: _GraphValue, + inputs: _ViewInputs, + body: @escaping (_Graph, _ViewInputs) -> _ViewOutputs + ) -> _ViewOutputs { + var inputs = inputs + inputs.preferences.add(K.self) + let outputs = body(_Graph(), inputs) + guard let keyValue = outputs[K.self] else { + return outputs + } + let binder = Attribute(PreferenceBinder( + modifier: modifier.value, + keyValue: keyValue, + phase: inputs.viewPhase, + lastResetSeed: .zero, + lastValue: nil + )) + binder.flags = .transactional + return outputs + } +} + +@available(*, unavailable) +extension _PreferenceActionModifier: Sendable {} + +// MARK: - PreferenceBinder + +private struct PreferenceBinder: StatefulRule, AsyncAttribute where K: PreferenceKey, K.Value: Equatable { + @Attribute var modifier: _PreferenceActionModifier + @Attribute var keyValue: K.Value + @Attribute var phase: _GraphInputs.Phase + var cycleDetector: UpdateCycleDetector + var lastResetSeed: UInt32 + var lastValue: K.Value? + + init( + modifier: Attribute<_PreferenceActionModifier>, + keyValue: Attribute, + phase: Attribute<_GraphInputs.Phase>, + cycleDetector: UpdateCycleDetector = .init(), + lastResetSeed: UInt32, + lastValue: K.Value? + ) { + self._modifier = modifier + self._keyValue = keyValue + self._phase = phase + self.cycleDetector = cycleDetector + self.lastResetSeed = lastResetSeed + self.lastValue = lastValue + } + + typealias Value = Void + + mutating func updateValue() { + if lastResetSeed != phase.resetSeed { + lastResetSeed = phase.resetSeed + cycleDetector.reset() + lastValue = nil + } + let (newValue, changed) = $keyValue.changedValue() + guard changed || (lastValue == nil && _SemanticFeature_v6_1.isEnabled) else { + return + } + guard lastValue != newValue else { + return + } + lastValue = newValue + + guard cycleDetector.dispatch( + label: "Bound preference \(K.self)", + isDebug: true + ) else { + return + } + let action = Graph.withoutUpdate { + modifier.action + } + Update.enqueueAction(reason: nil) { + action(newValue) + } + } +} diff --git a/Sources/OpenSwiftUICore/Data/Preference/PreferenceWritingModifier.swift b/Sources/OpenSwiftUICore/Data/Preference/PreferenceWritingModifier.swift index c0bf0206a..0e7de3ac7 100644 --- a/Sources/OpenSwiftUICore/Data/Preference/PreferenceWritingModifier.swift +++ b/Sources/OpenSwiftUICore/Data/Preference/PreferenceWritingModifier.swift @@ -30,9 +30,9 @@ public struct _PreferenceWritingModifier: ViewModifier, MultiViewModifier, inputs: _ViewInputs, body: @escaping (_Graph, _ViewInputs) -> _ViewOutputs ) -> _ViewOutputs { - var inputs = inputs - inputs.preferences.remove(Key.self) - var outputs = body(_Graph(), inputs) + var newInputs = inputs + newInputs.preferences.remove(Key.self) + var outputs = body(_Graph(), newInputs) outputs.preferences .makePreferenceWriter( inputs: inputs.preferences,