From 4237f0fae33d481fb7ca4fa7884c5113e9a6ac3d Mon Sep 17 00:00:00 2001 From: Kyle Date: Thu, 2 Oct 2025 19:22:22 +0800 Subject: [PATCH 1/3] Add EnvironmentKeyWritingModifier --- .../EnvironmentKeyWritingModifier.swift | 139 ++++++++++++++++++ .../_EnvironmentKeyWritingModifier.swift | 34 ----- 2 files changed, 139 insertions(+), 34 deletions(-) create mode 100644 Sources/OpenSwiftUICore/Data/Environment/EnvironmentKeyWritingModifier.swift delete mode 100644 Sources/OpenSwiftUICore/Modifier/ViewModifier/_EnvironmentKeyWritingModifier.swift diff --git a/Sources/OpenSwiftUICore/Data/Environment/EnvironmentKeyWritingModifier.swift b/Sources/OpenSwiftUICore/Data/Environment/EnvironmentKeyWritingModifier.swift new file mode 100644 index 000000000..24d56d73f --- /dev/null +++ b/Sources/OpenSwiftUICore/Data/Environment/EnvironmentKeyWritingModifier.swift @@ -0,0 +1,139 @@ +// +// EnvironmentKeyWritingModifier.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete +// ID: 3B04936F6043A290A3E53AE94FE09355 (SwiftUICore) + +import OpenAttributeGraphShims + +// MARK: EnvironmentKeyWritingModifier + +/// A modifier that sets a value for an environment keyPath. +@available(OpenSwiftUI_v1_0, *) +@frozen +public struct _EnvironmentKeyWritingModifier: ViewModifier, _GraphInputsModifier, PrimitiveViewModifier { + + /// The environment keyPath to set. + public var keyPath: WritableKeyPath + + /// The environment value to set for `keyPath`. + public var value: Value + + /// Creates an instance that sets a `value` for an environment `keyPath`. + @inlinable + public init(keyPath: WritableKeyPath, value: Value) { + self.keyPath = keyPath + self.value = value + } + + public static func _makeInputs( + modifier: _GraphValue, + inputs: inout _GraphInputs + ) { + let childEnvironment = Attribute( + ChildEnvironment( + modifier: modifier.value, + env: inputs.environment + ) + ) + inputs.environment = childEnvironment + } +} + +@available(*, unavailable) +extension _EnvironmentKeyWritingModifier: Sendable {} + +@available(OpenSwiftUI_v1_0, *) +extension View { + + /// Sets the environment value of the specified key path to the given value. + /// + /// Use this modifier to set one of the writable properties of the + /// ``EnvironmentValues`` structure, including custom values that you + /// create. For example, you can set the value associated with the + /// ``EnvironmentValues/truncationMode`` key: + /// + /// MyView() + /// .environment(\.truncationMode, .head) + /// + /// You then read the value inside `MyView` or one of its descendants + /// using the ``Environment`` property wrapper: + /// + /// struct MyView: View { + /// @Environment(\.truncationMode) var truncationMode: Text.TruncationMode + /// + /// var body: some View { ... } + /// } + /// + /// OpenSwiftUI provides dedicated view modifiers for setting most + /// environment values, like the ``View/truncationMode(_:)`` + /// modifier which sets the ``EnvironmentValues/truncationMode`` value: + /// + /// MyView() + /// .truncationMode(.head) + /// + /// Prefer the dedicated modifier when available, and offer your own when + /// defining custom environment values, as described in + /// ``Entry()``. + /// + /// This modifier affects the given view, + /// as well as that view's descendant views. It has no effect + /// outside the view hierarchy on which you call it. + /// + /// - Parameters: + /// - keyPath: A key path that indicates the property of the + /// ``EnvironmentValues`` structure to update. + /// - value: The new value to set for the item specified by `keyPath`. + /// + /// - Returns: A view that has the given value set in its environment. + @inlinable + nonisolated public func environment(_ keyPath: WritableKeyPath, _ value: V) -> some View { + modifier(_EnvironmentKeyWritingModifier(keyPath: keyPath, value: value)) + } +} + +// MARK: - ChildEnvironment + +private struct ChildEnvironment: StatefulRule, AsyncAttribute, CustomStringConvertible { + @Attribute var modifier: _EnvironmentKeyWritingModifier + @Attribute var env: EnvironmentValues + var oldModifier: _EnvironmentKeyWritingModifier? + + init( + modifier: Attribute<_EnvironmentKeyWritingModifier>, + env: Attribute, + oldModifier: _EnvironmentKeyWritingModifier? = nil + ) { + self._modifier = modifier + self._env = env + self.oldModifier = oldModifier + } + + var description: String { + "EnvironmentWriting: \(Value.self)" + } + + typealias Value = EnvironmentValues + + mutating func updateValue() { + let (modifier, modifierChanged) = $modifier.changedValue() + let (environment, environmentChanged) = $env.changedValue() + let modifierValueChanged: Bool + if !environmentChanged, modifierChanged { + modifierValueChanged = oldModifier.map { oldModifier in + !(oldModifier.keyPath == modifier.keyPath && compareValues(oldModifier.value, modifier.value, mode: .equatableUnlessPOD)) + } ?? true + } else { + modifierValueChanged = false + } + + if environmentChanged || modifierValueChanged || !hasValue { + var env = environment + env[keyPath: modifier.keyPath] = modifier.value + value = env + oldModifier = modifier + } + } +} diff --git a/Sources/OpenSwiftUICore/Modifier/ViewModifier/_EnvironmentKeyWritingModifier.swift b/Sources/OpenSwiftUICore/Modifier/ViewModifier/_EnvironmentKeyWritingModifier.swift deleted file mode 100644 index c43b93878..000000000 --- a/Sources/OpenSwiftUICore/Modifier/ViewModifier/_EnvironmentKeyWritingModifier.swift +++ /dev/null @@ -1,34 +0,0 @@ -// -// _EnvironmentKeyWritingModifier.swift -// OpenSwiftUI -// -// Audited for 3.5.2 -// Status: WIP - -@frozen -public struct _EnvironmentKeyWritingModifier: ViewModifier, _GraphInputsModifier, PrimitiveViewModifier { - public var keyPath: WritableKeyPath - public var value: Value - - @inlinable - public init(keyPath: WritableKeyPath, value: Value) { - self.keyPath = keyPath - self.value = value - } - - public static func _makeInputs(modifier: _GraphValue<_EnvironmentKeyWritingModifier>, inputs: inout _GraphInputs) { - // TODO - } - - -} - -@available(*, unavailable) -extension _EnvironmentKeyWritingModifier: Sendable {} - -extension View { - @inlinable - nonisolated public func environment(_ keyPath: WritableKeyPath, _ value: V) -> some View { - modifier(_EnvironmentKeyWritingModifier(keyPath: keyPath, value: value)) - } -} From 4808106af7d13248240baf0ed2ac895e2c0cae6d Mon Sep 17 00:00:00 2001 From: Kyle Date: Thu, 2 Oct 2025 19:22:41 +0800 Subject: [PATCH 2/3] Update EnvironmentKeyTransformModifier --- .../EnvironmentKeyTransformModifier.swift | 107 ++++++++++-------- 1 file changed, 60 insertions(+), 47 deletions(-) diff --git a/Sources/OpenSwiftUICore/Data/Environment/EnvironmentKeyTransformModifier.swift b/Sources/OpenSwiftUICore/Data/Environment/EnvironmentKeyTransformModifier.swift index ed4b21340..29df6407b 100644 --- a/Sources/OpenSwiftUICore/Data/Environment/EnvironmentKeyTransformModifier.swift +++ b/Sources/OpenSwiftUICore/Data/Environment/EnvironmentKeyTransformModifier.swift @@ -28,82 +28,95 @@ public struct _EnvironmentKeyTransformModifier: ViewModifier, _GraphInput self.transform = transform } - public static func _makeInputs(modifier: _GraphValue, inputs: inout _GraphInputs) { - let childEnvironment = ChildEnvironment( - modifier: modifier.value, - environment: inputs.cachedEnvironment.wrappedValue.environment, - oldValue: nil, - oldKeyPath: nil + public static func _makeInputs( + modifier: _GraphValue, + inputs: inout _GraphInputs + ) { + let childEnvironment = Attribute( + ChildEnvironment( + modifier: modifier.value, + environment: inputs.environment + ) ) - inputs.environment = Attribute(childEnvironment) + inputs.environment = childEnvironment } } +@available(*, unavailable) +extension _EnvironmentKeyTransformModifier: Sendable {} + +extension View { + /// Transforms the environment value of the specified key path with the + /// given function. + @inlinable + nonisolated public func transformEnvironment( + _ keyPath: WritableKeyPath, + transform: @escaping (inout V) -> Void + ) -> some View { + modifier(_EnvironmentKeyTransformModifier( + keyPath: keyPath, + transform: transform + )) + } +} + +// MARK: - ChildEnvironment [6.5.4] + private struct ChildEnvironment: StatefulRule, AsyncAttribute, CustomStringConvertible { - @Attribute private var modifier: _EnvironmentKeyTransformModifier - @Attribute private var environment: EnvironmentValues - private var oldValue: Value? - private var oldKeyPath: WritableKeyPath? - + @Attribute var modifier: _EnvironmentKeyTransformModifier + @Attribute var environment: EnvironmentValues + var oldValue: Value? + var oldKeyPath: WritableKeyPath? + init( modifier: Attribute<_EnvironmentKeyTransformModifier>, environment: Attribute, - oldValue: Value?, - oldKeyPath: WritableKeyPath? + oldValue: Value? = nil, + oldKeyPath: WritableKeyPath? = nil ) { _modifier = modifier _environment = environment self.oldValue = oldValue self.oldKeyPath = oldKeyPath } - + var description: String { "EnvironmentTransform: EnvironmentValues" } - + typealias Value = EnvironmentValues - + mutating func updateValue() { - var (environment, environmentChanged) = _environment.changedValue() - let keyPath = modifier.keyPath - var newValue = environment[keyPath: keyPath] + let (environment, environmentChanged) = $environment.changedValue() + let newKeyPath = modifier.keyPath + var newValue = environment[keyPath: newKeyPath] $modifier.syncMainIfReferences { modifier in withObservation { modifier.transform(&newValue) } } - guard !environmentChanged, - let valueChanged = oldValue.map({ compareValues($0, newValue, mode: .equatableUnlessPOD) }), !valueChanged, - let keyPathChanged = oldKeyPath.map({ $0 == keyPath }), !keyPathChanged, - hasValue - else { - environment[keyPath: keyPath] = newValue - value = environment + let valueChanged = if !environmentChanged { + // FIXME: The map logic is buggy + oldValue.map{ compareValues($0, newValue, mode: .equatableUnlessPOD) } ?? true + } else { + true + } + let keyPathChanged = if !valueChanged { + // FIXME: The map logic is buggy + oldKeyPath.map({ $0 == newKeyPath }) ?? true + } else { + true + } + if environmentChanged || valueChanged || keyPathChanged || !hasValue { + var env = environment + env[keyPath: newKeyPath] = newValue + value = env oldValue = newValue - oldKeyPath = keyPath - return + oldKeyPath = newKeyPath } } } -@available(*, unavailable) -extension _EnvironmentKeyTransformModifier: Sendable {} - -extension View { - /// Transforms the environment value of the specified key path with the - /// given function. - @inlinable - nonisolated public func transformEnvironment( - _ keyPath: WritableKeyPath, - transform: @escaping (inout V) -> Void - ) -> some View { - modifier(_EnvironmentKeyTransformModifier( - keyPath: keyPath, - transform: transform - )) - } -} - // MARK: - EnvironmentModifier package protocol EnvironmentModifier: _GraphInputsModifier { From de1add4cd1d967939fc5494dcd08d3143e156fc4 Mon Sep 17 00:00:00 2001 From: Kyle Date: Thu, 2 Oct 2025 19:35:40 +0800 Subject: [PATCH 3/3] Add test case --- ...yTransformModifierCompatibilityTests.swift | 39 +++++++++++++++++++ ...KeyWritingModifierCompatibilityTests.swift | 39 +++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 Tests/OpenSwiftUICompatibilityTests/Data/Environment/EnvironmentKeyTransformModifierCompatibilityTests.swift create mode 100644 Tests/OpenSwiftUICompatibilityTests/Data/Environment/EnvironmentKeyWritingModifierCompatibilityTests.swift diff --git a/Tests/OpenSwiftUICompatibilityTests/Data/Environment/EnvironmentKeyTransformModifierCompatibilityTests.swift b/Tests/OpenSwiftUICompatibilityTests/Data/Environment/EnvironmentKeyTransformModifierCompatibilityTests.swift new file mode 100644 index 000000000..1f1714957 --- /dev/null +++ b/Tests/OpenSwiftUICompatibilityTests/Data/Environment/EnvironmentKeyTransformModifierCompatibilityTests.swift @@ -0,0 +1,39 @@ +// +// EnvironmentKeyTransformModifierCompatibilityTests.swift +// OpenSwiftUICompatibilityTests + +import OpenSwiftUITestsSupport +import Testing + +extension EnvironmentValues { + @Entry fileprivate var customValue = 0 +} + +@MainActor +struct EnvironmentKeyTransformModifierCompatibilityTests { + @Test + func environmentKeyTransform() async throws { + struct ContentView: View { + var body: some View { + Subview() + .transformEnvironment(\.customValue) { $0 = 1 } + } + } + + struct Subview: View { + @Environment(\.customValue) private var value + + var body: some View { + Color.red + .onAppear { + #expect(value == 1) + } + } + } + try await triggerLayoutWithWindow(expectedCount: 0) { _ in + PlatformHostingController( + rootView: ContentView() + ) + } + } +} diff --git a/Tests/OpenSwiftUICompatibilityTests/Data/Environment/EnvironmentKeyWritingModifierCompatibilityTests.swift b/Tests/OpenSwiftUICompatibilityTests/Data/Environment/EnvironmentKeyWritingModifierCompatibilityTests.swift new file mode 100644 index 000000000..288447141 --- /dev/null +++ b/Tests/OpenSwiftUICompatibilityTests/Data/Environment/EnvironmentKeyWritingModifierCompatibilityTests.swift @@ -0,0 +1,39 @@ +// +// EnvironmentKeyWritingModifierCompatibilityTests.swift +// OpenSwiftUICompatibilityTests + +import OpenSwiftUITestsSupport +import Testing + +extension EnvironmentValues { + @Entry fileprivate var customValue = 0 +} + +@MainActor +struct EnvironmentKeyWritingModifierCompatibilityTests { + @Test + func environmentKeyWriting() async throws { + struct ContentView: View { + var body: some View { + Subview() + .environment(\.customValue, 1) + } + } + + struct Subview: View { + @Environment(\.customValue) private var value + + var body: some View { + Color.red + .onAppear { + #expect(value == 1) + } + } + } + try await triggerLayoutWithWindow(expectedCount: 0) { _ in + PlatformHostingController( + rootView: ContentView() + ) + } + } +}