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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ private struct CustomFeedbackGenerator<T>: SensoryFeedbackGeneratorModifier wher
if let newFeedback {
newValue = (
newFeedback,
feedbackRequestContext.implementation(type: newFeedback.type)
feedbackRequestContext.implementation(newFeedback)
)
} else {
newValue = nil
Expand Down Expand Up @@ -216,7 +216,7 @@ private struct FeedbackGenerator<T>: 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
Expand All @@ -241,7 +241,7 @@ extension View {
// MARK: - FeedbackRequestContext

struct FeedbackRequestContext {
func implementation(type: SensoryFeedback.FeedbackType) -> (any PlatformSensoryFeedback)? {
func implementation(_ feedback: SensoryFeedback) -> (any PlatformSensoryFeedback)? {
nil
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,9 +230,9 @@ struct FeedbackRequestContext {
var location: WeakAttribute<CGPoint> = .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
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import UIKit

class AnyUIKitSensoryFeedbackCache {
func implementation(
type: SensoryFeedback.FeedbackType
_ feedback: SensoryFeedback
) -> LocationBasedSensoryFeedback? {
_openSwiftUIBaseClassAbstractMethod()
}
Expand All @@ -26,14 +26,14 @@ class AnyUIKitSensoryFeedbackCache {

class UIKitSensoryFeedbackCache<V>: AnyUIKitSensoryFeedbackCache where V: View {
weak var host: _UIHostingView<V>?
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
Expand All @@ -42,7 +42,7 @@ class UIKitSensoryFeedbackCache<V>: AnyUIKitSensoryFeedbackCache where V: View {
UINotificationFeedbackGenerator()
}
case .warning:
getGenerator(type) {
getGenerator(feedback.type) {
NotificationFeedbackImplementation(
generator: $0,
type: .warning
Expand All @@ -51,7 +51,7 @@ class UIKitSensoryFeedbackCache<V>: AnyUIKitSensoryFeedbackCache where V: View {
UINotificationFeedbackGenerator()
}
case .error:
getGenerator(type) {
getGenerator(feedback.type) {
NotificationFeedbackImplementation(
generator: $0,
type: .error
Expand All @@ -63,27 +63,32 @@ class UIKitSensoryFeedbackCache<V>: 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
)
} createIfNeeded: {
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 {
Expand All @@ -92,11 +97,16 @@ class UIKitSensoryFeedbackCache<V>: 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 {
Expand All @@ -109,56 +119,17 @@ class UIKitSensoryFeedbackCache<V>: 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<Generator, Feedback>(
_ 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading