From bfc4e76994b7f4c563651a787712b0042dc3b136 Mon Sep 17 00:00:00 2001 From: Kyle Date: Thu, 11 Dec 2025 00:53:22 +0800 Subject: [PATCH 01/10] Add a new AvailabilityMacro entry --- Configurations/Shared/basic/OpenSwiftUI.xcconfig | 2 +- Configurations/Shared/basic/SwiftUI.xcconfig | 2 +- Package.resolved | 2 +- Package.swift | 1 + 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Configurations/Shared/basic/OpenSwiftUI.xcconfig b/Configurations/Shared/basic/OpenSwiftUI.xcconfig index 6ac157056..0d2959dbd 100644 --- a/Configurations/Shared/basic/OpenSwiftUI.xcconfig +++ b/Configurations/Shared/basic/OpenSwiftUI.xcconfig @@ -2,4 +2,4 @@ GCC_PREPROCESSOR_DEFINITIONS_OPENSWIFTUI = OPENSWIFTUI=1 SWIFT_ACTIVE_COMPILATION_CONDITIONS_OPENSWIFTUI = OPENSWIFTUI -OPENSWIFTUI_AVAILABILITY_MACRO = "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v1_0:iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v1_4:iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v2_0:iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_macOS_v2_0:iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v2_1:iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v2_3:iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v3_0:iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v3_2:iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v3_4:iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v4_0:iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v4_1:iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v4_4:iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v5_0:iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v5_1:iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v5_2:iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v5_4:iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v6_0:iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v7_0:iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0" +OPENSWIFTUI_AVAILABILITY_MACRO = "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v1_0:iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v1_4:iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v2_0:iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_macOS_v2_0:iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v2_1:iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v2_3:iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v3_0:iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v3_2:iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v3_4:iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v4_0:iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v4_1:iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v4_4:iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v5_0:iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v5_1:iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v5_2:iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v5_4:iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v5_5:iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v6_0:iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v7_0:iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0" diff --git a/Configurations/Shared/basic/SwiftUI.xcconfig b/Configurations/Shared/basic/SwiftUI.xcconfig index d739ad347..6b6539a73 100644 --- a/Configurations/Shared/basic/SwiftUI.xcconfig +++ b/Configurations/Shared/basic/SwiftUI.xcconfig @@ -1 +1 @@ -OPENSWIFTUI_AVAILABILITY_MACRO = "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v1_0:iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v1_4:iOS 13.4, macOS 10.15.4, tvOS 13.4, watchOS 6.2" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v2_0:iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_macOS_v2_0:macOS 11.0" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v2_1:iOS 14.2, macOS 11.0, tvOS 14.1, watchOS 7.1" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v2_3:iOS 14.5, macOS 11.3, tvOS 14.5, watchOS 7.4" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v3_0:iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v3_2:iOS 15.2, macOS 12.1, tvOS 15.2, watchOS 8.3" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v3_4:iOS 15.4, macOS 12.3, tvOS 15.4, watchOS 8.5" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v4_0:iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v4_1:iOS 16.1, macOS 13.0, tvOS 16.1, watchOS 9.1" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v4_4:iOS 16.4, macOS 13.3, tvOS 16.4, watchOS 9.4" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v5_0:iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v5_1:iOS 17.1, macOS 14.1, tvOS 17.1, watchOS 10.1, visionOS 1.0" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v5_2:iOS 17.2, macOS 14.2, tvOS 17.2, watchOS 10.2, visionOS 1.0" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v5_4:iOS 17.4, macOS 14.4, tvOS 17.4, watchOS 10.4, visionOS 1.1" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v6_0:iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v7_0:iOS 19.0, macOS 16.0, tvOS 19.0, watchOS 12.0, visionOS 3.0" +OPENSWIFTUI_AVAILABILITY_MACRO = "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v1_0:iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v1_4:iOS 13.4, macOS 10.15.4, tvOS 13.4, watchOS 6.2" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v2_0:iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_macOS_v2_0:macOS 11.0" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v2_1:iOS 14.2, macOS 11.0, tvOS 14.1, watchOS 7.1" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v2_3:iOS 14.5, macOS 11.3, tvOS 14.5, watchOS 7.4" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v3_0:iOS 15.0, macOS 12.0, tvOS 15.0, watchOS 8.0" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v3_2:iOS 15.2, macOS 12.1, tvOS 15.2, watchOS 8.3" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v3_4:iOS 15.4, macOS 12.3, tvOS 15.4, watchOS 8.5" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v4_0:iOS 16.0, macOS 13.0, tvOS 16.0, watchOS 9.0" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v4_1:iOS 16.1, macOS 13.0, tvOS 16.1, watchOS 9.1" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v4_4:iOS 16.4, macOS 13.3, tvOS 16.4, watchOS 9.4" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v5_0:iOS 17.0, macOS 14.0, tvOS 17.0, watchOS 10.0" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v5_1:iOS 17.1, macOS 14.1, tvOS 17.1, watchOS 10.1, visionOS 1.0" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v5_2:iOS 17.2, macOS 14.2, tvOS 17.2, watchOS 10.2, visionOS 1.0" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v5_4:iOS 17.4, macOS 14.4, tvOS 17.4, watchOS 10.4, visionOS 1.1" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v5_5:iOS 17.5, macOS 14.5, tvOS 17.5, watchOS 10.5, visionOS 1.2" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v6_0:iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0" "-Xfrontend" "-enable-experimental-feature" "-Xfrontend" "AvailabilityMacro=OpenSwiftUI_v7_0:iOS 19.0, macOS 16.0, tvOS 19.0, watchOS 12.0, visionOS 3.0" diff --git a/Package.resolved b/Package.resolved index 0b9c15f89..0364f348b 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "8425ef2c9fa27534027824da9a10246f9b735e1b4d0ffc21363e1d9bfdf98930", + "originHash" : "373d4af9c37ac358f7c48ef2ced6e6c83689073755afce25ca6121fd7de0cce7", "pins" : [ { "identity" : "darwinprivateframeworks", diff --git a/Package.swift b/Package.swift index a36634244..c31e4eec1 100644 --- a/Package.swift +++ b/Package.swift @@ -469,6 +469,7 @@ extension [SwiftSetting] { .enableExperimentalFeature("AvailabilityMacro=OpenSwiftUI_v5_1:\(ignoreAvailability ? minimumVersion : "iOS 17.1, macOS 14.1, tvOS 17.1, watchOS 10.1, visionOS 1.0")"), .enableExperimentalFeature("AvailabilityMacro=OpenSwiftUI_v5_2:\(ignoreAvailability ? minimumVersion : "iOS 17.2, macOS 14.2, tvOS 17.2, watchOS 10.2, visionOS 1.0")"), .enableExperimentalFeature("AvailabilityMacro=OpenSwiftUI_v5_4:\(ignoreAvailability ? minimumVersion : "iOS 17.4, macOS 14.4, tvOS 17.4, watchOS 10.4, visionOS 1.1")"), + .enableExperimentalFeature("AvailabilityMacro=OpenSwiftUI_v5_5:\(ignoreAvailability ? minimumVersion : "iOS 17.5, macOS 14.5, tvOS 17.5, watchOS 10.5, visionOS 1.2")"), .enableExperimentalFeature("AvailabilityMacro=OpenSwiftUI_v6_0:\(ignoreAvailability ? minimumVersion : "iOS 18.0, macOS 15.0, tvOS 18.0, watchOS 11.0, visionOS 2.0")"), .enableExperimentalFeature("AvailabilityMacro=OpenSwiftUI_v7_0:\(ignoreAvailability ? minimumVersion : "iOS 19.0, macOS 16.0, tvOS 19.0, watchOS 12.0, visionOS 3.0")"), .enableExperimentalFeature("AvailabilityMacro=_distantFuture:iOS 99.0, macOS 99.0, tvOS 99.0, watchOS 99.0, visionOS 99.0"), From 0d62b714f4ff8149019622eb7890f46da508b804 Mon Sep 17 00:00:00 2001 From: Kyle Date: Thu, 11 Dec 2025 02:00:15 +0800 Subject: [PATCH 02/10] Add SensoryFeedback definition --- .../SensoryFeedback/SensoryFeedback.swift | 197 ++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 Sources/OpenSwiftUI/View/Control/SensoryFeedback/SensoryFeedback.swift diff --git a/Sources/OpenSwiftUI/View/Control/SensoryFeedback/SensoryFeedback.swift b/Sources/OpenSwiftUI/View/Control/SensoryFeedback/SensoryFeedback.swift new file mode 100644 index 000000000..796a82c75 --- /dev/null +++ b/Sources/OpenSwiftUI/View/Control/SensoryFeedback/SensoryFeedback.swift @@ -0,0 +1,197 @@ +// +// SensoryFeedback.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: Complete + +/// Represents a type of haptic and/or audio feedback that can be played. +/// +/// This feedback can be passed to `View.sensoryFeedback` to play it. +@available(OpenSwiftUI_v5_0, *) +@available(visionOS, unavailable) +public struct SensoryFeedback: Equatable, Sendable { + enum FeedbackType: Hashable { + case success + case warning + case error + case increase + case decrease + case selection + case alignment + case levelChange + case start + case stop + case pathComplete + case impactWeight(SensoryFeedback.Weight.Storage, Double) + case impactFlexibility(SensoryFeedback.Flexibility.Storage, Double) + } + + var type: FeedbackType + + /// Indicates that a task or action has completed. + /// + /// Only plays feedback on iOS and watchOS. + public static let success: SensoryFeedback = .init(type: .success) + + /// Indicates that a task or action has produced a warning of some kind. + /// + /// Only plays feedback on iOS and watchOS. + public static let warning: SensoryFeedback = .init(type: .warning) + + /// Indicates that an error has occurred. + /// + /// Only plays feedback on iOS and watchOS. + public static let error: SensoryFeedback = .init(type: .error) + + /// Indicates that a UI element’s values are changing. + /// + /// Only plays feedback on iOS and watchOS. + public static let selection: SensoryFeedback = .init(type: .selection) + + /// Indicates that an important value increased above a significant + /// threshold. + /// + /// Only plays feedback on watchOS. + public static let increase: SensoryFeedback = .init(type: .increase) + + /// Indicates that an important value decreased below a significant + /// threshold. + /// + /// Only plays feedback on watchOS. + public static let decrease: SensoryFeedback = .init(type: .decrease) + + /// Indicates that an activity started. + /// + /// Use this haptic when starting a timer or any other activity that can be + /// explicitly started and stopped. + /// + /// Only plays feedback on watchOS. + public static let start: SensoryFeedback = .init(type: .start) + + /// Indicates that an activity stopped. + /// + /// Use this haptic when stopping a timer or other activity that was + /// previously started. + /// + /// Only plays feedback on watchOS. + public static let stop: SensoryFeedback = .init(type: .stop) + + /// Indicates the alignment of a dragged item. + /// + /// For example, use this pattern in a drawing app when the user drags a + /// shape into alignment with another shape. + /// + /// Only plays feedback on iOS and macOS. + public static let alignment: SensoryFeedback = .init(type: .alignment) + + /// Indicates movement between discrete levels of pressure. + /// + /// For example, as the user presses a fast-forward button on a video + /// player, playback could increase or decrease and haptic feedback could be + /// provided as different levels of pressure are reached. + /// + /// Only plays feedback on macOS. + public static let levelChange: SensoryFeedback = .init(type: .levelChange) + + /// Indicates a drawn path has completed and/or recognized. + /// + /// Use this to provide feedback for closed shape drawing or similar + /// actions. It should supplement the user experience, since only some + /// platforms will play feedback in response to it. + /// + /// Only plays feedback on iOS. + @available(OpenSwiftUI_v5_5, *) + public static let pathComplete: SensoryFeedback = .init(type: .pathComplete) + + /// Provides a physical metaphor you can use to complement a visual + /// experience. + /// + /// Use this to provide feedback for UI elements colliding. It should + /// supplement the user experience, since only some platforms will play + /// feedback in response to it. + /// + /// Only plays feedback on iOS and watchOS. + public static let impact: SensoryFeedback = .init(type: .impactWeight(.light, 1.0)) + + /// Provides a physical metaphor you can use to complement a visual + /// experience. + /// + /// Use this to provide feedback for UI elements colliding. It should + /// supplement the user experience, since only some platforms will play + /// feedback in response to it. + /// + /// Not all platforms will play different feedback for different weights and + /// intensities of impact. + /// + /// Only plays feedback on iOS and watchOS. + public static func impact(weight: SensoryFeedback.Weight, intensity: Double = 1.0) -> SensoryFeedback { + .init(type: .impactWeight(weight.storage, intensity)) + } + + /// Provides a physical metaphor you can use to complement a visual + /// experience. + /// + /// Use this to provide feedback for UI elements colliding. It should + /// supplement the user experience, since only some platforms will play + /// feedback in response to it. + /// + /// Not all platforms will play different feedback for different + /// flexibilities and intensities of impact. + /// + /// Only plays feedback on iOS and watchOS. + public static func impact(flexibility: SensoryFeedback.Flexibility, intensity: Double = 1.0) -> SensoryFeedback { + .init(type: .impactFlexibility(flexibility.storage, intensity)) + } + + // MARK: - SensoryFeedback.Weight + + /// The weight to be represented by a type of feedback. + /// + /// `Weight` values can be passed to + /// `SensoryFeedback.impact(weight:intensity:)`. + public struct Weight: Equatable, Sendable { + enum Storage: Hashable { + case light + case medium + case heavy + } + + var storage: Storage + + /// Indicates a collision between small or lightweight UI objects. + public static let light: SensoryFeedback.Weight = .init(storage: .light) + + /// Indicates a collision between medium-sized or medium-weight UI + /// objects. + public static let medium: SensoryFeedback.Weight = .init(storage: .medium) + + /// Indicates a collision between large or heavyweight UI objects. + public static let heavy: SensoryFeedback.Weight = .init(storage: .heavy) + } + + // MARK: - SensoryFeedback.Flexibility + + /// The flexibility to be represented by a type of feedback. + /// + /// `Flexibility` values can be passed to + /// `SensoryFeedback.impact(flexibility:intensity:)`. + public struct Flexibility: Equatable, Sendable { + enum Storage: Hashable { + case rigid + case solid + case soft + } + var storage: Storage + + /// Indicates a collision between hard or inflexible UI objects. + public static let rigid: SensoryFeedback.Flexibility = .init(storage: .rigid) + + /// Indicates a collision between solid UI objects of medium + /// flexibility. + public static let solid: SensoryFeedback.Flexibility = .init(storage: .solid) + + /// Indicates a collision between soft or flexible UI objects. + public static let soft: SensoryFeedback.Flexibility = .init(storage: .soft) + } +} From 188cf0fb3d185e96e088d8c4e199ad231a668db2 Mon Sep 17 00:00:00 2001 From: Kyle Date: Thu, 11 Dec 2025 01:33:41 +0800 Subject: [PATCH 03/10] Add PlatformSensoryFeedback --- .../View/Control/SensoryFeedback/SensoryFeedback.swift | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Sources/OpenSwiftUI/View/Control/SensoryFeedback/SensoryFeedback.swift b/Sources/OpenSwiftUI/View/Control/SensoryFeedback/SensoryFeedback.swift index 796a82c75..7302b6a2e 100644 --- a/Sources/OpenSwiftUI/View/Control/SensoryFeedback/SensoryFeedback.swift +++ b/Sources/OpenSwiftUI/View/Control/SensoryFeedback/SensoryFeedback.swift @@ -5,6 +5,8 @@ // Audited for 6.5.4 // Status: Complete +// MARK: - SensoryFeedback + /// Represents a type of haptic and/or audio feedback that can be played. /// /// This feedback can be passed to `View.sensoryFeedback` to play it. @@ -195,3 +197,11 @@ public struct SensoryFeedback: Equatable, Sendable { public static let soft: SensoryFeedback.Flexibility = .init(storage: .soft) } } + +// MARK: - PlatformSensoryFeedback + +protocol PlatformSensoryFeedback { + func setUp() + func tearDown() + func generate() +} From df71e43a49bd27488c5d3cb524b698f09fd8bff1 Mon Sep 17 00:00:00 2001 From: Kyle Date: Fri, 12 Dec 2025 01:21:27 +0800 Subject: [PATCH 04/10] Add UIKitFeedbackImplementation --- .../UIKit/UIKitFeedbackImplementation.swift | 180 ++++++++++++++++++ .../UIKit/UIKitSensoryFeedbackCache.swift | 132 +++++++++++++ 2 files changed, 312 insertions(+) create mode 100644 Sources/OpenSwiftUI/View/Control/SensoryFeedback/UIKit/UIKitFeedbackImplementation.swift create mode 100644 Sources/OpenSwiftUI/View/Control/SensoryFeedback/UIKit/UIKitSensoryFeedbackCache.swift diff --git a/Sources/OpenSwiftUI/View/Control/SensoryFeedback/UIKit/UIKitFeedbackImplementation.swift b/Sources/OpenSwiftUI/View/Control/SensoryFeedback/UIKit/UIKitFeedbackImplementation.swift new file mode 100644 index 000000000..ab6eb2df6 --- /dev/null +++ b/Sources/OpenSwiftUI/View/Control/SensoryFeedback/UIKit/UIKitFeedbackImplementation.swift @@ -0,0 +1,180 @@ +// +// UIKitFeedbackImplementation.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: WIP +// ID: C9541C03AF81FECFD19A57A1BB81CE81 (SwiftUI) + +#if os(iOS) || os(visionOS) + +import OpenAttributeGraphShims +@_spi(ForOpenSwiftUIOnly) +import OpenSwiftUICore +import UIKit + +// MARK: - LocationBasedSensoryFeedback + +protocol LocationBasedSensoryFeedback: PlatformSensoryFeedback { + func generate(location: CGPoint) +} + +// MARK: - NotificationFeedbackImplementation + +struct NotificationFeedbackImplementation: LocationBasedSensoryFeedback { + let generator: UINotificationFeedbackGenerator + + let type: UINotificationFeedbackGenerator.FeedbackType + + func setUp() { + generator.prepare() + } + + func tearDown() { + _openSwiftUIEmptyStub() + } + + func generate() { + preconditionFailure("Requires location.") + } + + func generate(location: CGPoint) { + generator.notificationOccurred(type, at: location) + } +} + +// MARK: - SelectionFeedbackImplementation + +struct SelectionFeedbackImplementation: LocationBasedSensoryFeedback { + let generator: UISelectionFeedbackGenerator + + func setUp() { + generator.prepare() + } + + func tearDown() { + _openSwiftUIEmptyStub() + } + + func generate() { + preconditionFailure("Requires location.") + } + + func generate(location: CGPoint) { + generator.selectionChanged(at: location) + } +} + +// MARK: - CanvasFeedbackImplementation + +struct CanvasFeedbackImplementation: LocationBasedSensoryFeedback { + let generator: UICanvasFeedbackGenerator + let type: SensoryFeedback.FeedbackType + + func setUp() { + generator.prepare() + } + + func tearDown() { + _openSwiftUIEmptyStub() + } + + func generate() { + preconditionFailure("Requires location.") + } + + func generate(location: CGPoint) { + switch type { + case .alignment: + generator.alignmentOccurred(at: location) + case .pathComplete: + generator.pathCompleted(at: location) + default: + break + } + } +} + +// MARK: - ImpactFeedbackImplementation + +struct ImpactFeedbackImplementation: LocationBasedSensoryFeedback { + let generator: UIImpactFeedbackGenerator + var intensity: Double + + func setUp() { + generator.prepare() + } + + func tearDown() { + _openSwiftUIEmptyStub() + } + + func generate() { + preconditionFailure("Requires location.") + } + + func generate(location: CGPoint) { + generator.impactOccurred(intensity: intensity, at: location) + } +} + +// MARK: - LocationBasedFeedbackAdaptor + +struct LocationBasedFeedbackAdaptor: PlatformSensoryFeedback { + var location: Attribute + var base: any LocationBasedSensoryFeedback + + func setUp() { + base.setUp() + } + + func tearDown() { + base.tearDown() + } + + func generate() { + let currentLocation: CGPoint = Update.ensure { + Graph.withoutUpdate { + location.value + } + } + base.generate(location: currentLocation) + } +} + +// MARK: - LocationBasedFeedbackAdaptor + +private struct FeedbackLocation: Rule { + @Attribute var position: ViewOrigin + @Attribute var size: ViewSize + @Attribute var transform: ViewTransform + + var value: CGPoint { + var transform = transform + transform.appendPosition(position) + var rect = CGRect(origin: position, size: size.value) + rect.convert(to: .id(hostingViewCoordinateSpace), transform: transform) + return rect.center + } +} + +// MARK: - FeedbackRequestContext + +struct FeedbackRequestContext { + var location: WeakAttribute + weak var cache: AnyUIKitSensoryFeedbackCache? + + func implementation(type: SensoryFeedback.FeedbackType) -> PlatformSensoryFeedback? { + guard let cache, + let feeback = cache.implementation(type: type), + let location = location.attribute else { + return nil + } + return LocationBasedFeedbackAdaptor( + location: location, + base: feeback + ) + } +} + +#endif diff --git a/Sources/OpenSwiftUI/View/Control/SensoryFeedback/UIKit/UIKitSensoryFeedbackCache.swift b/Sources/OpenSwiftUI/View/Control/SensoryFeedback/UIKit/UIKitSensoryFeedbackCache.swift new file mode 100644 index 000000000..52b4770c9 --- /dev/null +++ b/Sources/OpenSwiftUI/View/Control/SensoryFeedback/UIKit/UIKitSensoryFeedbackCache.swift @@ -0,0 +1,132 @@ +// +// UIKitSensoryFeedbackCache.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: Complete +// ID: 54085EE6CA77C60CA86318C59A381498 (SwiftUI) + +#if os(iOS) || os(visionOS) + +import OpenSwiftUICore +import UIKit + +// MARK: - AnyUIKitSensoryFeedbackCache + +class AnyUIKitSensoryFeedbackCache { + func implementation( + type: SensoryFeedback.FeedbackType + ) -> LocationBasedSensoryFeedback? { + _openSwiftUIBaseClassAbstractMethod() + } +} + +// MARK: - UIKitSensoryFeedbackCache + +class UIKitSensoryFeedbackCache: AnyUIKitSensoryFeedbackCache where V: View { + weak var host: _UIHostingView? + var cachedGenerators: [SensoryFeedback.FeedbackType: UIFeedbackGenerator] = [:] + + override func implementation( + type: SensoryFeedback.FeedbackType + ) -> LocationBasedSensoryFeedback? { + switch type { + case .success: + getGenerator(type) { + NotificationFeedbackImplementation( + generator: $0, + type: .success + ) + } createIfNeeded: { + UINotificationFeedbackGenerator() + } + case .warning: + getGenerator(type) { + NotificationFeedbackImplementation( + generator: $0, + type: .warning + ) + } createIfNeeded: { + UINotificationFeedbackGenerator() + } + case .error: + getGenerator(type) { + NotificationFeedbackImplementation( + generator: $0, + type: .error + ) + } createIfNeeded: { + UINotificationFeedbackGenerator() + } + case .levelChange, .start, .stop: + nil + case .increase, .decrease, .selection: + getGenerator(type) { + SelectionFeedbackImplementation( + generator: $0 + ) + } createIfNeeded: { + UISelectionFeedbackGenerator() + } + case .alignment, .pathComplete: + getGenerator(type) { + CanvasFeedbackImplementation( + generator: $0, + type: type + ) + } createIfNeeded: { + UICanvasFeedbackGenerator() + } + case let .impactWeight(weight, intensity): + getGenerator(type) { + ImpactFeedbackImplementation( + generator: $0, + intensity: intensity + ) + } createIfNeeded: { () -> UIImpactFeedbackGenerator in + switch weight { + case .light: UIImpactFeedbackGenerator(style: .light) + case .medium: UIImpactFeedbackGenerator(style: .medium) + case .heavy: UIImpactFeedbackGenerator(style: .heavy) + } + } + case let .impactFlexibility(flexibility, intensity): + getGenerator(type) { + ImpactFeedbackImplementation( + generator: $0, + intensity: intensity + ) + } createIfNeeded: { () -> UIImpactFeedbackGenerator in + switch flexibility { + case .rigid: UIImpactFeedbackGenerator(style: .rigid) + case .solid: UIImpactFeedbackGenerator(style: .medium) + case .soft: UIImpactFeedbackGenerator(style: .soft) + } + } + } + } + + private func getGenerator( + _ type: SensoryFeedback.FeedbackType, + work: (Generator) -> Feedback, + createIfNeeded: () -> Generator + ) -> Feedback where Generator: UIFeedbackGenerator, Feedback: LocationBasedSensoryFeedback { + let generator: Generator + if let cachedGenerator = cachedGenerators[type] { + generator = cachedGenerator as! Generator + } else { + generator = createIfNeeded() + cachedGenerators[type] = generator + host!.addInteraction(generator) + } + return work(generator) + } +} + +// MARK: - FeedbackCacheKey + +private struct FeedbackCacheKey: EnvironmentKey { + static var defaultValue: WeakBox { .init() } +} + +#endif From fc115ee82d99812d9704e443b7a612bd68f5249e Mon Sep 17 00:00:00 2001 From: Kyle Date: Fri, 12 Dec 2025 23:38:34 +0800 Subject: [PATCH 05/10] Add FeedbackRequestContextWriter --- .../UIKit/UIKitFeedbackImplementation.swift | 58 ++++++++++++++++++- .../UIKit/UIKitSensoryFeedbackCache.swift | 11 ++++ 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/Sources/OpenSwiftUI/View/Control/SensoryFeedback/UIKit/UIKitFeedbackImplementation.swift b/Sources/OpenSwiftUI/View/Control/SensoryFeedback/UIKit/UIKitFeedbackImplementation.swift index ab6eb2df6..1d8584375 100644 --- a/Sources/OpenSwiftUI/View/Control/SensoryFeedback/UIKit/UIKitFeedbackImplementation.swift +++ b/Sources/OpenSwiftUI/View/Control/SensoryFeedback/UIKit/UIKitFeedbackImplementation.swift @@ -3,7 +3,7 @@ // OpenSwiftUI // // Audited for 6.5.4 -// Status: WIP +// Status: Complete // ID: C9541C03AF81FECFD19A57A1BB81CE81 (SwiftUI) #if os(iOS) || os(visionOS) @@ -19,6 +19,62 @@ protocol LocationBasedSensoryFeedback: PlatformSensoryFeedback { func generate(location: CGPoint) } +// MARK: - FeedbackRequestContextWriter + +private struct FeedbackRequestContextWriter: MultiViewModifier, PrimitiveViewModifier where Base: SensoryFeedbackGeneratorModifier { + var base: Base + + nonisolated static func _makeView( + modifier: _GraphValue, + inputs: _ViewInputs, + body: @escaping (_Graph, _ViewInputs) -> _ViewOutputs + ) -> _ViewOutputs { + let base = modifier[offset: { .of(&$0.base) }].value + let child = { + guard inputs.needsGeometry else { + return base + } + let location = if inputs.base.interfaceIdiom.accepts(.pad) { + Attribute( + FeedbackLocation( + position: inputs.position, + size: inputs.size, + transform: inputs.transform + ) + ) + } else { + ViewGraph.current.$zeroPoint + } + let feedbackCache = inputs.base.feedbackCache + let modifier = Attribute( + ChildModifier( + base: base, + location: location, + feedbackCache: feedbackCache + ) + ) + return modifier + }() + return Base._makeView( + modifier: _GraphValue(child), + inputs: inputs, + body: body + ) + } + + struct ChildModifier: Rule { + @Attribute var base: Base + @Attribute var location: CGPoint + @Attribute var feedbackCache: AnyUIKitSensoryFeedbackCache? + + var value: Base { + var base = base + base.feedbackRequestContext = .init(location: WeakAttribute($location), cache: feedbackCache) + return base + } + } +} + // MARK: - NotificationFeedbackImplementation struct NotificationFeedbackImplementation: LocationBasedSensoryFeedback { diff --git a/Sources/OpenSwiftUI/View/Control/SensoryFeedback/UIKit/UIKitSensoryFeedbackCache.swift b/Sources/OpenSwiftUI/View/Control/SensoryFeedback/UIKit/UIKitSensoryFeedbackCache.swift index 52b4770c9..52af5db67 100644 --- a/Sources/OpenSwiftUI/View/Control/SensoryFeedback/UIKit/UIKitSensoryFeedbackCache.swift +++ b/Sources/OpenSwiftUI/View/Control/SensoryFeedback/UIKit/UIKitSensoryFeedbackCache.swift @@ -8,6 +8,7 @@ #if os(iOS) || os(visionOS) +import OpenAttributeGraphShims import OpenSwiftUICore import UIKit @@ -129,4 +130,14 @@ private struct FeedbackCacheKey: EnvironmentKey { static var defaultValue: WeakBox { .init() } } +extension CachedEnvironment.ID { + static let feedbackCache: CachedEnvironment.ID = .init() +} + +extension _GraphInputs { + var feedbackCache: Attribute { + mapEnvironment(id: .feedbackCache) { $0[FeedbackCacheKey.self].base } + } +} + #endif From 835500eccbdfb79e7dfd33e4647a830f800ff11b Mon Sep 17 00:00:00 2001 From: Kyle Date: Sat, 13 Dec 2025 16:20:55 +0800 Subject: [PATCH 06/10] Add SensoryFeedbackGenerator --- .../SensoryFeedbackGenerator.swift | 228 ++++++++++++++++++ .../UIKit/UIKitFeedbackImplementation.swift | 12 +- 2 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 Sources/OpenSwiftUI/View/Control/SensoryFeedback/SensoryFeedbackGenerator.swift diff --git a/Sources/OpenSwiftUI/View/Control/SensoryFeedback/SensoryFeedbackGenerator.swift b/Sources/OpenSwiftUI/View/Control/SensoryFeedback/SensoryFeedbackGenerator.swift new file mode 100644 index 000000000..7739e8c60 --- /dev/null +++ b/Sources/OpenSwiftUI/View/Control/SensoryFeedback/SensoryFeedbackGenerator.swift @@ -0,0 +1,228 @@ +// +// SensoryFeedbackGenerator.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: Complete +// ID: E5C2FE5C277CCA85C518490456542950 (SwiftUI) + +// MARK: - View + senosryFeedback + +@available(OpenSwiftUI_v5_0, *) +@available(visionOS, unavailable) +extension View { + + /// Plays the specified `feedback` when the provided `trigger` value + /// changes. + /// + /// For example, you could play feedback when a state value changes: + /// + /// struct MyView: View { + /// @State private var showAccessory = false + /// + /// var body: some View { + /// ContentView() + /// .sensoryFeedback(.selection, trigger: showAccessory) + /// .onLongPressGesture { + /// showAccessory.toggle() + /// } + /// + /// if showAccessory { + /// AccessoryView() + /// } + /// } + /// } + /// + /// - Parameters: + /// - feedback: Which type of feedback to play. + /// - trigger: A value to monitor for changes to determine when to play. + nonisolated public func sensoryFeedback( + _ feedback: SensoryFeedback, + trigger: T + ) -> some View where T: Equatable { + platformSensoryFeedback( + FeedbackGenerator( + feedbackRequestContext: .init(location: .init()), + feedback: feedback, + trigger: trigger, + condition: nil, + implementation: nil + ) + ) + } + + /// Plays the specified `feedback` when the provided `trigger` value changes + /// and the `condition` closure returns `true`. + /// + /// For example, you could play feedback for certain state transitions: + /// + /// struct MyView: View { + /// @State private var phase = Phase.inactive + /// + /// var body: some View { + /// ContentView(phase: $phase) + /// .sensoryFeedback(.selection, trigger: phase) { old, new in + /// old == .inactive || new == .expanded + /// } + /// } + /// + /// enum Phase { + /// case inactive + /// case preparing + /// case active + /// case expanded + /// } + /// } + /// + /// - Parameters: + /// - feedback: Which type of feedback to play. + /// - trigger: A value to monitor for changes to determine when to play. + /// - condition: A closure to determine whether to play the feedback when + /// `trigger` changes. + nonisolated public func sensoryFeedback( + _ feedback: SensoryFeedback, + trigger: T, + condition: @escaping (_ oldValue: T, _ newValue: T) -> Bool + ) -> some View where T: Equatable { + platformSensoryFeedback( + FeedbackGenerator( + feedbackRequestContext: .init(location: .init()), + feedback: feedback, + trigger: trigger, + condition: condition, + implementation: nil + ) + ) + } + + /// Plays feedback when returned from the `feedback` closure after the + /// provided `trigger` value changes. + /// + /// For example, you could play different feedback for different state + /// transitions: + /// + /// struct MyView: View { + /// @State private var phase = Phase.inactive + /// + /// var body: some View { + /// ContentView(phase: $phase) + /// .sensoryFeedback(trigger: phase) { old, new in + /// switch (old, new) { + /// case (.inactive, _): return .success + /// case (_, .expanded): return .impact + /// default: return nil + /// } + /// } + /// } + /// + /// enum Phase { + /// case inactive + /// case preparing + /// case active + /// case expanded + /// } + /// } + /// + /// - Parameters: + /// - trigger: A value to monitor for changes to determine when to play. + /// - feedback: A closure to determine whether to play the feedback and + /// what type of feedback to play when `trigger` changes. + nonisolated public func sensoryFeedback( + trigger: T, + _ feedback: @escaping (_ oldValue: T, _ newValue: T) -> SensoryFeedback? + ) -> some View where T: Equatable { + platformSensoryFeedback( + CustomFeedbackGenerator( + feedbackRequestContext: .init(location: .init()), + trigger: trigger, + feedback: feedback + ) + ) + } +} + +// MARK: - SensoryFeedbackGeneratorModifier + +protocol SensoryFeedbackGeneratorModifier: ViewModifier { + var feedbackRequestContext: FeedbackRequestContext { get set } +} + +// MARK: - CustomFeedbackGenerator + +private struct CustomFeedbackGenerator: SensoryFeedbackGeneratorModifier where T: Equatable { + var feedbackRequestContext: FeedbackRequestContext + var trigger: T + var feedback: (T, T) -> SensoryFeedback? + @State var state: (feedback: SensoryFeedback, implementation: (any PlatformSensoryFeedback)?)? + + init( + feedbackRequestContext: FeedbackRequestContext, + trigger: T, + feedback: @escaping (T, T) -> SensoryFeedback? + ) { + self.feedbackRequestContext = feedbackRequestContext + self.trigger = trigger + self.feedback = feedback + self._state = State(wrappedValue: nil) + } + + func body(content: Content) -> some View { + content + .onChange(of: trigger) { oldValue, newValue in + let newFeedback = feedback(oldValue, newValue) + if state?.feedback != newFeedback { + state?.implementation?.tearDown() + let newValue: (SensoryFeedback, (any PlatformSensoryFeedback)?)? + if let newFeedback { + newValue = ( + newFeedback, + feedbackRequestContext.implementation(type: newFeedback.type) + ) + } else { + newValue = nil + } + state = newValue + state?.implementation?.setUp() + } + state?.implementation?.generate() + } + } +} + +// MARK: - FeedbackGenerator + +private struct FeedbackGenerator: SensoryFeedbackGeneratorModifier where T: Equatable { + var feedbackRequestContext: FeedbackRequestContext + var feedback: SensoryFeedback + var trigger: T + var condition: ((T, T) -> Bool)? + @State var implementation: (any PlatformSensoryFeedback)? + + init( + feedbackRequestContext: FeedbackRequestContext, + feedback: SensoryFeedback, + trigger: T, + condition: ((T, T) -> Bool)?, + implementation: (any PlatformSensoryFeedback)? + ) { + self.feedbackRequestContext = feedbackRequestContext + self.feedback = feedback + self.trigger = trigger + self.condition = condition + self._implementation = .init(initialValue: implementation) + } + + func body(content: Content) -> some View { + content + .task(id: feedback) { + implementation?.tearDown() + implementation = feedbackRequestContext.implementation(type: feedback.type) + implementation?.setUp() + } + .onChange(of: trigger) { oldValue, newValue in + if condition?(oldValue, newValue) ?? true { + implementation?.generate() + } + } + } +} diff --git a/Sources/OpenSwiftUI/View/Control/SensoryFeedback/UIKit/UIKitFeedbackImplementation.swift b/Sources/OpenSwiftUI/View/Control/SensoryFeedback/UIKit/UIKitFeedbackImplementation.swift index 1d8584375..11b4a4b74 100644 --- a/Sources/OpenSwiftUI/View/Control/SensoryFeedback/UIKit/UIKitFeedbackImplementation.swift +++ b/Sources/OpenSwiftUI/View/Control/SensoryFeedback/UIKit/UIKitFeedbackImplementation.swift @@ -19,6 +19,16 @@ protocol LocationBasedSensoryFeedback: PlatformSensoryFeedback { func generate(location: CGPoint) } +// MARK: - View + platformSensoryFeedback + +extension View { + nonisolated func platformSensoryFeedback( + _ base: Base + ) -> some View where Base: SensoryFeedbackGeneratorModifier { + modifier(FeedbackRequestContextWriter(base: base)) + } +} + // MARK: - FeedbackRequestContextWriter private struct FeedbackRequestContextWriter: MultiViewModifier, PrimitiveViewModifier where Base: SensoryFeedbackGeneratorModifier { @@ -220,7 +230,7 @@ struct FeedbackRequestContext { var location: WeakAttribute weak var cache: AnyUIKitSensoryFeedbackCache? - func implementation(type: SensoryFeedback.FeedbackType) -> PlatformSensoryFeedback? { + func implementation(type: SensoryFeedback.FeedbackType) -> (any PlatformSensoryFeedback)? { guard let cache, let feeback = cache.implementation(type: type), let location = location.attribute else { From 20c9b3ffab91976effd7dd0156e4d11e2118cf39 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sat, 13 Dec 2025 16:54:36 +0800 Subject: [PATCH 07/10] Add missing UIKit integration --- .../Integration/Hosting/UIKit/View/UIHostingView.swift | 10 ++++++++-- .../UIKit/UIKitSensoryFeedbackCache.swift | 9 ++++++++- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/Sources/OpenSwiftUI/Integration/Hosting/UIKit/View/UIHostingView.swift b/Sources/OpenSwiftUI/Integration/Hosting/UIKit/View/UIHostingView.swift index f19b3953d..72fe34c66 100644 --- a/Sources/OpenSwiftUI/Integration/Hosting/UIKit/View/UIHostingView.swift +++ b/Sources/OpenSwiftUI/Integration/Hosting/UIKit/View/UIHostingView.swift @@ -124,7 +124,11 @@ open class _UIHostingView: UIView, XcodeViewDebugDataProvider where Con didChangeColorScheme(from: oldValue) } } - + + // TODO + + let feedbackCache: UIKitSensoryFeedbackCache = .init() + // TODO // var currentAccessibilityFocusStore: AccessibilityFocusStore = .init() @@ -177,7 +181,7 @@ open class _UIHostingView: UIView, XcodeViewDebugDataProvider where Con base.setupNotifications() } // RepresentableContextValues.current = - + feedbackCache.host = self // TODO HostingViewRegistry.shared.add(self) Update.end() @@ -512,6 +516,8 @@ extension _UIHostingView: ViewRendererHost { if let displayGamut = DisplayGamut(rawValue: traitCollection.displayGamut.rawValue) { environment.displayGamut = displayGamut } + // TODO + environment.feedbackCache = feedbackCache viewGraph.setEnvironment(environment) } diff --git a/Sources/OpenSwiftUI/View/Control/SensoryFeedback/UIKit/UIKitSensoryFeedbackCache.swift b/Sources/OpenSwiftUI/View/Control/SensoryFeedback/UIKit/UIKitSensoryFeedbackCache.swift index 52af5db67..96e2c2384 100644 --- a/Sources/OpenSwiftUI/View/Control/SensoryFeedback/UIKit/UIKitSensoryFeedbackCache.swift +++ b/Sources/OpenSwiftUI/View/Control/SensoryFeedback/UIKit/UIKitSensoryFeedbackCache.swift @@ -136,7 +136,14 @@ extension CachedEnvironment.ID { extension _GraphInputs { var feedbackCache: Attribute { - mapEnvironment(id: .feedbackCache) { $0[FeedbackCacheKey.self].base } + mapEnvironment(id: .feedbackCache) { $0.feedbackCache } + } +} + +extension EnvironmentValues { + var feedbackCache: AnyUIKitSensoryFeedbackCache? { + get { self[FeedbackCacheKey.self].base } + set { self[FeedbackCacheKey.self] = WeakBox(newValue) } } } From 8096969d55449f72ca639317f7494fd232cf05c8 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sat, 13 Dec 2025 17:39:31 +0800 Subject: [PATCH 08/10] Add AppKitFeedbackImplementation --- .../AppKit/AppKitFeedbackImplementation.swift | 54 +++++++++++++++++++ .../SensoryFeedbackGenerator.swift | 6 +-- .../UIKit/UIKitFeedbackImplementation.swift | 2 +- 3 files changed, 58 insertions(+), 4 deletions(-) create mode 100644 Sources/OpenSwiftUI/View/Control/SensoryFeedback/AppKit/AppKitFeedbackImplementation.swift diff --git a/Sources/OpenSwiftUI/View/Control/SensoryFeedback/AppKit/AppKitFeedbackImplementation.swift b/Sources/OpenSwiftUI/View/Control/SensoryFeedback/AppKit/AppKitFeedbackImplementation.swift new file mode 100644 index 000000000..dc5cccf12 --- /dev/null +++ b/Sources/OpenSwiftUI/View/Control/SensoryFeedback/AppKit/AppKitFeedbackImplementation.swift @@ -0,0 +1,54 @@ +// +// AppKitFeedbackImplementation.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: Complete + +#if os(macOS) +import AppKit +import OpenSwiftUICore + +// MARK: - View + platformSensoryFeedback + +extension View { + nonisolated func platformSensoryFeedback( + _ base: Base + ) -> some View where Base: SensoryFeedbackGeneratorModifier { + modifier(base) + } +} + +// MARK: - HapticFeedbackManagerImplementation + +struct HapticFeedbackManagerImplementation: PlatformSensoryFeedback { + var pattern: NSHapticFeedbackManager.FeedbackPattern + + func setUp() { + _openSwiftUIEmptyStub() + } + + func tearDown() { + _openSwiftUIEmptyStub() + } + + func generate() { + NSHapticFeedbackManager.defaultPerformer.perform( + pattern, + performanceTime: .default + ) + } +} + +// MARK: - FeedbackRequestContext + +struct FeedbackRequestContext { + func implementation(type: SensoryFeedback.FeedbackType) -> (any PlatformSensoryFeedback)? { + switch type { + case .alignment: HapticFeedbackManagerImplementation(pattern: .alignment) + case .levelChange: HapticFeedbackManagerImplementation(pattern: .levelChange) + default: nil + } + } +} +#endif diff --git a/Sources/OpenSwiftUI/View/Control/SensoryFeedback/SensoryFeedbackGenerator.swift b/Sources/OpenSwiftUI/View/Control/SensoryFeedback/SensoryFeedbackGenerator.swift index 7739e8c60..002bc7384 100644 --- a/Sources/OpenSwiftUI/View/Control/SensoryFeedback/SensoryFeedbackGenerator.swift +++ b/Sources/OpenSwiftUI/View/Control/SensoryFeedback/SensoryFeedbackGenerator.swift @@ -42,7 +42,7 @@ extension View { ) -> some View where T: Equatable { platformSensoryFeedback( FeedbackGenerator( - feedbackRequestContext: .init(location: .init()), + feedbackRequestContext: .init(), feedback: feedback, trigger: trigger, condition: nil, @@ -86,7 +86,7 @@ extension View { ) -> some View where T: Equatable { platformSensoryFeedback( FeedbackGenerator( - feedbackRequestContext: .init(location: .init()), + feedbackRequestContext: .init(), feedback: feedback, trigger: trigger, condition: condition, @@ -133,7 +133,7 @@ extension View { ) -> some View where T: Equatable { platformSensoryFeedback( CustomFeedbackGenerator( - feedbackRequestContext: .init(location: .init()), + feedbackRequestContext: .init(), trigger: trigger, feedback: feedback ) diff --git a/Sources/OpenSwiftUI/View/Control/SensoryFeedback/UIKit/UIKitFeedbackImplementation.swift b/Sources/OpenSwiftUI/View/Control/SensoryFeedback/UIKit/UIKitFeedbackImplementation.swift index 11b4a4b74..aecbb009e 100644 --- a/Sources/OpenSwiftUI/View/Control/SensoryFeedback/UIKit/UIKitFeedbackImplementation.swift +++ b/Sources/OpenSwiftUI/View/Control/SensoryFeedback/UIKit/UIKitFeedbackImplementation.swift @@ -227,7 +227,7 @@ private struct FeedbackLocation: Rule { // MARK: - FeedbackRequestContext struct FeedbackRequestContext { - var location: WeakAttribute + var location: WeakAttribute = .init() weak var cache: AnyUIKitSensoryFeedbackCache? func implementation(type: SensoryFeedback.FeedbackType) -> (any PlatformSensoryFeedback)? { From 8d98525c22e793fe4c40070ad431b240b2bb5cef Mon Sep 17 00:00:00 2001 From: Kyle Date: Sat, 13 Dec 2025 20:34:59 +0800 Subject: [PATCH 09/10] Fix increase and decrease issue --- .../SensoryFeedback/UIKit/UIKitSensoryFeedbackCache.swift | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Sources/OpenSwiftUI/View/Control/SensoryFeedback/UIKit/UIKitSensoryFeedbackCache.swift b/Sources/OpenSwiftUI/View/Control/SensoryFeedback/UIKit/UIKitSensoryFeedbackCache.swift index 96e2c2384..a2730e19f 100644 --- a/Sources/OpenSwiftUI/View/Control/SensoryFeedback/UIKit/UIKitSensoryFeedbackCache.swift +++ b/Sources/OpenSwiftUI/View/Control/SensoryFeedback/UIKit/UIKitSensoryFeedbackCache.swift @@ -59,9 +59,9 @@ class UIKitSensoryFeedbackCache: AnyUIKitSensoryFeedbackCache where V: View { } createIfNeeded: { UINotificationFeedbackGenerator() } - case .levelChange, .start, .stop: - nil - case .increase, .decrease, .selection: + // SwiftUI implementation Bug: introduced since iOS 17 & iOS 26.2 is still not fixed + // FB21332474 + case /*.increase, .decrease,*/ .selection: getGenerator(type) { SelectionFeedbackImplementation( generator: $0 @@ -104,6 +104,7 @@ class UIKitSensoryFeedbackCache: AnyUIKitSensoryFeedbackCache where V: View { case .soft: UIImpactFeedbackGenerator(style: .soft) } } + default: nil } } From 17cef5034a6bd79d9db065158b078e5ee9619923 Mon Sep 17 00:00:00 2001 From: Kyle Date: Sat, 13 Dec 2025 22:03:35 +0800 Subject: [PATCH 10/10] Fix non Darwin platform build issue --- .../SensoryFeedbackGenerator.swift | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/Sources/OpenSwiftUI/View/Control/SensoryFeedback/SensoryFeedbackGenerator.swift b/Sources/OpenSwiftUI/View/Control/SensoryFeedback/SensoryFeedbackGenerator.swift index 002bc7384..36ca26246 100644 --- a/Sources/OpenSwiftUI/View/Control/SensoryFeedback/SensoryFeedbackGenerator.swift +++ b/Sources/OpenSwiftUI/View/Control/SensoryFeedback/SensoryFeedbackGenerator.swift @@ -226,3 +226,23 @@ private struct FeedbackGenerator: SensoryFeedbackGeneratorModifier where T: E } } } + +#if !canImport(Darwin) +// MARK: - View + platformSensoryFeedback + +extension View { + nonisolated func platformSensoryFeedback( + _ base: Base + ) -> some View where Base: SensoryFeedbackGeneratorModifier { + modifier(base) + } +} + +// MARK: - FeedbackRequestContext + +struct FeedbackRequestContext { + func implementation(type: SensoryFeedback.FeedbackType) -> (any PlatformSensoryFeedback)? { + nil + } +} +#endif