Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions Example/Modules/Platform/cocoa/SwiftUI_SPI.swiftinterface
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,23 @@ import QuartzCore
import Swift
@_exported import SwiftUI

@available(iOS 18.2, macOS 15.2, *)
@frozen public struct TransactionalPreferenceActionModifier<Key> : SwiftUI.ViewModifier where Key : SwiftUI.PreferenceKey, Key.Value : Swift.Equatable {

@augmentcode augmentcode Bot Jul 15, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@available(iOS 18.2, macOS 15.2, *) makes this symbol effectively available on tvOS/watchOS/visionOS at all versions; consider adding explicit availability for the other Apple platforms (or marking them unavailable) to avoid cross-platform build mismatches. (Also applies to the similar availability usage in the transactional example/test in this PR.)

Severity: medium

Other Locations
  • Example/Shared/ViewModifier/TransactionalPreferenceActionModifierExample.swift:14
  • Example/OpenSwiftUIUITests/Modifier/ViewModifier/PreferenceActionModifierUITests.swift:50

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

public var action: (Key.Value, SwiftUI.Transaction) -> Swift.Void
@_alwaysEmitIntoClient public init(action: @escaping (Key.Value, SwiftUI.Transaction) -> Swift.Void) {
self.action = action
}
nonisolated public static func _makeView(modifier: SwiftUI._GraphValue<SwiftUI_SPI.TransactionalPreferenceActionModifier<Key>>, inputs: SwiftUI._ViewInputs, body: @escaping (SwiftUI._Graph, SwiftUI._ViewInputs) -> SwiftUI._ViewOutputs) -> SwiftUI._ViewOutputs
public typealias Body = Swift.Never
}

@available(iOS 18.2, macOS 15.2, *)
extension SwiftUI.View {
@_alwaysEmitIntoClient nonisolated public func onPreferenceChange<Key>(_ key: Key.Type = Key.self, action: @escaping @Sendable (Key.Value, SwiftUI.Transaction) -> Swift.Void) -> some SwiftUI.View where Key : SwiftUI.PreferenceKey, Key.Value : Swift.Equatable {
return modifier(SwiftUI_SPI.TransactionalPreferenceActionModifier<Key>(action: action))
}
}

