From 49c979dd19654f215d94278f97658a7524486d11 Mon Sep 17 00:00:00 2001 From: Kyle Date: Fri, 12 Jun 2026 02:36:34 +0800 Subject: [PATCH 1/5] Implement TapGesture --- .../Event/Gesture/RepeatGesture.swift | 1 + .../Event/Gesture/TapGesture.swift | 167 ++++++++++++++++++ 2 files changed, 168 insertions(+) create mode 100644 Sources/OpenSwiftUICore/Event/Gesture/TapGesture.swift diff --git a/Sources/OpenSwiftUICore/Event/Gesture/RepeatGesture.swift b/Sources/OpenSwiftUICore/Event/Gesture/RepeatGesture.swift index 100b99478..88aeae7f4 100644 --- a/Sources/OpenSwiftUICore/Event/Gesture/RepeatGesture.swift +++ b/Sources/OpenSwiftUICore/Event/Gesture/RepeatGesture.swift @@ -24,6 +24,7 @@ package struct RepeatGesture: GestureModifier { package var maximumDelay: Double package init(count: Int, maximumDelay: Double = 0.35) { + precondition(count > 0, "count must be positive") self.count = count self.maximumDelay = maximumDelay } diff --git a/Sources/OpenSwiftUICore/Event/Gesture/TapGesture.swift b/Sources/OpenSwiftUICore/Event/Gesture/TapGesture.swift new file mode 100644 index 000000000..271df0399 --- /dev/null +++ b/Sources/OpenSwiftUICore/Event/Gesture/TapGesture.swift @@ -0,0 +1,167 @@ +// +// TapGesture.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete +// ID: 067A6A2846A89ACCD702678B6B8D0D6F (SwiftUICore) + +package import Foundation +import OpenAttributeGraphShims + +// MARK: - TapGesture + +/// A gesture that recognizes one or more taps. +/// +/// To recognize a tap gesture on a view, create and configure the gesture, and +/// then add it to the view using the ``View/gesture(_:including:)`` modifier. +/// The following code adds a tap gesture to a ``Circle`` that toggles the color +/// of the circle: +/// +/// struct TapGestureView: View { +/// @State private var tapped = false +/// +/// var tap: some Gesture { +/// TapGesture(count: 1) +/// .onEnded { _ in self.tapped = !self.tapped } +/// } +/// +/// var body: some View { +/// Circle() +/// .fill(self.tapped ? Color.blue : Color.red) +/// .frame(width: 100, height: 100, alignment: .center) +/// .gesture(tap) +/// } +/// } +@available(OpenSwiftUI_v1_0, *) +public struct TapGesture: PrimitiveGesture, Gesture { + private struct Phase: Rule { + @Attribute var phase: GesturePhase + + typealias Value = GesturePhase + + var value: GesturePhase { + phase.withValue(()) + } + } + + private struct Child: Rule { + @Attribute var gesture: TapGesture + + var value: some Gesture { + SingleTapGesture() + .repeatCount(gesture.count) + .category(gesture.count == 1 ? .select : []) + .requiredTapCount(gesture.count) + } + } + + /// The required number of tap events. + public var count: Int + + /// Creates a tap gesture with the number of required taps. + /// + /// - Parameter count: The required number of taps to complete the tap + /// gesture. + public init(count: Int = 1) { + self.count = count + } + + nonisolated public static func _makeGesture( + gesture: _GraphValue, + inputs: _GestureInputs + ) -> _GestureOutputs { + let child = Attribute(Child(gesture: gesture.value)) + let outputs = Child.Value.makeDebuggableGesture( + gesture: _GraphValue(child), + inputs: inputs + ) + let phase = Attribute(Phase(phase: outputs.phase)) + return outputs.withPhase(phase) + } + + public typealias Value = Void +} + +@available(*, unavailable) +extension TapGesture: Sendable {} + +// MARK: - View + TapGesture + +@available(OpenSwiftUI_v1_0, *) +extension View { + /// Adds an action to perform when this view recognizes a tap gesture. + /// + /// Use this method to perform the specified `action` when the user clicks + /// or taps on the view or container `count` times. + /// + /// > Note: If you create a control that's functionally equivalent + /// to a ``Button``, use ``ButtonStyle`` to create a customized button + /// instead. + /// + /// In the example below, the color of the heart images changes to a random + /// color from the `colors` array whenever the user clicks or taps on the + /// view twice: + /// + /// struct TapGestureExample: View { + /// let colors: [Color] = [.gray, .red, .orange, .yellow, + /// .green, .blue, .purple, .pink] + /// @State private var fgColor: Color = .gray + /// + /// var body: some View { + /// Image(systemName: "heart.fill") + /// .resizable() + /// .frame(width: 200, height: 200) + /// .foregroundColor(fgColor) + /// .onTapGesture(count: 2) { + /// fgColor = colors.randomElement()! + /// } + /// } + /// } + /// + /// ![A screenshot of a view of a heart.](OpenSwiftUI-View-TapGesture.png) + /// + /// - Parameters: + /// - count: The number of taps or clicks required to trigger the action + /// closure provided in `action`. Defaults to `1`. + /// - action: The action to perform. + nonisolated public func onTapGesture( + count: Int = 1, + perform action: @escaping () -> Void + ) -> some View { + gesture( + TapGesture(count: count).onEnded { _ in + action() + }, + including: .all + ) + } +} + +// MARK: - Tap Threshold Constant + +package let tapMovementThreshold: CGFloat = 45 + +package let tapDurationThreshold: CGFloat = 0.75 + +// MARK: - SingleTapGesture + +package struct SingleTapGesture: Gesture where BaseEvent: TappableEventType { + package init() {} + +// typealias Body = ModifierGesture, ModifierGesture, DistanceGesture>, A>, ModifierGesture, EventListener>, A>, ModifierGesture<(DependentGesture in _8687835E41FEE17B108D67665C1D2D0B), ModifierGesture, EventListener>>>>> + + + package var body: some Gesture { + EventListener() + .discrete() + .dependency(.failIfActive) + .gated(by: EventListener().duration(maximum: tapDurationThreshold)) + .gated(by: DistanceGesture(maximumDistance: tapMovementThreshold).coordinateSpace(.global)) + .eventFilter(forType: MouseEvent.self) { event in + event.button == .primary + } + } + + package typealias Value = BaseEvent +} From 7f1b8920c8b1339f5478aa7e0f56cbbe71cd54b8 Mon Sep 17 00:00:00 2001 From: Kyle Date: Fri, 12 Jun 2026 03:19:00 +0800 Subject: [PATCH 2/5] Add TapGestureExample --- .../Event/Gesture/TapGestureExample.swift | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 Example/Shared/Event/Gesture/TapGestureExample.swift diff --git a/Example/Shared/Event/Gesture/TapGestureExample.swift b/Example/Shared/Event/Gesture/TapGestureExample.swift new file mode 100644 index 000000000..f1695699a --- /dev/null +++ b/Example/Shared/Event/Gesture/TapGestureExample.swift @@ -0,0 +1,25 @@ +// +// TapGestureExample.swift +// Shared + +#if OPENSWIFTUI +import OpenSwiftUI +#else +import SwiftUI +#endif + +struct TapGestureExample: View { + let colors: [Color] = [.gray, .red, .orange, .yellow, + .green, .blue, .purple, .pink] + @State private var fgColor: Color = .gray + + var body: some View { + Image(systemName: "heart.fill") + .resizable() + .frame(width: 200, height: 200) + .foregroundColor(fgColor) + .onTapGesture(count: 2) { + fgColor = colors.randomElement()! + } + } +} From d176221e5c66216ed6b1dacae688cd52f20dd35a Mon Sep 17 00:00:00 2001 From: Kyle Date: Fri, 12 Jun 2026 22:31:41 +0800 Subject: [PATCH 3/5] Add SpatialTapGesture --- .../Event/Gesture/SpatialTapGesture.swift | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 Sources/OpenSwiftUI/Event/Gesture/SpatialTapGesture.swift diff --git a/Sources/OpenSwiftUI/Event/Gesture/SpatialTapGesture.swift b/Sources/OpenSwiftUI/Event/Gesture/SpatialTapGesture.swift new file mode 100644 index 000000000..cc88799b2 --- /dev/null +++ b/Sources/OpenSwiftUI/Event/Gesture/SpatialTapGesture.swift @@ -0,0 +1,125 @@ +// +// SpatialTapGesture.swift +// OpenSwiftUI +// +// Audited for 6.5.4 +// Status: Complete +// ID: 4CBCFA1A8492A311E8B21AE224C33BFC (SwiftUI) + +@_spi(ForOpenSwiftUIOnly) public import OpenSwiftUICore +import OpenAttributeGraphShims + +// MARK: - SpatialTapGesture + +@available(iOS 16.0, macOS 13.0, watchOS 9.0, visionOS 1.0, *) +@_spi_available(tvOS, introduced: 17.0) +public struct SpatialTapGesture: PrimitiveGesture, Gesture { + private struct Phase: Rule { + @Attribute var phase: GesturePhase + + typealias Value = GesturePhase + + var value: GesturePhase { + phase.map { event in + SpatialTapGesture.Value(location: event.location) + } + } + } + + private struct Child: Rule { + @Attribute var gesture: SpatialTapGesture + + var value: some Gesture { + SingleTapGesture() + .repeatCount(gesture.count) + .coordinateSpace(gesture.coordinateSpace) + .requiredTapCount(gesture.count) + } + } + + public struct Value: Equatable, @unchecked Sendable { + public var location: CGPoint + } + + public var count: Int + + public var coordinateSpace: CoordinateSpace + + @available(iOS, introduced: 16.0, deprecated: 100000.0, message: "use overload that accepts a CoordinateSpaceProtocol instead") + @available(macOS, introduced: 13.0, deprecated: 100000.0, message: "use overload that accepts a CoordinateSpaceProtocol instead") + @available(watchOS, introduced: 9.0, deprecated: 100000.0, message: "use overload that accepts a CoordinateSpaceProtocol instead") + @available(tvOS, unavailable) + @available(visionOS, introduced: 1.0, deprecated: 100000.0, message: "use overload that accepts a CoordinateSpaceProtocol instead") + @_disfavoredOverload + public init(count: Int = 1, coordinateSpace: CoordinateSpace = .local) { + self.count = count + self.coordinateSpace = coordinateSpace + } + + @available(iOS 17.0, macOS 14.0, watchOS 10.0, *) + @_spi_available(tvOS, introduced: 17.0) + public init(count: Int = 1, coordinateSpace: some CoordinateSpaceProtocol = .local) { + self.count = count + self.coordinateSpace = coordinateSpace.coordinateSpace + } + + nonisolated public static func _makeGesture( + gesture: _GraphValue, + inputs: _GestureInputs + ) -> _GestureOutputs { + let child = Attribute(Child(gesture: gesture.value)) + let outputs = Child.Value.makeDebuggableGesture( + gesture: _GraphValue(child), + inputs: inputs + ) + let phase = Attribute(Phase(phase: outputs.phase)) + return outputs.withPhase(phase) + } + + public typealias Body = Never +} + +@available(*, unavailable) +extension SpatialTapGesture: Sendable {} + +// MARK: - View + SpatialTapGesture + +@available(iOS 16.0, macOS 13.0, watchOS 9.0, visionOS 1.0, *) +@available(tvOS, unavailable) +extension View { + @available(iOS, introduced: 16.0, deprecated: 100000.0, message: "use overload that accepts a CoordinateSpaceProtocol instead") + @available(macOS, introduced: 13.0, deprecated: 100000.0, message: "use overload that accepts a CoordinateSpaceProtocol instead") + @available(watchOS, introduced: 9.0, deprecated: 100000.0, message: "use overload that accepts a CoordinateSpaceProtocol instead") + @available(tvOS, unavailable) + @available(visionOS, introduced: 1.0, deprecated: 100000.0, message: "use overload that accepts a CoordinateSpaceProtocol instead") + @_disfavoredOverload + nonisolated public func onTapGesture( + count: Int = 1, + coordinateSpace: CoordinateSpace = .local, + perform action: @escaping (CGPoint) -> Void + ) -> some View { + gesture( + SpatialTapGesture(count: count, coordinateSpace: coordinateSpace).onEnded { value in + action(value.location) + }, + including: .all + ) + } +} + +@available(iOS 17.0, macOS 14.0, watchOS 10.0, *) +@_spi_available(tvOS, introduced: 17.0) +extension View { + nonisolated public func onTapGesture( + count: Int = 1, + coordinateSpace: some CoordinateSpaceProtocol = .local, + perform action: @escaping (CGPoint) -> Void + ) -> some View { + gesture( + SpatialTapGesture(count: count, coordinateSpace: coordinateSpace).onEnded { value in + action(value.location) + }, + including: .all + ) + } +} From 53699226036cd817c3f68ae145e84ba14e23e7b0 Mon Sep 17 00:00:00 2001 From: Kyle Date: Fri, 12 Jun 2026 22:42:29 +0800 Subject: [PATCH 4/5] Add documentation and fix _spi_available --- .../Event/Gesture/SpatialTapGesture.swift | 131 ++++++++++++++++-- .../View/Image/ImageDynamicRange.swift | 6 +- 2 files changed, 120 insertions(+), 17 deletions(-) diff --git a/Sources/OpenSwiftUI/Event/Gesture/SpatialTapGesture.swift b/Sources/OpenSwiftUI/Event/Gesture/SpatialTapGesture.swift index cc88799b2..a8c4c1a6b 100644 --- a/Sources/OpenSwiftUI/Event/Gesture/SpatialTapGesture.swift +++ b/Sources/OpenSwiftUI/Event/Gesture/SpatialTapGesture.swift @@ -11,7 +11,31 @@ import OpenAttributeGraphShims // MARK: - SpatialTapGesture -@available(iOS 16.0, macOS 13.0, watchOS 9.0, visionOS 1.0, *) +/// A gesture that recognizes one or more taps and reports their location. +/// +/// To recognize a tap gesture on a view, create and configure the gesture, and +/// then add it to the view using the ``View/gesture(_:including:)`` modifier. +/// The following code adds a tap gesture to a ``Circle`` that toggles the color +/// of the circle based on the tap location: +/// +/// struct TapGestureView: View { +/// @State private var location: CGPoint = .zero +/// +/// var tap: some Gesture { +/// SpatialTapGesture() +/// .onEnded { event in +/// self.location = event.location +/// } +/// } +/// +/// var body: some View { +/// Circle() +/// .fill(self.location.y > 50 ? Color.blue : Color.red) +/// .frame(width: 100, height: 100, alignment: .center) +/// .gesture(tap) +/// } +/// } +@available(OpenSwiftUI_v4_0, *) @_spi_available(tvOS, introduced: 17.0) public struct SpatialTapGesture: PrimitiveGesture, Gesture { private struct Phase: Rule { @@ -37,26 +61,41 @@ public struct SpatialTapGesture: PrimitiveGesture, Gesture { } } + /// The attributes of a tap gesture. public struct Value: Equatable, @unchecked Sendable { + /// The location of the tap gesture's current event. public var location: CGPoint } + /// The required number of tap events. public var count: Int + /// The coordinate space in which to receive location values. public var coordinateSpace: CoordinateSpace - @available(iOS, introduced: 16.0, deprecated: 100000.0, message: "use overload that accepts a CoordinateSpaceProtocol instead") - @available(macOS, introduced: 13.0, deprecated: 100000.0, message: "use overload that accepts a CoordinateSpaceProtocol instead") - @available(watchOS, introduced: 9.0, deprecated: 100000.0, message: "use overload that accepts a CoordinateSpaceProtocol instead") + /// Creates a tap gesture with the number of required taps and the + /// coordinate space of the gesture's location. + /// + /// - Parameters: + /// - count: The required number of taps to complete the tap + /// gesture. + /// - coordinateSpace: The coordinate space of the tap gesture's location. + @available(*, deprecated, message: "use overload that accepts a CoordinateSpaceProtocol instead") @available(tvOS, unavailable) - @available(visionOS, introduced: 1.0, deprecated: 100000.0, message: "use overload that accepts a CoordinateSpaceProtocol instead") @_disfavoredOverload public init(count: Int = 1, coordinateSpace: CoordinateSpace = .local) { self.count = count self.coordinateSpace = coordinateSpace } - @available(iOS 17.0, macOS 14.0, watchOS 10.0, *) + /// Creates a tap gesture with the number of required taps and the + /// coordinate space of the gesture's location. + /// + /// - Parameters: + /// - count: The required number of taps to complete the tap + /// gesture. + /// - coordinateSpace: The coordinate space of the tap gesture's location. + @available(OpenSwiftUI_v5_0, *) @_spi_available(tvOS, introduced: 17.0) public init(count: Int = 1, coordinateSpace: some CoordinateSpaceProtocol = .local) { self.count = count @@ -68,7 +107,7 @@ public struct SpatialTapGesture: PrimitiveGesture, Gesture { inputs: _GestureInputs ) -> _GestureOutputs { let child = Attribute(Child(gesture: gesture.value)) - let outputs = Child.Value.makeDebuggableGesture( + let outputs = Child.Value._makeGesture( gesture: _GraphValue(child), inputs: inputs ) @@ -84,14 +123,44 @@ extension SpatialTapGesture: Sendable {} // MARK: - View + SpatialTapGesture -@available(iOS 16.0, macOS 13.0, watchOS 9.0, visionOS 1.0, *) +@available(OpenSwiftUI_v4_0, *) @available(tvOS, unavailable) extension View { - @available(iOS, introduced: 16.0, deprecated: 100000.0, message: "use overload that accepts a CoordinateSpaceProtocol instead") - @available(macOS, introduced: 13.0, deprecated: 100000.0, message: "use overload that accepts a CoordinateSpaceProtocol instead") - @available(watchOS, introduced: 9.0, deprecated: 100000.0, message: "use overload that accepts a CoordinateSpaceProtocol instead") - @available(tvOS, unavailable) - @available(visionOS, introduced: 1.0, deprecated: 100000.0, message: "use overload that accepts a CoordinateSpaceProtocol instead") + /// Adds an action to perform when this view recognizes a tap gesture, + /// and provides the action with the location of the interaction. + /// + /// Use this method to perform the specified `action` when the user clicks + /// or taps on the modified view `count` times. The action closure receives + /// the location of the interaction. + /// + /// > Note: If you create a control that's functionally equivalent + /// to a ``Button``, use ``ButtonStyle`` to create a customized button + /// instead. + /// + /// The following code adds a tap gesture to a ``Circle`` that toggles the color + /// of the circle based on the tap location. + /// + /// struct TapGestureExample: View { + /// @State private var location: CGPoint = .zero + /// + /// var body: some View { + /// Circle() + /// .fill(self.location.y > 50 ? Color.blue : Color.red) + /// .frame(width: 100, height: 100, alignment: .center) + /// .onTapGesture { location in + /// self.location = location + /// } + /// } + /// } + /// + /// - Parameters: + /// - count: The number of taps or clicks required to trigger the action + /// closure provided in `action`. Defaults to `1`. + /// - coordinateSpace: The coordinate space in which to receive + /// location values. Defaults to ``CoordinateSpace/local``. + /// - action: The action to perform. This closure receives an input + /// that indicates where the interaction occurred. + @available(*, deprecated, message: "use overload that accepts a CoordinateSpaceProtocol instead") @_disfavoredOverload nonisolated public func onTapGesture( count: Int = 1, @@ -107,9 +176,43 @@ extension View { } } -@available(iOS 17.0, macOS 14.0, watchOS 10.0, *) +@available(OpenSwiftUI_v5_0, *) @_spi_available(tvOS, introduced: 17.0) extension View { + /// Adds an action to perform when this view recognizes a tap gesture, + /// and provides the action with the location of the interaction. + /// + /// Use this method to perform the specified `action` when the user clicks + /// or taps on the modified view `count` times. The action closure receives + /// the location of the interaction. + /// + /// > Note: If you create a control that's functionally equivalent + /// to a ``Button``, use ``ButtonStyle`` to create a customized button + /// instead. + /// + /// The following code adds a tap gesture to a ``Circle`` that toggles the color + /// of the circle based on the tap location. + /// + /// struct TapGestureExample: View { + /// @State private var location: CGPoint = .zero + /// + /// var body: some View { + /// Circle() + /// .fill(self.location.y > 50 ? Color.blue : Color.red) + /// .frame(width: 100, height: 100, alignment: .center) + /// .onTapGesture { location in + /// self.location = location + /// } + /// } + /// } + /// + /// - Parameters: + /// - count: The number of taps or clicks required to trigger the action + /// closure provided in `action`. Defaults to `1`. + /// - coordinateSpace: The coordinate space in which to receive + /// location values. Defaults to ``CoordinateSpace/local``. + /// - action: The action to perform. This closure receives an input + /// that indicates where the interaction occurred. nonisolated public func onTapGesture( count: Int = 1, coordinateSpace: some CoordinateSpaceProtocol = .local, diff --git a/Sources/OpenSwiftUICore/View/Image/ImageDynamicRange.swift b/Sources/OpenSwiftUICore/View/Image/ImageDynamicRange.swift index 88470433c..1c60b1a58 100644 --- a/Sources/OpenSwiftUICore/View/Image/ImageDynamicRange.swift +++ b/Sources/OpenSwiftUICore/View/Image/ImageDynamicRange.swift @@ -11,7 +11,7 @@ package import Foundation // MARK: - Image.DynamicRange @available(OpenSwiftUI_v5_0, *) -// @_spi_available(watchOS, introduced: 10.0) +@_spi_available(watchOS, introduced: 10.0) extension Image { public struct DynamicRange: Hashable, Sendable { package enum Storage: UInt8, Hashable, Comparable { @@ -123,7 +123,7 @@ private struct AllowedDynamicRangeKey : EnvironmentKey { } @available(OpenSwiftUI_v5_0, *) -// @_spi_available(watchOS, introduced: 10.0) +@_spi_available(watchOS, introduced: 10.0) extension EnvironmentValues { /// The allowed dynamic range for the view, or nil. public var allowedDynamicRange: Image.DynamicRange? { @@ -145,7 +145,7 @@ extension EnvironmentValues { } } -@available(iOS 17.0, macOS 14.0, tvOS 17.0, *) +@available(OpenSwiftUI_v5_0, *) @_spi_available(watchOS, introduced: 10.0) extension View { From 74cc81630f894eddda92301263ae301263afbe90 Mon Sep 17 00:00:00 2001 From: Kyle Date: Fri, 12 Jun 2026 22:51:08 +0800 Subject: [PATCH 5/5] Add SpatialTapGesture example --- .../Gesture/SpatialTapGestureExample.swift | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 Example/Shared/Event/Gesture/SpatialTapGestureExample.swift diff --git a/Example/Shared/Event/Gesture/SpatialTapGestureExample.swift b/Example/Shared/Event/Gesture/SpatialTapGestureExample.swift new file mode 100644 index 000000000..ee9dbf219 --- /dev/null +++ b/Example/Shared/Event/Gesture/SpatialTapGestureExample.swift @@ -0,0 +1,27 @@ +// +// SpatialTapGestureExample.swift +// Shared + +#if OPENSWIFTUI +import OpenSwiftUI +#else +import SwiftUI +#endif + +struct SpatialTapGestureExample: View { + @State private var location: CGPoint = .zero + + var tap: some Gesture { + SpatialTapGesture() + .onEnded { event in + location = event.location + } + } + + var body: some View { + Circle() + .fill(location.y > 50 ? Color.blue : Color.red) + .frame(width: 100, height: 100, alignment: .center) + .gesture(tap) + } +}