diff --git a/Sources/OpenSwiftUI/View/Control/SensoryFeedback/AppKit/AppKitFeedbackImplementation.swift b/Sources/OpenSwiftUI/View/Control/SensoryFeedback/AppKit/AppKitFeedbackImplementation.swift index dc5cccf12..735aa070d 100644 --- a/Sources/OpenSwiftUI/View/Control/SensoryFeedback/AppKit/AppKitFeedbackImplementation.swift +++ b/Sources/OpenSwiftUI/View/Control/SensoryFeedback/AppKit/AppKitFeedbackImplementation.swift @@ -43,8 +43,8 @@ struct HapticFeedbackManagerImplementation: PlatformSensoryFeedback { // MARK: - FeedbackRequestContext struct FeedbackRequestContext { - func implementation(type: SensoryFeedback.FeedbackType) -> (any PlatformSensoryFeedback)? { - switch type { + func implementation(_ feedback: SensoryFeedback) -> (any PlatformSensoryFeedback)? { + switch feedback.type { case .alignment: HapticFeedbackManagerImplementation(pattern: .alignment) case .levelChange: HapticFeedbackManagerImplementation(pattern: .levelChange) default: nil diff --git a/Sources/OpenSwiftUI/View/Control/SensoryFeedback/SensoryFeedback.swift b/Sources/OpenSwiftUI/View/Control/SensoryFeedback/SensoryFeedback.swift index 7302b6a2e..0a22909fb 100644 --- a/Sources/OpenSwiftUI/View/Control/SensoryFeedback/SensoryFeedback.swift +++ b/Sources/OpenSwiftUI/View/Control/SensoryFeedback/SensoryFeedback.swift @@ -13,7 +13,7 @@ @available(OpenSwiftUI_v5_0, *) @available(visionOS, unavailable) public struct SensoryFeedback: Equatable, Sendable { - enum FeedbackType: Hashable { + enum FeedbackType: Hashable, Sendable { case success case warning case error @@ -25,12 +25,26 @@ public struct SensoryFeedback: Equatable, Sendable { case start case stop case pathComplete - case impactWeight(SensoryFeedback.Weight.Storage, Double) - case impactFlexibility(SensoryFeedback.Flexibility.Storage, Double) + case impactWeight(SensoryFeedback.Weight.Storage) + case impactFlexibility(SensoryFeedback.Flexibility.Storage) } var type: FeedbackType + // Added in 8.0.66. + // See more infor on https://kyleye.top/posts/swiftui-sensoryfeedback-intensity-payload/ + enum Payload: Equatable, Sendable { + case intensity(Double) + case empty + } + + var payload: Payload + + init(type: FeedbackType, payload: Payload = .empty) { + self.type = type + self.payload = payload + } + /// Indicates that a task or action has completed. /// /// Only plays feedback on iOS and watchOS. @@ -114,7 +128,7 @@ public struct SensoryFeedback: Equatable, Sendable { /// feedback in response to it. /// /// Only plays feedback on iOS and watchOS. - public static let impact: SensoryFeedback = .init(type: .impactWeight(.light, 1.0)) + public static let impact: SensoryFeedback = .init(type: .impactWeight(.light), payload: .intensity(1.0)) /// Provides a physical metaphor you can use to complement a visual /// experience. @@ -128,7 +142,7 @@ public struct SensoryFeedback: Equatable, Sendable { /// /// 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)) + .init(type: .impactWeight(weight.storage), payload: .intensity(intensity)) } /// Provides a physical metaphor you can use to complement a visual @@ -143,7 +157,7 @@ public struct SensoryFeedback: Equatable, Sendable { /// /// 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)) + .init(type: .impactFlexibility(flexibility.storage), payload: .intensity(intensity)) } // MARK: - SensoryFeedback.Weight diff --git a/Sources/OpenSwiftUI/View/Control/SensoryFeedback/SensoryFeedbackGenerator.swift b/Sources/OpenSwiftUI/View/Control/SensoryFeedback/SensoryFeedbackGenerator.swift index 36ca26246..9f4d160d2 100644 --- a/Sources/OpenSwiftUI/View/Control/SensoryFeedback/SensoryFeedbackGenerator.swift +++ b/Sources/OpenSwiftUI/View/Control/SensoryFeedback/SensoryFeedbackGenerator.swift @@ -176,7 +176,7 @@ private struct CustomFeedbackGenerator: SensoryFeedbackGeneratorModifier wher if let newFeedback { newValue = ( newFeedback, - feedbackRequestContext.implementation(type: newFeedback.type) + feedbackRequestContext.implementation(newFeedback) ) } else { newValue = nil @@ -216,7 +216,7 @@ private struct FeedbackGenerator: SensoryFeedbackGeneratorModifier where T: E content .task(id: feedback) { implementation?.tearDown() - implementation = feedbackRequestContext.implementation(type: feedback.type) + implementation = feedbackRequestContext.implementation(feedback) implementation?.setUp() } .onChange(of: trigger) { oldValue, newValue in @@ -241,7 +241,7 @@ extension View { // MARK: - FeedbackRequestContext struct FeedbackRequestContext { - func implementation(type: SensoryFeedback.FeedbackType) -> (any PlatformSensoryFeedback)? { + func implementation(_ feedback: SensoryFeedback) -> (any PlatformSensoryFeedback)? { nil } } diff --git a/Sources/OpenSwiftUI/View/Control/SensoryFeedback/UIKit/UIKitFeedbackImplementation.swift b/Sources/OpenSwiftUI/View/Control/SensoryFeedback/UIKit/UIKitFeedbackImplementation.swift index aecbb009e..1a55689af 100644 --- a/Sources/OpenSwiftUI/View/Control/SensoryFeedback/UIKit/UIKitFeedbackImplementation.swift +++ b/Sources/OpenSwiftUI/View/Control/SensoryFeedback/UIKit/UIKitFeedbackImplementation.swift @@ -230,9 +230,9 @@ struct FeedbackRequestContext { var location: WeakAttribute = .init() weak var cache: AnyUIKitSensoryFeedbackCache? - func implementation(type: SensoryFeedback.FeedbackType) -> (any PlatformSensoryFeedback)? { + func implementation(_ feedback: SensoryFeedback) -> (any PlatformSensoryFeedback)? { guard let cache, - let feeback = cache.implementation(type: type), + let feeback = cache.implementation(feedback), let location = location.attribute else { return nil } diff --git a/Sources/OpenSwiftUI/View/Control/SensoryFeedback/UIKit/UIKitSensoryFeedbackCache.swift b/Sources/OpenSwiftUI/View/Control/SensoryFeedback/UIKit/UIKitSensoryFeedbackCache.swift index c9fa46ae7..5b9a9d161 100644 --- a/Sources/OpenSwiftUI/View/Control/SensoryFeedback/UIKit/UIKitSensoryFeedbackCache.swift +++ b/Sources/OpenSwiftUI/View/Control/SensoryFeedback/UIKit/UIKitSensoryFeedbackCache.swift @@ -16,7 +16,7 @@ import UIKit class AnyUIKitSensoryFeedbackCache { func implementation( - type: SensoryFeedback.FeedbackType + _ feedback: SensoryFeedback ) -> LocationBasedSensoryFeedback? { _openSwiftUIBaseClassAbstractMethod() } @@ -26,14 +26,14 @@ class AnyUIKitSensoryFeedbackCache { class UIKitSensoryFeedbackCache: AnyUIKitSensoryFeedbackCache where V: View { weak var host: _UIHostingView? - private var cachedGenerators: [GeneratorCacheKey: UIFeedbackGenerator] = [:] + private var cachedGenerators: [SensoryFeedback.FeedbackType: UIFeedbackGenerator] = [:] override func implementation( - type: SensoryFeedback.FeedbackType + _ feedback: SensoryFeedback ) -> LocationBasedSensoryFeedback? { - switch type { + switch feedback.type { case .success: - getGenerator(type) { + getGenerator(feedback.type) { NotificationFeedbackImplementation( generator: $0, type: .success @@ -42,7 +42,7 @@ class UIKitSensoryFeedbackCache: AnyUIKitSensoryFeedbackCache where V: View { UINotificationFeedbackGenerator() } case .warning: - getGenerator(type) { + getGenerator(feedback.type) { NotificationFeedbackImplementation( generator: $0, type: .warning @@ -51,7 +51,7 @@ class UIKitSensoryFeedbackCache: AnyUIKitSensoryFeedbackCache where V: View { UINotificationFeedbackGenerator() } case .error: - getGenerator(type) { + getGenerator(feedback.type) { NotificationFeedbackImplementation( generator: $0, type: .error @@ -63,7 +63,7 @@ class UIKitSensoryFeedbackCache: AnyUIKitSensoryFeedbackCache where V: View { // SwiftUI implementation Bug: introduced since iOS 17 & iOS 26.2 is still not fixed // FB21332474 case /*.increase, .decrease,*/ .selection: - getGenerator(type) { + getGenerator(feedback.type) { SelectionFeedbackImplementation( generator: $0 ) @@ -71,19 +71,24 @@ class UIKitSensoryFeedbackCache: AnyUIKitSensoryFeedbackCache where V: View { UISelectionFeedbackGenerator() } case .alignment, .pathComplete: - getGenerator(type) { + getGenerator(feedback.type) { CanvasFeedbackImplementation( generator: $0, - type: type + type: feedback.type ) } createIfNeeded: { UICanvasFeedbackGenerator() } - case let .impactWeight(weight, intensity): - getGenerator(type) { + case let .impactWeight(weight): + getGenerator(feedback.type) { ImpactFeedbackImplementation( generator: $0, - intensity: intensity + intensity: { + guard case let .intensity(intensity) = feedback.payload else { + fatalError("Misconfigured feedback") + } + return intensity + }() ) } createIfNeeded: { () -> UIImpactFeedbackGenerator in switch weight { @@ -92,11 +97,16 @@ class UIKitSensoryFeedbackCache: AnyUIKitSensoryFeedbackCache where V: View { case .heavy: UIImpactFeedbackGenerator(style: .heavy) } } - case let .impactFlexibility(flexibility, intensity): - getGenerator(type) { + case let .impactFlexibility(flexibility): + getGenerator(feedback.type) { ImpactFeedbackImplementation( generator: $0, - intensity: intensity + intensity: { + guard case let .intensity(intensity) = feedback.payload else { + fatalError("Misconfigured feedback") + } + return intensity + }() ) } createIfNeeded: { () -> UIImpactFeedbackGenerator in switch flexibility { @@ -109,56 +119,17 @@ class UIKitSensoryFeedbackCache: AnyUIKitSensoryFeedbackCache where V: View { } } - /* OpenSwiftUI Addition Begin */ - - // MARK: - GeneratorCacheKey - - /// A cache key that excludes intensity to prevent unbounded cache growth. - /// - /// Using `SensoryFeedback.FeedbackType` directly as cache key causes issues - /// because it includes the intensity value. Since intensity is a `Double`, - /// every different intensity creates a new cache entry and generator interaction. - /// The generator style (weight/flexibility) doesn't depend on intensity - - /// intensity is only used at feedback generation time. - private enum GeneratorCacheKey: Hashable { - case success - case warning - case error - case selection - case alignment - case pathComplete - case impactWeight(SensoryFeedback.Weight.Storage) - case impactFlexibility(SensoryFeedback.Flexibility.Storage) - - init?(_ type: SensoryFeedback.FeedbackType) { - switch type { - case .success: self = .success - case .warning: self = .warning - case .error: self = .error - case .selection: self = .selection - case .alignment: self = .alignment - case .pathComplete: self = .pathComplete - case let .impactWeight(weight, _): self = .impactWeight(weight) - case let .impactFlexibility(flexibility, _): self = .impactFlexibility(flexibility) - default: return nil - } - } - } - - /* OpenSwiftUI Addition End */ - private func getGenerator( _ type: SensoryFeedback.FeedbackType, work: (Generator) -> Feedback, createIfNeeded: () -> Generator ) -> Feedback? where Generator: UIFeedbackGenerator, Feedback: LocationBasedSensoryFeedback { - guard let cacheKey = GeneratorCacheKey(type) else { return nil } let generator: Generator - if let cachedGenerator = cachedGenerators[cacheKey] { + if let cachedGenerator = cachedGenerators[type] { generator = cachedGenerator as! Generator } else { generator = createIfNeeded() - cachedGenerators[cacheKey] = generator + cachedGenerators[type] = generator host!.addInteraction(generator) } return work(generator) diff --git a/Sources/OpenSwiftUI/View/Control/SensoryFeedback/WatchKit/WatchKitFeedbackImplementation.swift b/Sources/OpenSwiftUI/View/Control/SensoryFeedback/WatchKit/WatchKitFeedbackImplementation.swift index cee097431..8f2fd5605 100644 --- a/Sources/OpenSwiftUI/View/Control/SensoryFeedback/WatchKit/WatchKitFeedbackImplementation.swift +++ b/Sources/OpenSwiftUI/View/Control/SensoryFeedback/WatchKit/WatchKitFeedbackImplementation.swift @@ -40,8 +40,8 @@ struct WatchKitFeedbackImplementation: PlatformSensoryFeedback { // MARK: - FeedbackRequestContext struct FeedbackRequestContext { - func implementation(type: SensoryFeedback.FeedbackType) -> (any PlatformSensoryFeedback)? { - switch type { + func implementation(_ feedback: SensoryFeedback) -> (any PlatformSensoryFeedback)? { + switch feedback.type { case .success: WatchKitFeedbackImplementation(haptic: .success) case .warning: WatchKitFeedbackImplementation(haptic: .retry) case .error: WatchKitFeedbackImplementation(haptic: .failure)