@_hasMissingDesignatedInitializers @available(iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *)
public class CAHostingLayer<Content> : QuartzCore.CALayer where Content : SwiftUI.View {
public init(rootView: Content, environment: SwiftUI.EnvironmentValues = .init())
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
//
// PreferenceActionModifierUITests.swift
// OpenSwiftUIUITests

import Foundation
import Testing
@testable import TestingHost

private final class PreferenceActionRecorder: @unchecked Sendable {
private let lock = NSLock()
private var value: String?

func record(value: String) {
lock.lock()
defer { lock.unlock() }
self.value = value
}

func snapshot() -> String? {
lock.lock()
defer { lock.unlock() }
return value
}
}

private final class TransactionalPreferenceActionRecorder: @unchecked Sendable {
private let lock = NSLock()
private var value: String?
private var disablesAnimations = false

func record(value: String, disablesAnimations: Bool) {
lock.lock()
defer { lock.unlock() }
self.value = value
self.disablesAnimations = disablesAnimations
}

func snapshot() -> (value: String?, disablesAnimations: Bool) {
lock.lock()
defer { lock.unlock() }
return (value, disablesAnimations)
}
}

@MainActor
struct PreferenceActionModifierUITests {
@Test
func onPreferenceChange() {
let recorder = PreferenceActionRecorder()
let controller = AnimationDebugController(
PreferenceActionModifierExample(action: { value in
recorder.record(value: value)
})
)
controller.advance(interval: .zero)
#expect(recorder.snapshot() == "changed")
withExtendedLifetime(controller) {}
}

@Test
@available(iOS 18.2, macOS 15.2, *)
func transactionalOnPreferenceChange() {
let recorder = TransactionalPreferenceActionRecorder()
let controller = AnimationDebugController(
TransactionalPreferenceActionExample(action: { value, transaction in
recorder.record(
value: value,
disablesAnimations: transaction.disablesAnimations
)
})
)
controller.advance(interval: .zero)
let snapshot = recorder.snapshot()
#expect(snapshot.value == "changed")
#expect(snapshot.disablesAnimations)
withExtendedLifetime(controller) {}
}
}
3 changes: 2 additions & 1 deletion Example/Project.swift
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,8 @@ let project = Project(
targets: targets,
schemes: schemes,
additionalFiles: [
"../Configurations/**",
"../Configurations/OpenSwiftUI-Info.plist",
"../Configurations/Shared/basic/**",
"Modules/**",
"ReferenceImages/**",
"OpenSwiftUIUITests/OpenSwiftUIUITests.xctestplan",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
//
// PreferenceActionModifierExample.swift
// Shared

#if OPENSWIFTUI
import OpenSwiftUI
#else
import SwiftUI
#endif

struct PreferenceActionModifierExample: View {
private struct Key: PreferenceKey {
static let defaultValue = ""

static func reduce(value: inout String, nextValue: () -> String) {
value = nextValue()
}
}

var action: (String) -> Void

var body: some View {
Color.red
.preference(key: Key.self, value: "changed")
.onPreferenceChange(Key.self, perform: action)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//
// TransactionalPreferenceActionModifierExample.swift
// Shared

#if OPENSWIFTUI
@_spi(Private) import OpenSwiftUI
#else
import SwiftUI_SPI
#endif

#if OPENSWIFTUI
@available(OpenSwiftUI_v8_0, *)
#else
@available(iOS 18.2, macOS 15.2, *)
#endif
struct TransactionalPreferenceActionExample: View {
private struct Key: PreferenceKey {
static let defaultValue = ""

static func reduce(value: inout String, nextValue: () -> String) {
value = nextValue()
}
}

var action: @Sendable (String, Transaction) -> Void

var body: some View {
Color.red
.preference(key: Key.self, value: "changed")
.onPreferenceChange(Key.self) { value, transaction in
action(value, transaction)
}
.transaction { transaction in
transaction.disablesAnimations = true
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,14 @@

@available(OpenSwiftUI_v2_0, *)
extension _PreferenceWritingModifier: _SceneModifier {
@MainActor
@preconcurrency
public static func _makeScene(
modifier: _GraphValue<Self>,
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,
Expand All @@ -32,9 +30,7 @@ extension _PreferenceWritingModifier: _SceneModifier {
@available(OpenSwiftUI_v4_0, *)
extension Scene {
@inlinable
@MainActor
@preconcurrency
internal func preference<K>(
package func preference<K>(
key: K.Type = K.self,
value: K.Value
) -> some Scene where K: PreferenceKey {
Expand All @@ -46,8 +42,6 @@ extension Scene {

@available(OpenSwiftUI_v2_0, *)
extension _PreferenceTransformModifier: _SceneModifier {
@MainActor
@preconcurrency
public static func _makeScene(
modifier: _GraphValue<Self>,
inputs: _SceneInputs,
Expand All @@ -66,9 +60,7 @@ extension _PreferenceTransformModifier: _SceneModifier {
@available(OpenSwiftUI_v4_0, *)
extension Scene {
@inlinable
@MainActor
@preconcurrency
internal func transformPreference<K>(
func transformPreference<K>(
_ key: K.Type = K.self,
_ callback: @escaping (inout K.Value) -> Void
) -> some Scene where K: PreferenceKey {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//
// PreferenceActionModifier.swift
// OpenSwiftUI
//
// Audited for 8.0.66
// Status: Complete

package import OpenSwiftUICore

@_spi(Private)
@available(OpenSwiftUI_v8_0, *)
extension View {
/// Adds an action to perform when the specified preference key's value
/// changes, including the transaction that produced the change.
nonisolated public func onPreferenceChange<K>(
_ key: K.Type = K.self,
action: @escaping @Sendable (K.Value, Transaction) -> Void
) -> some View where K: PreferenceKey, K.Value: Equatable {
return modifier(TransactionalPreferenceActionModifier<K>(action: action))
}
}
Loading
Loading