From 6b28f7f42c8503fc2c6e62171d1041c372405dad Mon Sep 17 00:00:00 2001 From: Kyle Date: Wed, 10 Jun 2026 00:50:06 +0800 Subject: [PATCH 1/6] Add optional gesture conformance --- .../Event/Gesture/GestureViewModifier.swift | 17 ------ .../Event/Gesture/OptionalGesture.swift | 55 +++++++++++++++++++ 2 files changed, 55 insertions(+), 17 deletions(-) create mode 100644 Sources/OpenSwiftUICore/Event/Gesture/OptionalGesture.swift diff --git a/Sources/OpenSwiftUICore/Event/Gesture/GestureViewModifier.swift b/Sources/OpenSwiftUICore/Event/Gesture/GestureViewModifier.swift index 39f71e3dc..4a77262e7 100644 --- a/Sources/OpenSwiftUICore/Event/Gesture/GestureViewModifier.swift +++ b/Sources/OpenSwiftUICore/Event/Gesture/GestureViewModifier.swift @@ -651,20 +651,3 @@ private struct ContentPhase: ResettableGestureRule { value = phase.withValue(()) } } - -// MARK: - Optional: Gesture [WIP] - -extension Optional: Gesture where Wrapped: Gesture { - public typealias Value = Wrapped.Value - - nonisolated public static func _makeGesture( - gesture: _GraphValue>, - inputs: _GestureInputs - ) -> _GestureOutputs { - _openSwiftUIUnimplementedFailure() - } - - public typealias Body = Never -} - -extension Optional: PrimitiveGesture where Wrapped: Gesture {} diff --git a/Sources/OpenSwiftUICore/Event/Gesture/OptionalGesture.swift b/Sources/OpenSwiftUICore/Event/Gesture/OptionalGesture.swift new file mode 100644 index 000000000..d341b82cb --- /dev/null +++ b/Sources/OpenSwiftUICore/Event/Gesture/OptionalGesture.swift @@ -0,0 +1,55 @@ +// +// OptionalGesture.swift +// OpenSwiftUICore +// +// Audited for 6.5.4 +// Status: Complete +// ID: AA5A5D08CE822AD3F841F82D9B77CD0F (SwiftUICore) + +import OpenAttributeGraphShims + +// MARK: - Optional + Gesture + +@available(OpenSwiftUI_v1_0, *) +extension Optional: Gesture where Wrapped: Gesture { + private struct Child: Rule { + @Attribute var gesture: Wrapped? + + typealias Value = AnyGesture + + var value: AnyGesture { + gesture.map { AnyGesture($0) } ?? AnyGesture(Empty()) + } + } + + private struct Empty: PrimitiveGesture { + typealias Value = Wrapped.Value + + nonisolated static func _makeGesture( + gesture: _GraphValue, + inputs: _GestureInputs + ) -> _GestureOutputs { + _GestureOutputs(phase: Attribute(value: GesturePhase.failed)) + } + + typealias Body = Never + } + + public typealias Value = Wrapped.Value + + nonisolated public static func _makeGesture( + gesture: _GraphValue>, + inputs: _GestureInputs + ) -> _GestureOutputs { + let child = Attribute(Child(gesture: gesture.value)) + return AnyGesture.makeDebuggableGesture( + gesture: _GraphValue(child), + inputs: inputs + ) + } + + public typealias Body = Never +} + +@available(OpenSwiftUI_v1_0, *) +extension Optional: PrimitiveGesture where Wrapped: Gesture {} From 006378c7715eac50c29b37bc4df94a74639e5321 Mon Sep 17 00:00:00 2001 From: Kyle Date: Wed, 10 Jun 2026 02:31:10 +0800 Subject: [PATCH 2/6] Fix spi + ForOpenSwiftUIOnly --- .../OpenSwiftUICore/Event/Gesture/GestureDependency.swift | 1 + Sources/OpenSwiftUICore/Event/Gesture/Map2Gesture.swift | 5 +---- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/Sources/OpenSwiftUICore/Event/Gesture/GestureDependency.swift b/Sources/OpenSwiftUICore/Event/Gesture/GestureDependency.swift index 7994e3130..d7e9a931f 100644 --- a/Sources/OpenSwiftUICore/Event/Gesture/GestureDependency.swift +++ b/Sources/OpenSwiftUICore/Event/Gesture/GestureDependency.swift @@ -75,6 +75,7 @@ private struct DependentPhase: Rule { } } +@_spi(ForOpenSwiftUIOnly) extension GesturePhase { func paused() -> GesturePhase { switch self { diff --git a/Sources/OpenSwiftUICore/Event/Gesture/Map2Gesture.swift b/Sources/OpenSwiftUICore/Event/Gesture/Map2Gesture.swift index 09ebbee47..bfa9f83c6 100644 --- a/Sources/OpenSwiftUICore/Event/Gesture/Map2Gesture.swift +++ b/Sources/OpenSwiftUICore/Event/Gesture/Map2Gesture.swift @@ -80,9 +80,8 @@ extension Gesture { // MARK: - GesturePhase + combining -@_spi(ForSwiftUIOnly) +@_spi(ForOpenSwiftUIOnly) extension GesturePhase { - @_spi(ForSwiftUIOnly) package func and( _ phase: GesturePhase, value transform: (Wrapped, Other) -> Result @@ -101,14 +100,12 @@ extension GesturePhase { } } - @_spi(ForSwiftUIOnly) package func and( _ phase: GesturePhase ) -> GesturePhase<(Wrapped, Other)> { and(phase) { ($0, $1) } } - @_spi(ForSwiftUIOnly) package func and( _ phase: GesturePhase ) -> GesturePhase { From 8d324692a3693f1c80ffc2890c41e30eb7bab4bc Mon Sep 17 00:00:00 2001 From: Kyle Date: Wed, 10 Jun 2026 00:50:12 +0800 Subject: [PATCH 3/6] Implement gesture debug wrapping --- .../Event/Gesture/GestureDebug.swift | 173 ++++++++++++++++-- 1 file changed, 159 insertions(+), 14 deletions(-) diff --git a/Sources/OpenSwiftUICore/Event/Gesture/GestureDebug.swift b/Sources/OpenSwiftUICore/Event/Gesture/GestureDebug.swift index 466ec0c6e..820537bd2 100644 --- a/Sources/OpenSwiftUICore/Event/Gesture/GestureDebug.swift +++ b/Sources/OpenSwiftUICore/Event/Gesture/GestureDebug.swift @@ -10,10 +10,12 @@ package import Foundation package import OpenAttributeGraphShims import OpenSwiftUI_SPI +// MARK: - GestureDebug + package enum GestureDebug { package typealias Properties = ArrayWith2Inline<(String, String)> - package enum Kind { + package enum Kind: Hashable { case empty case primitive case modifier @@ -38,11 +40,19 @@ package enum GestureDebug { package typealias Children = ArrayWith2Inline package var children: GestureDebug.Data.Children { - get { _openSwiftUIUnimplementedFailure() } - set { _openSwiftUIUnimplementedFailure() } + get { + switch childrenBox { + case let .value(children): + children + } + } + set { + childrenBox = .value(newValue) + } } package init() { + let box: ChildrenBox = .value(ArrayWith2Inline()) kind = .empty type = EmptyGesture<()>.self phase = .failed @@ -50,7 +60,7 @@ package enum GestureDebug { resetSeed = 0 frame = .zero properties = .init() - childrenBox = .value([]) // FIXME + childrenBox = box } package init( @@ -63,12 +73,61 @@ package enum GestureDebug { frame: CGRect, properties: GestureDebug.Properties ) { - _openSwiftUIUnimplementedFailure() + let box: ChildrenBox = .value(children) + self.kind = kind + self.type = type + self.phase = phase + self.attribute = AnyOptionalAttribute(attribute) + self.resetSeed = resetSeed + self.frame = frame + self.properties = properties + self.childrenBox = box + } + } + + fileprivate struct Value: Rule { + var kind: GestureDebug.Kind + var type: any Any.Type + @OptionalAttribute var properties: GestureDebug.Properties? + @Attribute var phase: GesturePhase + @Attribute var resetSeed: UInt32 + @Attribute var position: ViewOrigin + @Attribute var size: ViewSize + @Attribute var transform: ViewTransform + @OptionalAttribute var debugData1: GestureDebug.Data? + @OptionalAttribute var debugData2: GestureDebug.Data? + + var childData: GestureDebug.Data.Children { + switch (debugData1, debugData2) { + case (.none, .none): + GestureDebug.Data.Children() + case let (.some(debugData), .none), let (.none, .some(debugData)): + GestureDebug.Data.Children(debugData) + case let (.some(debugData1), .some(debugData2)): + GestureDebug.Data.Children(debugData1, debugData2) + } + } + + var value: GestureDebug.Data { + let origin = transform.convert(.localToSpace(.global), point: position) + let frame = CGRect(origin: origin, size: size.value) + return GestureDebug.Data( + kind: kind, + type: type, + children: childData, + phase: phase.withValue(()), + attribute: $phase.identifier, + resetSeed: resetSeed, + frame: frame, + properties: properties ?? .init() + ) } } } extension GestureDebug.Data: Defaultable { + package typealias Value = GestureDebug.Data + package static let defaultValue: GestureDebug.Data = .init() } @@ -90,6 +149,8 @@ extension Attribute where Value: DebuggableGesturePhase { } } +// MARK: - makeDebuggableGesture + extension Gesture { @inline(__always) nonisolated package static func makeDebuggableGesture( @@ -98,8 +159,7 @@ extension Gesture { ) -> _GestureOutputs { var outputs = _makeGesture(gesture: gesture, inputs: inputs) guard inputs.options.contains(.includeDebugOutput), - // FIXME - !(self is PrimitiveDebuggableGesture) else { + !(self is PrimitiveDebuggableGesture.Type) else { return outputs } outputs.wrapDebugOutputs(Self.self, inputs: inputs) @@ -116,8 +176,7 @@ extension GestureModifier { ) -> _GestureOutputs { var outputs = _makeGesture(modifier: modifier, inputs: inputs, body: body) guard inputs.options.contains(.includeDebugOutput), - // FIXME - !(self is PrimitiveDebuggableGesture) else { + !(self is PrimitiveDebuggableGesture.Type) else { return outputs } outputs.wrapDebugOutputs(Self.self, inputs: inputs) @@ -164,17 +223,30 @@ extension _GestureOutputs { ) } - private func reallyWrap( + private mutating func reallyWrap( _ type: T.Type, kind: GestureDebug.Kind, properties: Attribute?, inputs: _GestureInputs, data: (Attribute?, Attribute?) ) { - _openSwiftUIUnimplementedFailure() + debugData = Attribute(GestureDebug.Value( + kind: kind, + type: type, + properties: OptionalAttribute(properties), + phase: phase, + resetSeed: inputs.resetSeed, + position: inputs.animatedPosition(), + size: inputs.viewInputs.size, + transform: inputs.transform, + debugData1: OptionalAttribute(data.0), + debugData2: OptionalAttribute(data.1) + )) } } +// MARK: - GesturePhase + descriptionWithoutValue + @_spi(ForOpenSwiftUIOnly) extension GesturePhase { package var descriptionWithoutValue: String { @@ -187,14 +259,87 @@ extension GesturePhase { } } +// MARK: - GestureDebug.Data + printTree [TBA] + extension GestureDebug.Data { package func printTree() { - _openSwiftUIUnimplementedFailure() + printSubtree(parent: nil, indent: Indent(kind: kind)) } - private typealias Indent = String + private struct Indent { + var text: String + var kind: GestureDebug.Kind + + init(_ text: String = "", kind: GestureDebug.Kind = .empty) { + self.text = text + self.kind = kind + } + + var linePrefix: String { + switch kind { + case .gesture: + text + "* " + case .combiner: + text + "+ " + default: + text + } + } + + var childText: String { + switch kind { + case .gesture: + text + "* " + case .combiner: + text + "| " + default: + text + } + } + } private func printSubtree(parent: GestureDebug.Data?, indent: Indent) { - _openSwiftUIUnimplementedFailure() + var line = indent.linePrefix + let typeDescription = Metadata(type).description + switch kind { + case .empty: + line += "(empty)" + case .modifier: + line += ".(\(typeDescription))" + default: + line += typeDescription + } + if let attribute = attribute.attribute { + line += " \(attribute.description)" + } + line += " (\(phase.descriptionWithoutValue))" + if resetSeed != 0, parent?.resetSeed != resetSeed { + line += " reset:\(resetSeed)" + } + line += frameDescription(relativeTo: parent) + if !properties.isEmpty { + let items = properties.map { "\($0.0): \($0.1)" } + line += " [\(items.joined(separator: ", "))]" + } + Log.eventDebug(line) + + let childIndent = Indent(indent.childText, kind: kind) + for child in children { + child.printSubtree(parent: self, indent: childIndent) + } + } + + private func frameDescription(relativeTo parent: GestureDebug.Data?) -> String { + var items: [String] = [] + let size = frame.size + if parent?.frame.size != size, size != .zero { + items.append("{\((size.width, size.height))}") + } + + let origin = frame.origin + if parent?.frame.origin != origin, origin != .zero { + items.append("@\((origin.x, origin.y))") + } + return items.isEmpty ? "" : " " + items.joined(separator: " ") } } From 0d7e33351ac021d73831149c6f9af2efe0de9a85 Mon Sep 17 00:00:00 2001 From: Kyle Date: Thu, 11 Jun 2026 01:01:34 +0800 Subject: [PATCH 4/6] Add GestureDebugDualTests.swift --- .../Event/Gesture/GestureDebugTestsStub.c | 13 + .../Event/Gesture/GestureDebugTests.swift | 305 ++++++++++++++++++ .../Event/Gesture/GestureDebugDualTests.swift | 204 ++++++++++++ 3 files changed, 522 insertions(+) create mode 100644 Sources/OpenSwiftUISymbolDualTestsSupport/Event/Gesture/GestureDebugTestsStub.c create mode 100644 Tests/OpenSwiftUICoreTests/Event/Gesture/GestureDebugTests.swift create mode 100644 Tests/OpenSwiftUISymbolDualTests/Event/Gesture/GestureDebugDualTests.swift diff --git a/Sources/OpenSwiftUISymbolDualTestsSupport/Event/Gesture/GestureDebugTestsStub.c b/Sources/OpenSwiftUISymbolDualTestsSupport/Event/Gesture/GestureDebugTestsStub.c new file mode 100644 index 000000000..0c7ce7cfe --- /dev/null +++ b/Sources/OpenSwiftUISymbolDualTestsSupport/Event/Gesture/GestureDebugTestsStub.c @@ -0,0 +1,13 @@ +// +// GestureDebugTestsStub.c +// OpenSwiftUISymbolDualTestsSupport + +#include "OpenSwiftUIBase.h" + +#if OPENSWIFTUI_TARGET_OS_DARWIN + +#import + +DEFINE_SL_STUB_SLF(OpenSwiftUITestStub_GestureDebugDataPrintTree, SwiftUICore, $s7SwiftUI12GestureDebugO4DataV9printTreeyyF); + +#endif diff --git a/Tests/OpenSwiftUICoreTests/Event/Gesture/GestureDebugTests.swift b/Tests/OpenSwiftUICoreTests/Event/Gesture/GestureDebugTests.swift new file mode 100644 index 000000000..3a8f6adde --- /dev/null +++ b/Tests/OpenSwiftUICoreTests/Event/Gesture/GestureDebugTests.swift @@ -0,0 +1,305 @@ +// +// GestureDebugTests.swift +// OpenSwiftUICoreTests +// + +import Foundation +import OpenAttributeGraphShims +#if !OPENSWIFTUI_SWIFT_LOG +import OSLog +#endif +@_spi(ForOpenSwiftUIOnly) +@testable +#if OPENSWIFTUI_ENABLE_PRIVATE_IMPORTS +@_private(sourceFile: "GestureDebug.swift") +#endif +import OpenSwiftUICore +import Testing + +#if arch(x86_64) +private let isX86_64 = true +#else +private let isX86_64 = false +#endif + +struct PrintTreeCase: @unchecked Sendable { + var root: GestureDebug.Data + var expected: [String] +} + +private let printTreeCases: [PrintTreeCase] = [ + .gestureWithModifierChild, + .gestureWithZeroFrameChild, + .emptyRoot, + .combinerWithSameFrameChild, + .primitiveWithFrameDeltaChildren, +] + +private extension PrintTreeCase { + static var gestureWithModifierChild: PrintTreeCase { + var properties = GestureDebug.Properties() + properties.append(("trackingID", "touch#7")) + properties.append(("state", "active")) + + let child = makeData( + kind: .modifier, + phase: .ended(()), + resetSeed: 2, + frame: CGRect(x: 4, y: 5, width: 6, height: 7), + properties: properties + ) + let root = makeData( + kind: .gesture, + children: GestureDebug.Data.Children(child), + phase: .active(()), + resetSeed: 1, + frame: CGRect(x: 1, y: 2, width: 3, height: 4) + ) + return PrintTreeCase( + root: root, + expected: [ + "* EmptyGesture (active) reset:1 {(3.0, 4.0)} @(1.0, 2.0)", + "* * .(EmptyGesture) (ended) reset:2 {(6.0, 7.0)} @(4.0, 5.0) [trackingID: touch#7, state: active]", + ] + ) + } + + static var gestureWithZeroFrameChild: PrintTreeCase { + let child = makeData( + kind: .primitive, + phase: .failed, + frame: .zero + ) + let root = makeData( + kind: .gesture, + children: GestureDebug.Data.Children(child), + phase: .failed, + frame: CGRect(x: 1, y: 2, width: 3, height: 4) + ) + return PrintTreeCase( + root: root, + expected: [ + "* EmptyGesture (failed) {(3.0, 4.0)} @(1.0, 2.0)", + "* * EmptyGesture (failed)", + ] + ) + } + + static var emptyRoot: PrintTreeCase { + let root = makeData( + kind: .empty, + phase: .possible(nil), + frame: .zero + ) + return PrintTreeCase( + root: root, + expected: [ + "(empty) ()", + ] + ) + } + + static var combinerWithSameFrameChild: PrintTreeCase { + let frame = CGRect(x: 1, y: 2, width: 3, height: 4) + let child = makeData( + kind: .primitive, + phase: .failed, + resetSeed: 3, + frame: frame + ) + let root = makeData( + kind: .combiner, + children: GestureDebug.Data.Children(child), + phase: .possible(()), + resetSeed: 3, + frame: frame + ) + return PrintTreeCase( + root: root, + expected: [ + "+ EmptyGesture (possible(some)) reset:3 {(3.0, 4.0)} @(1.0, 2.0)", + "| + EmptyGesture (failed)", + ] + ) + } + + static var primitiveWithFrameDeltaChildren: PrintTreeCase { + let parentFrame = CGRect(x: 1, y: 2, width: 3, height: 4) + let sizeChangedChild = makeData( + kind: .primitive, + phase: .active(()), + frame: CGRect(x: 1, y: 2, width: 5, height: 6) + ) + let originChangedChild = makeData( + kind: .modifier, + phase: .ended(()), + frame: CGRect(x: 7, y: 8, width: 3, height: 4) + ) + let root = makeData( + kind: .primitive, + children: GestureDebug.Data.Children(sizeChangedChild, originChangedChild), + phase: .failed, + frame: parentFrame + ) + return PrintTreeCase( + root: root, + expected: [ + "EmptyGesture (failed) {(3.0, 4.0)} @(1.0, 2.0)", + "EmptyGesture (active) {(5.0, 6.0)}", + ".(EmptyGesture) (ended) @(7.0, 8.0)", + ] + ) + } +} + +private func makeData( + kind: GestureDebug.Kind = .primitive, + type: any Any.Type = EmptyGesture.self, + children: GestureDebug.Data.Children = GestureDebug.Data.Children(), + phase: GesturePhase<()> = .failed, + resetSeed: UInt32 = 0, + frame: CGRect = .zero, + properties: GestureDebug.Properties = GestureDebug.Properties() +) -> GestureDebug.Data { + GestureDebug.Data( + kind: kind, + type: type, + children: children, + phase: phase, + attribute: nil, + resetSeed: resetSeed, + frame: frame, + properties: properties + ) +} + +#if !OPENSWIFTUI_SWIFT_LOG +@MainActor +@Suite(.disabled(if: isX86_64, "OSLogStore does not reliably return current-process log entries on x86_64 simulator.")) +struct GestureDebugLogTests { + // NOTE: entry.date has some range diff. So we can't use $0.date > date. Use count instead. + @available(iOS 15, macOS 12, *) + private func getLogEntries(count: Int) throws -> [String] { + let store = try OSLogStore(scope: .currentProcessIdentifier) + let entries = try store + .getEntries() // NOTE: options and position are not respected consistently. + .lazy + .compactMap { + $0 as? OSLogEntryLog + } + .filter { + $0.subsystem == "com.apple.diagnostics.events" && $0.category == "OpenSwiftUI" + } + .reversed() + .prefix(count) + .reversed() + return Array(entries.map { $0.composedMessage }) + } + + @available(iOS 15, macOS 12, *) + @Test(arguments: printTreeCases) + func printTreeEmitsExpectedLines(_ testCase: PrintTreeCase) throws { + testCase.root.printTree() + let logs = try getLogEntries(count: testCase.expected.count) + #expect(logs == testCase.expected) + } + + #if OPENSWIFTUI_ENABLE_PRIVATE_IMPORTS + @available(iOS 15, macOS 12, *) + @Test + func printSubtreeEmitsOneFormattedLine() throws { + let parent = makeData( + kind: .gesture, + phase: .active(()), + resetSeed: 1, + frame: CGRect(x: 1, y: 2, width: 3, height: 4) + ) + let child = makeData( + kind: .modifier, + phase: .ended(()), + resetSeed: 2, + frame: CGRect(x: 4, y: 5, width: 6, height: 7) + ) + + child.printSubtree(parent: parent, indent: GestureDebug.Data.Indent("* ", kind: .gesture)) + let logs = try getLogEntries(count: 1) + #expect(logs == [ + "* * .(EmptyGesture) (ended) reset:2 {(6.0, 7.0)} @(4.0, 5.0)", + ]) + } + #endif +} +#endif + +#if OPENSWIFTUI_ENABLE_PRIVATE_IMPORTS +struct GestureDebugTests { + @Test + func frameDescriptionWithoutParent() { + #expect( + makeData(frame: .zero).frameDescription(relativeTo: nil) == "" + ) + + let sizeOnlyFrame = CGRect(origin: .zero, size: CGSize(width: 10, height: 20)) + #expect( + makeData(frame: sizeOnlyFrame).frameDescription(relativeTo: nil) == + " {\((sizeOnlyFrame.size.width, sizeOnlyFrame.size.height))}" + ) + + let originOnlyFrame = CGRect(origin: CGPoint(x: 3, y: 4), size: .zero) + #expect( + makeData(frame: originOnlyFrame).frameDescription(relativeTo: nil) == + " @\((originOnlyFrame.origin.x, originOnlyFrame.origin.y))" + ) + + let fullFrame = CGRect(x: 3, y: 4, width: 10, height: 20) + #expect( + makeData(frame: fullFrame).frameDescription(relativeTo: nil) == + " {\((fullFrame.size.width, fullFrame.size.height))} @\((fullFrame.origin.x, fullFrame.origin.y))" + ) + } + + @Test + func frameDescriptionRelativeToParent() { + let parentFrame = CGRect(x: 3, y: 4, width: 10, height: 20) + let parent = makeData(frame: parentFrame) + + #expect( + makeData(frame: parentFrame).frameDescription(relativeTo: parent) == "" + ) + + let changedSizeFrame = CGRect(x: 3, y: 4, width: 11, height: 22) + #expect( + makeData(frame: changedSizeFrame).frameDescription(relativeTo: parent) == + " {\((changedSizeFrame.size.width, changedSizeFrame.size.height))}" + ) + + let changedOriginFrame = CGRect(x: 5, y: 7, width: 10, height: 20) + #expect( + makeData(frame: changedOriginFrame).frameDescription(relativeTo: parent) == + " @\((changedOriginFrame.origin.x, changedOriginFrame.origin.y))" + ) + + let changedFrame = CGRect(x: 5, y: 7, width: 11, height: 22) + #expect( + makeData(frame: changedFrame).frameDescription(relativeTo: parent) == + " {\((changedFrame.size.width, changedFrame.size.height))} @\((changedFrame.origin.x, changedFrame.origin.y))" + ) + + #expect( + makeData(frame: .zero).frameDescription(relativeTo: parent) == "" + ) + } + + @Test + func indentPrefixesFollowKind() { + #expect(GestureDebug.Data.Indent(kind: .gesture).linePrefix == "* ") + #expect(GestureDebug.Data.Indent(kind: .gesture).childText == "* ") + + #expect(GestureDebug.Data.Indent(kind: .combiner).linePrefix == "+ ") + #expect(GestureDebug.Data.Indent(kind: .combiner).childText == "| ") + + #expect(GestureDebug.Data.Indent(kind: .modifier).linePrefix == "") + #expect(GestureDebug.Data.Indent(kind: .modifier).childText == "") + } +} +#endif diff --git a/Tests/OpenSwiftUISymbolDualTests/Event/Gesture/GestureDebugDualTests.swift b/Tests/OpenSwiftUISymbolDualTests/Event/Gesture/GestureDebugDualTests.swift new file mode 100644 index 000000000..a4e3bd633 --- /dev/null +++ b/Tests/OpenSwiftUISymbolDualTests/Event/Gesture/GestureDebugDualTests.swift @@ -0,0 +1,204 @@ +// +// GestureDebugDualTests.swift +// OpenSwiftUISymbolDualTests + +#if canImport(SwiftUI, _underlyingVersion: 6.5.4) +import Foundation +import OSLog +@_spi(ForOpenSwiftUIOnly) +import OpenSwiftUICore +import Testing + +extension GestureDebug.Data { + @_silgen_name("OpenSwiftUITestStub_GestureDebugDataPrintTree") + func swiftUI_printTree() +} + +#if arch(x86_64) +private let isX86_64 = true +#else +private let isX86_64 = false +#endif + +struct PrintTreeCase: @unchecked Sendable { + var root: GestureDebug.Data + var expected: [String] +} + +private let printTreeCases: [PrintTreeCase] = [ + .gestureWithModifierChild, + .gestureWithZeroFrameChild, + .emptyRoot, + .combinerWithSameFrameChild, + .primitiveWithFrameDeltaChildren, +] + +private extension PrintTreeCase { + static var gestureWithModifierChild: PrintTreeCase { + var properties = GestureDebug.Properties() + properties.append(("trackingID", "touch#7")) + properties.append(("state", "active")) + + let child = makeData( + kind: .modifier, + phase: .ended(()), + resetSeed: 2, + frame: CGRect(x: 4, y: 5, width: 6, height: 7), + properties: properties + ) + let root = makeData( + kind: .gesture, + children: GestureDebug.Data.Children(child), + phase: .active(()), + resetSeed: 1, + frame: CGRect(x: 1, y: 2, width: 3, height: 4) + ) + return PrintTreeCase( + root: root, + expected: [ + "* EmptyGesture (active) reset:1 {(3.0, 4.0)} @(1.0, 2.0)", + "* * .(EmptyGesture) (ended) reset:2 {(6.0, 7.0)} @(4.0, 5.0) [trackingID: touch#7, state: active]", + ] + ) + } + + static var gestureWithZeroFrameChild: PrintTreeCase { + let child = makeData( + kind: .primitive, + phase: .failed, + frame: .zero + ) + let root = makeData( + kind: .gesture, + children: GestureDebug.Data.Children(child), + phase: .failed, + frame: CGRect(x: 1, y: 2, width: 3, height: 4) + ) + return PrintTreeCase( + root: root, + expected: [ + "* EmptyGesture (failed) {(3.0, 4.0)} @(1.0, 2.0)", + "* * EmptyGesture (failed)", + ] + ) + } + + static var emptyRoot: PrintTreeCase { + let root = makeData( + kind: .empty, + phase: .possible(nil), + frame: .zero + ) + return PrintTreeCase( + root: root, + expected: [ + "(empty) ()", + ] + ) + } + + static var combinerWithSameFrameChild: PrintTreeCase { + let frame = CGRect(x: 1, y: 2, width: 3, height: 4) + let child = makeData( + kind: .primitive, + phase: .failed, + resetSeed: 3, + frame: frame + ) + let root = makeData( + kind: .combiner, + children: GestureDebug.Data.Children(child), + phase: .possible(()), + resetSeed: 3, + frame: frame + ) + return PrintTreeCase( + root: root, + expected: [ + "+ EmptyGesture (possible(some)) reset:3 {(3.0, 4.0)} @(1.0, 2.0)", + "| + EmptyGesture (failed)", + ] + ) + } + + static var primitiveWithFrameDeltaChildren: PrintTreeCase { + let parentFrame = CGRect(x: 1, y: 2, width: 3, height: 4) + let sizeChangedChild = makeData( + kind: .primitive, + phase: .active(()), + frame: CGRect(x: 1, y: 2, width: 5, height: 6) + ) + let originChangedChild = makeData( + kind: .modifier, + phase: .ended(()), + frame: CGRect(x: 7, y: 8, width: 3, height: 4) + ) + let root = makeData( + kind: .primitive, + children: GestureDebug.Data.Children(sizeChangedChild, originChangedChild), + phase: .failed, + frame: parentFrame + ) + return PrintTreeCase( + root: root, + expected: [ + "EmptyGesture (failed) {(3.0, 4.0)} @(1.0, 2.0)", + "EmptyGesture (active) {(5.0, 6.0)}", + ".(EmptyGesture) (ended) @(7.0, 8.0)", + ] + ) + } +} + +private func makeData( + kind: GestureDebug.Kind = .primitive, + type: any Any.Type = EmptyGesture.self, + children: GestureDebug.Data.Children = GestureDebug.Data.Children(), + phase: GesturePhase<()> = .failed, + resetSeed: UInt32 = 0, + frame: CGRect = .zero, + properties: GestureDebug.Properties = GestureDebug.Properties() +) -> GestureDebug.Data { + GestureDebug.Data( + kind: kind, + type: type, + children: children, + phase: phase, + attribute: nil, + resetSeed: resetSeed, + frame: frame, + properties: properties + ) +} + +@MainActor +@Suite(.disabled(if: isX86_64, "OSLogStore does not reliably return current-process log entries on x86_64 simulator.")) +struct GestureDebugDualTests { + // NOTE: entry.date has some range diff. So we can't use $0.date > date. Use count instead. + @available(iOS 15, macOS 12, *) + private func getLogEntries(count: Int) throws -> [String] { + let store = try OSLogStore(scope: .currentProcessIdentifier) + let entries = try store + .getEntries() // NOTE: options and position are not respected consistently. + .lazy + .compactMap { + $0 as? OSLogEntryLog + } + .filter { + $0.subsystem == "com.apple.diagnostics.events" && $0.category == "SwiftUI" + } + .reversed() + .prefix(count) + .reversed() + return Array(entries.map { $0.composedMessage }) + } + + @available(iOS 15, macOS 12, *) + @Test(arguments: printTreeCases) + func printTreeAcceptsOpenSwiftUIGestureDebugData(_ testCase: PrintTreeCase) throws { + testCase.root.swiftUI_printTree() + let logs = try getLogEntries(count: testCase.expected.count) + #expect(logs == testCase.expected) + } +} +#endif From c8ad6f4de216993c9c9703fa7c149d6d992b1a12 Mon Sep 17 00:00:00 2001 From: Kyle Date: Thu, 11 Jun 2026 01:40:28 +0800 Subject: [PATCH 5/6] Implement gesture view modifier --- .../Event/Gesture/GestureViewModifier.swift | 261 +++++++++++++----- 1 file changed, 198 insertions(+), 63 deletions(-) diff --git a/Sources/OpenSwiftUICore/Event/Gesture/GestureViewModifier.swift b/Sources/OpenSwiftUICore/Event/Gesture/GestureViewModifier.swift index 4a77262e7..c60a8d480 100644 --- a/Sources/OpenSwiftUICore/Event/Gesture/GestureViewModifier.swift +++ b/Sources/OpenSwiftUICore/Event/Gesture/GestureViewModifier.swift @@ -3,7 +3,7 @@ // OpenSwiftUICore // // Audited for 6.5.4 -// Status: WIP +// Status: Complete // ID: 9DF46B4E935FF03A55FF3DDFB0B1FF2B (SwiftUICore) package import OpenAttributeGraphShims @@ -272,7 +272,7 @@ extension AnyGestureResponder { } } -// MARK: - GestureResponder [WIP] +// MARK: - GestureResponder private class GestureResponder: DefaultLayoutViewResponder, AnyGestureResponder where Modifier: GestureViewModifier { let modifier: Attribute @@ -332,7 +332,7 @@ private class GestureResponder: DefaultLayoutViewResponder, AnyGesture } func makeSubviewsGesture(inputs: _GestureInputs) -> _GestureOutputs { - makeGesture(inputs: inputs) + super.makeGesture(inputs: inputs) } override var gestureContainer: AnyObject? { @@ -351,7 +351,11 @@ private class GestureResponder: DefaultLayoutViewResponder, AnyGesture cacheKey: UInt32?, options: ViewResponder.ContainsPointsOptions ) -> ViewResponder.ContainsPointsResult { - _openSwiftUIUnimplementedFailure() + var result = super.containsGlobalPoints(points, cacheKey: cacheKey, options: options) + if options.contains(.useZDistanceAsPriority) { + result.priority = ViewResponder.gestureContainmentPriority + } + return result } override func bindEvent(_ event: any EventType) -> ResponderNode? { @@ -369,31 +373,40 @@ private class GestureResponder: DefaultLayoutViewResponder, AnyGesture override func makeGesture(inputs: _GestureInputs) -> _GestureOutputs { makeWrappedGesture(inputs: inputs) { childInputs in -// let childViewInputs = childInputs.viewInputs -// let closure: () -> _GestureOutputs = { [self] in -// if inputs.options.contains(.skipCombiners) { -// let childGesture = Attribute(GestureViewChild( -// modifier: modifier, -// isEnabled: childViewInputs.isEnabled, -// viewPhase: childViewInputs.viewPhase -// )) -// return AnyGesture.makeDebuggableGesture(gesture: _GraphValue(childGesture), inputs: childInputs) -// } else { -// let childGesture = Attribute(CombiningGestureViewChild( -// modifier: modifier, -// isEnabled: childViewInputs.isEnabled, -// viewPhase: childViewInputs.viewPhase, -// node: self -// )) -// return Modifier.Combiner.Result.makeDebuggableGesture(gesture: _GraphValue(childGesture), inputs: childInputs) -// } -// } -// guard inputs.options.contains(.includeDebugOutput) else { -// return closure() -// } -// // TODO: GestureViewDebug -// return closure() - _openSwiftUIUnimplementedFailure() + let childViewInputs = childInputs.viewInputs + let outputs: _GestureOutputs = { + if childInputs.options.contains(.skipCombiners) { + let childGesture = Attribute(GestureViewChild( + modifier: modifier, + isEnabled: childViewInputs.isEnabled, + viewPhase: childViewInputs.viewPhase + )) + return AnyGesture.makeDebuggableGesture( + gesture: _GraphValue(childGesture), + inputs: childInputs + ) + } else { + let childGesture = Attribute(CombiningGestureViewChild( + modifier: modifier, + isEnabled: childViewInputs.isEnabled, + viewPhase: childViewInputs.viewPhase, + node: self + )) + return Modifier.Combiner.Result.makeDebuggableGesture( + gesture: _GraphValue(childGesture), + inputs: childInputs + ) + } + }() + guard childInputs.options.contains(.includeDebugOutput) else { + return outputs + } + var wrappedOutputs = outputs + wrappedOutputs.debugData = Attribute(GestureViewDebug( + modifier: modifier, + debugData: OptionalAttribute(outputs.debugData) + )) + return wrappedOutputs } } @@ -408,6 +421,58 @@ private class GestureResponder: DefaultLayoutViewResponder, AnyGesture } } +// MARK: - GestureAccessibilityProvider + +package protocol GestureAccessibilityProvider { + nonisolated static func makeGesture( + mask: @autoclosure () -> Attribute, + inputs: _ViewInputs, + outputs: inout _ViewOutputs + ) +} + +// MARK: - SimultaneousGestureModifier + +struct SimultaneousGestureModifier: GestureViewModifier where T: Gesture { + var gesture: T + var name: String? + var gestureMask: GestureMask + + init( + _ gesture: T, + name: String?, + gestureMask: GestureMask + ) { + self.gesture = gesture + self.name = name + self.gestureMask = gestureMask + } + + typealias ContentGesture = T + typealias Combiner = SimultaneousGestureCombiner +} + +// MARK: - HighPriorityGestureModifier + +struct HighPriorityGestureModifier: GestureViewModifier where T: Gesture { + var gesture: T + var name: String? + var gestureMask: GestureMask + + init( + _ gesture: T, + name: String?, + gestureMask: GestureMask + ) { + self.gesture = gesture + self.name = name + self.gestureMask = gestureMask + } + + typealias ContentGesture = T + typealias Combiner = HighPriorityGestureCombiner +} + // MARK: - GestureFilter private struct GestureFilter: StatefulRule where Modifier: GestureViewModifier { @@ -442,15 +507,7 @@ private struct GestureFilter: StatefulRule where Modifier: GestureView } } -// MARK: - GestureAccessibilityProvider - -package protocol GestureAccessibilityProvider { - nonisolated static func makeGesture( - mask: @autoclosure () -> Attribute, - inputs: _ViewInputs, - outputs: inout _ViewOutputs - ) -} +// MARK: - EmptyGestureAccessibilityProvider struct EmptyGestureAccessibilityProvider: GestureAccessibilityProvider { nonisolated static func makeGesture( @@ -458,9 +515,12 @@ struct EmptyGestureAccessibilityProvider: GestureAccessibilityProvider { inputs: _ViewInputs, outputs: inout _ViewOutputs ) { + _openSwiftUIEmptyStub() } } +// MARK: - Inputs + gestureAccessibilityProvider + extension _GraphInputs { private struct GestureAccessibilityProviderKey: GraphInput { static let defaultValue: (any GestureAccessibilityProvider.Type) = EmptyGestureAccessibilityProvider.self @@ -543,9 +603,29 @@ private struct CombiningGestureViewChild: Rule where Modifier: Gesture } } -// MARK: - GestureViewDebug [WIP] +// MARK: - GestureViewDebug + +private struct GestureViewDebug: Rule where Modifier: GestureViewModifier { + @Attribute var modifier: Modifier + @OptionalAttribute var debugData: GestureDebug.Data? -private struct GestureViewDebug where Modifier: GestureViewModifier { + typealias Value = GestureDebug.Data + + var value: GestureDebug.Data { + guard let debugData else { + return GestureDebug.Data() + } + return GestureDebug.Data( + kind: .gesture, + type: Modifier.ContentGesture.self, + children: [debugData], + phase: debugData.phase, + attribute: $modifier.identifier, + resetSeed: debugData.resetSeed, + frame: debugData.frame, + properties: .init() + ) + } } // MARK: - SubviewsGesture @@ -577,15 +657,41 @@ private struct SubviewsGesture: PrimitiveGesture, PrimitiveDebuggableGesture { } } -// MARK: - SimultaneousGestureCombiner [WIP] +// MARK: - SimultaneousGestureCombiner -struct SimultaneousGestureCombiner {} +struct SimultaneousGestureCombiner: GestureCombiner { + typealias Base = SimultaneousGesture, AnyGesture> -// MARK: - HighPriorityGestureCombiner [WIP] + typealias Result = _MapGesture -struct HighPriorityGestureCombiner {} + static func combine( + _ first: AnyGesture, + _ second: AnyGesture + ) -> Result { + first.simultaneously(with: second).map { _ in } + } + + static var exclusionPolicy: GestureResponderExclusionPolicy { .simultaneous } +} + +// MARK: - HighPriorityGestureCombiner + +struct HighPriorityGestureCombiner: GestureCombiner { + typealias Base = ExclusiveGesture, AnyGesture> + + typealias Result = _MapGesture + + static func combine( + _ first: AnyGesture, + _ second: AnyGesture + ) -> Result { + second.exclusively(before: first).map { _ in } + } + + static var exclusionPolicy: GestureResponderExclusionPolicy { .highPriority } +} -// MARK: - SubviewsPhase [WIP] +// MARK: - SubviewsPhase private struct SubviewsPhase: StatefulRule, ObservedAttribute { struct Value { @@ -605,7 +711,36 @@ private struct SubviewsPhase: StatefulRule, ObservedAttribute { @OptionalAttribute var childDebugData: GestureDebug.Data? mutating func updateValue() { - _openSwiftUIUnimplementedFailure() + let node = gesture.node + if resetSeed != oldSeed || childSubgraph == nil || oldNode !== node { + if let childSubgraph { + outputs.detachIndirectOutputs() + self.childSubgraph = nil + _childPhase = .init() + childSubgraph.willInvalidate(isInserted: true) + childSubgraph.invalidate() + } + oldNode?.resetGesture() + + let newSubgraph = Subgraph(graph: parentSubgraph.graph) + childSubgraph = newSubgraph + parentSubgraph.addChild(newSubgraph) + let childOutputs = newSubgraph.apply { + var childInputs = inputs + childInputs.copyCaches() + let childOutputs = node.makeSubviewsGesture(inputs: childInputs) + outputs.attachIndirectOutputs(childOutputs) + return childOutputs + } + _childPhase = OptionalAttribute(childOutputs.phase) + _childDebugData = OptionalAttribute(childOutputs.debugData) + oldSeed = resetSeed + oldNode = node + } + value = Value( + phase: childPhase ?? .failed, + debugData: childDebugData ?? GestureDebug.Data() + ) } func destroy() { @@ -613,6 +748,23 @@ private struct SubviewsPhase: StatefulRule, ObservedAttribute { } } +// MARK: - ContentPhase + +private struct ContentPhase: ResettableGestureRule { + @Attribute var phase: GesturePhase + @Attribute var resetSeed: UInt32 + var lastResetSeed: UInt32 + + typealias Value = GesturePhase + + mutating func updateValue() { + guard resetIfNeeded() else { + return + } + value = phase.withValue(()) + } +} + // MARK: - ContentGesture private struct ContentGesture: GestureModifier { @@ -634,20 +786,3 @@ private struct ContentGesture: GestureModifier { return outputs.withPhase(phase) } } - -// MARK: - ContentPhase - -private struct ContentPhase: ResettableGestureRule { - @Attribute var phase: GesturePhase - @Attribute var resetSeed: UInt32 - var lastResetSeed: UInt32 - - typealias Value = GesturePhase - - mutating func updateValue() { - guard resetIfNeeded() else { - return - } - value = phase.withValue(()) - } -} From 57ab1631bb9a55e9e96db4b2a0ff126741e3c295 Mon Sep 17 00:00:00 2001 From: Kyle Date: Thu, 11 Jun 2026 02:51:39 +0800 Subject: [PATCH 6/6] Add gesture mask view modifiers --- .../Event/Gesture/GestureMask.swift | 522 ++++++++++++++++++ 1 file changed, 522 insertions(+) diff --git a/Sources/OpenSwiftUICore/Event/Gesture/GestureMask.swift b/Sources/OpenSwiftUICore/Event/Gesture/GestureMask.swift index e1a7c94ab..b5de81676 100644 --- a/Sources/OpenSwiftUICore/Event/Gesture/GestureMask.swift +++ b/Sources/OpenSwiftUICore/Event/Gesture/GestureMask.swift @@ -5,6 +5,528 @@ // Audited for 6.5.4 // Status: Complete +// MARK: - View + Gesture + +@available(OpenSwiftUI_v1_0, *) +extension View { + /// Attaches a gesture to the view with a lower precedence than gestures + /// defined by the view. + /// + /// Use this method when you need to attach a gesture to a view. The + /// example below defines a custom gesture that prints a message to the + /// console and attaches it to the view's ``VStack``. Inside the ``VStack`` + /// a red heart ``Image`` defines its own ``TapGesture`` + /// handler that also prints a message to the console, and blue rectangle + /// with no custom gesture handlers. Tapping or clicking the image + /// prints a message to the console from the tap gesture handler on the + /// image, while tapping or clicking the rectangle inside the ``VStack`` + /// prints a message in the console from the enclosing vertical stack + /// gesture handler. + /// + /// struct GestureExample: View { + /// @State private var message = "Message" + /// let newGesture = TapGesture().onEnded { + /// print("Tap on VStack.") + /// } + /// + /// var body: some View { + /// VStack(spacing:25) { + /// Image(systemName: "heart.fill") + /// .resizable() + /// .frame(width: 75, height: 75) + /// .padding() + /// .foregroundColor(.red) + /// .onTapGesture { + /// print("Tap on image.") + /// } + /// Rectangle() + /// .fill(Color.blue) + /// } + /// .gesture(newGesture) + /// .frame(width: 200, height: 200) + /// .border(Color.purple) + /// } + /// } + /// + /// - Parameters: + /// - gesture: A gesture to attach to the view. + /// - mask: A value that controls how adding this gesture to the view + /// affects other gestures recognized by the view and its subviews. + /// Defaults to ``OpenSwiftUI/GestureMask/all``. + nonisolated public func gesture( + _ gesture: T, + including mask: GestureMask = .all + ) -> some View where T: Gesture { + modifier(AddGestureModifier(gesture, gestureMask: mask)) + } + + /// Attaches a gesture to the view with a higher precedence than gestures + /// defined by the view. + /// + /// Use this method when you need to define a high priority gesture + /// to take precedence over the view's existing gestures. The + /// example below defines a custom gesture that prints a message to the + /// console and attaches it to the view's ``VStack``. Inside the ``VStack`` + /// a red heart ``Image`` defines its own ``TapGesture`` handler that + /// also prints a message to the console, and a blue rectangle + /// with no custom gesture handlers. Tapping or clicking any of the + /// views results in a console message from the high priority gesture + /// attached to the enclosing ``VStack``. + /// + /// struct HighPriorityGestureExample: View { + /// @State private var message = "Message" + /// let newGesture = TapGesture().onEnded { + /// print("Tap on VStack.") + /// } + /// + /// var body: some View { + /// VStack(spacing:25) { + /// Image(systemName: "heart.fill") + /// .resizable() + /// .frame(width: 75, height: 75) + /// .padding() + /// .foregroundColor(.red) + /// .onTapGesture { + /// print("Tap on image.") + /// } + /// Rectangle() + /// .fill(Color.blue) + /// } + /// .highPriorityGesture(newGesture) + /// .frame(width: 200, height: 200) + /// .border(Color.purple) + /// } + /// } + /// + /// - Parameters: + /// - gesture: A gesture to attach to the view. + /// - mask: A value that controls how adding this gesture to the view + /// affects other gestures recognized by the view and its subviews. + /// Defaults to ``OpenSwiftUI/GestureMask/all``. + nonisolated public func highPriorityGesture( + _ gesture: T, + including mask: GestureMask = .all + ) -> some View where T: Gesture { + modifier(HighPriorityGestureModifier(gesture, name: nil, gestureMask: mask)) + } + + /// Attaches a gesture to the view to process simultaneously with gestures + /// defined by the view. + /// + /// Use this method when you need to define and process a view specific + /// gesture simultaneously with the same priority as the + /// view's existing gestures. The example below defines a custom gesture + /// that prints a message to the console and attaches it to the view's + /// ``VStack``. Inside the ``VStack`` is a red heart ``Image`` defines its + /// own ``TapGesture`` handler that also prints a message to the console + /// and a blue rectangle with no custom gesture handlers. + /// + /// Tapping or clicking the "heart" image sends two messages to the + /// console: one for the image's tap gesture handler, and the other from a + /// custom gesture handler attached to the enclosing vertical stack. + /// Tapping or clicking on the blue rectangle results only in the single + /// message to the console from the tap recognizer attached to the + /// ``VStack``: + /// + /// struct SimultaneousGestureExample: View { + /// @State private var message = "Message" + /// let newGesture = TapGesture().onEnded { + /// print("Gesture on VStack.") + /// } + /// + /// var body: some View { + /// VStack(spacing:25) { + /// Image(systemName: "heart.fill") + /// .resizable() + /// .frame(width: 75, height: 75) + /// .padding() + /// .foregroundColor(.red) + /// .onTapGesture { + /// print("Gesture on image.") + /// } + /// Rectangle() + /// .fill(Color.blue) + /// } + /// .simultaneousGesture(newGesture) + /// .frame(width: 200, height: 200) + /// .border(Color.purple) + /// } + /// } + /// + /// - Parameters: + /// - gesture: A gesture to attach to the view. + /// - mask: A value that controls how adding this gesture to the view + /// affects other gestures recognized by the view and its subviews. + /// Defaults to ``OpenSwiftUI/GestureMask/all``. + nonisolated public func simultaneousGesture( + _ gesture: T, + including mask: GestureMask = .all + ) -> some View where T: Gesture { + modifier(SimultaneousGestureModifier(gesture, name: nil, gestureMask: mask)) + } + + /// Attaches a gesture to the view with a lower precedence than gestures + /// defined by the view. + /// + /// Use this method when you need to attach a gesture to a view. The + /// example below defines a custom gesture that prints a message to the + /// console and attaches it to the view's ``VStack``. Inside the ``VStack`` + /// a red heart ``Image`` defines its own ``TapGesture`` + /// handler that also prints a message to the console, and blue rectangle + /// with no custom gesture handlers. Tapping or clicking the image + /// prints a message to the console from the tap gesture handler on the + /// image, while tapping or clicking the rectangle inside the ``VStack`` + /// prints a message in the console from the enclosing vertical stack + /// gesture handler. + /// + /// You can also use the ``isEnabled`` parameter to conditionally disable + /// the gesture. + /// + /// struct GestureExample: View { + /// @State private var message = "Message" + /// var isGestureEnabled: Bool + /// let newGesture = TapGesture().onEnded { + /// print("Tap on VStack.") + /// } + /// + /// var body: some View { + /// VStack(spacing:25) { + /// Image(systemName: "heart.fill") + /// .resizable() + /// .frame(width: 75, height: 75) + /// .padding() + /// .foregroundColor(.red) + /// .onTapGesture { + /// print("Tap on image.") + /// } + /// Rectangle() + /// .fill(Color.blue) + /// } + /// .gesture(newGesture, isEnabled: isGestureEnabled) + /// .frame(width: 200, height: 200) + /// .border(Color.purple) + /// } + /// } + /// + /// - Parameters: + /// - gesture: A gesture to attach to the view. + /// - isEnabled: Whether the added gesture is enabled. + @_alwaysEmitIntoClient + nonisolated public func gesture( + _ gesture: T, + isEnabled: Bool + ) -> some View where T: Gesture { + self.gesture(gesture, including: isEnabled ? .all : .subviews) + } + + /// Attaches a gesture to the view with a higher precedence than gestures + /// defined by the view. + /// + /// Use this method when you need to define a high priority gesture + /// to take precedence over the view's existing gestures. The + /// example below defines a custom gesture that prints a message to the + /// console and attaches it to the view's ``VStack``. Inside the ``VStack`` + /// a red heart ``Image`` defines its own ``TapGesture`` handler that + /// also prints a message to the console, and a blue rectangle + /// with no custom gesture handlers. Tapping or clicking any of the + /// views results in a console message from the high priority gesture + /// attached to the enclosing ``VStack``. + /// + /// You can also use the ``isEnabled`` parameter to conditionally disable + /// the gesture. + /// + /// struct HighPriorityGestureExample: View { + /// @State private var message = "Message" + /// var isGestureEnabled: Bool + /// let newGesture = TapGesture().onEnded { + /// print("Tap on VStack.") + /// } + /// + /// var body: some View { + /// VStack(spacing:25) { + /// Image(systemName: "heart.fill") + /// .resizable() + /// .frame(width: 75, height: 75) + /// .padding() + /// .foregroundColor(.red) + /// .onTapGesture { + /// print("Tap on image.") + /// } + /// Rectangle() + /// .fill(Color.blue) + /// } + /// .highPriorityGesture( + /// newGesture, isEnabled: isGestureEnabled) + /// .frame(width: 200, height: 200) + /// .border(Color.purple) + /// } + /// } + /// + /// - Parameters: + /// - gesture: A gesture to attach to the view. + /// - isEnabled: Whether the added gesture is enabled. + @_alwaysEmitIntoClient + nonisolated public func highPriorityGesture( + _ gesture: T, + isEnabled: Bool + ) -> some View where T: Gesture { + highPriorityGesture(gesture, including: isEnabled ? .all : .subviews) + } + + /// Attaches a gesture to the view to process simultaneously with gestures + /// defined by the view. + /// + /// Use this method when you need to define and process a view specific + /// gesture simultaneously with the same priority as the + /// view's existing gestures. The example below defines a custom gesture + /// that prints a message to the console and attaches it to the view's + /// ``VStack``. Inside the ``VStack`` is a red heart ``Image`` defines its + /// own ``TapGesture`` handler that also prints a message to the console + /// and a blue rectangle with no custom gesture handlers. + /// + /// You can also use the ``isEnabled`` parameter to conditionally disable + /// the gesture. + /// + /// Tapping or clicking the "heart" image sends two messages to the + /// console: one for the image's tap gesture handler, and the other from a + /// custom gesture handler attached to the enclosing vertical stack. + /// Tapping or clicking on the blue rectangle results only in the single + /// message to the console from the tap recognizer attached to the + /// ``VStack``: + /// + /// struct SimultaneousGestureExample: View { + /// @State private var message = "Message" + /// var isGestureEnabled: Bool + /// let newGesture = TapGesture().onEnded { + /// print("Gesture on VStack.") + /// } + /// + /// var body: some View { + /// VStack(spacing:25) { + /// Image(systemName: "heart.fill") + /// .resizable() + /// .frame(width: 75, height: 75) + /// .padding() + /// .foregroundColor(.red) + /// .onTapGesture { + /// print("Gesture on image.") + /// } + /// Rectangle() + /// .fill(Color.blue) + /// } + /// .simultaneousGesture( + /// newGesture, isEnabled: isGestureEnabled) + /// .frame(width: 200, height: 200) + /// .border(Color.purple) + /// } + /// } + /// + /// - Parameters: + /// - gesture: A gesture to attach to the view. + /// - isEnabled: Whether the added gesture is enabled. + @_alwaysEmitIntoClient + nonisolated public func simultaneousGesture( + _ gesture: T, + isEnabled: Bool + ) -> some View where T: Gesture { + simultaneousGesture(gesture, including: isEnabled ? .all : .subviews) + } +} + +@available(OpenSwiftUI_v6_0, *) +extension View { + /// Attaches a gesture to the view with a lower precedence than gestures + /// defined by the view. + /// + /// Use this method when you need to attach a gesture to a view. The + /// example below defines a custom gesture that prints a message to the + /// console and attaches it to the view's ``VStack``. Inside the ``VStack`` + /// a red heart ``Image`` defines its own ``TapGesture`` + /// handler that also prints a message to the console, and blue rectangle + /// with no custom gesture handlers. Tapping or clicking the image + /// prints a message to the console from the tap gesture handler on the + /// image, while tapping or clicking the rectangle inside the ``VStack`` + /// prints a message in the console from the enclosing vertical stack + /// gesture handler. + /// + /// You can also use the ``isEnabled`` parameter to conditionally disable + /// the gesture. + /// + /// struct GestureExample: View { + /// @State private var message = "Message" + /// var isGestureEnabled: Bool + /// let newGesture = TapGesture().onEnded { + /// print("Tap on VStack.") + /// } + /// + /// var body: some View { + /// VStack(spacing:25) { + /// Image(systemName: "heart.fill") + /// .resizable() + /// .frame(width: 75, height: 75) + /// .padding() + /// .foregroundColor(.red) + /// .onTapGesture { + /// print("Tap on image.") + /// } + /// Rectangle() + /// .fill(Color.blue) + /// } + /// .gesture(newGesture, isEnabled: isGestureEnabled) + /// .frame(width: 200, height: 200) + /// .border(Color.purple) + /// } + /// } + /// + /// - Parameters: + /// - gesture: A gesture to attach to the view. + /// - name: A string that identifies the gesture. In iOS, the name can be + /// used to set up failure relationships between UIKit gesture + /// recognizers and this gesture. + /// - isEnabled: Whether the added gesture is enabled. The default value + /// is `true`. + nonisolated public func gesture( + _ gesture: T, + name: String, + isEnabled: Bool = true + ) -> some View where T: Gesture { + modifier(AddGestureModifier( + gesture, + name: name, + gestureMask: isEnabled ? .all : .subviews + )) + } + + /// Attaches a gesture to the view with a higher precedence than gestures + /// defined by the view. + /// + /// Use this method when you need to define a high priority gesture + /// to take precedence over the view's existing gestures. The + /// example below defines a custom gesture that prints a message to the + /// console and attaches it to the view's ``VStack``. Inside the ``VStack`` + /// a red heart ``Image`` defines its own ``TapGesture`` handler that + /// also prints a message to the console, and a blue rectangle + /// with no custom gesture handlers. Tapping or clicking any of the + /// views results in a console message from the high priority gesture + /// attached to the enclosing ``VStack``. + /// + /// You can also use the ``isEnabled`` parameter to conditionally disable + /// the gesture. + /// + /// struct HighPriorityGestureExample: View { + /// @State private var message = "Message" + /// var isGestureEnabled: Bool + /// let newGesture = TapGesture().onEnded { + /// print("Tap on VStack.") + /// } + /// + /// var body: some View { + /// VStack(spacing:25) { + /// Image(systemName: "heart.fill") + /// .resizable() + /// .frame(width: 75, height: 75) + /// .padding() + /// .foregroundColor(.red) + /// .onTapGesture { + /// print("Tap on image.") + /// } + /// Rectangle() + /// .fill(Color.blue) + /// } + /// .highPriorityGesture( + /// newGesture, isEnabled: isGestureEnabled) + /// .frame(width: 200, height: 200) + /// .border(Color.purple) + /// } + /// } + /// + /// - Parameters: + /// - gesture: A gesture to attach to the view. + /// - name: A string that identifies the gesture. In iOS, the name can be + /// used to set up failure relationships between UIKit gesture + /// recognizers and this gesture. + /// - isEnabled: Whether the added gesture is enabled. The default value + /// is `true`. + nonisolated public func highPriorityGesture( + _ gesture: T, + name: String, + isEnabled: Bool = true + ) -> some View where T: Gesture { + modifier(HighPriorityGestureModifier( + gesture, + name: name, + gestureMask: isEnabled ? .all : .subviews + )) + } + + /// Attaches a gesture to the view to process simultaneously with gestures + /// defined by the view. + /// + /// Use this method when you need to define and process a view specific + /// gesture simultaneously with the same priority as the + /// view's existing gestures. The example below defines a custom gesture + /// that prints a message to the console and attaches it to the view's + /// ``VStack``. Inside the ``VStack`` is a red heart ``Image`` defines its + /// own ``TapGesture`` handler that also prints a message to the console + /// and a blue rectangle with no custom gesture handlers. + /// + /// You can also use the ``isEnabled`` parameter to conditionally disable + /// the gesture. + /// + /// Tapping or clicking the "heart" image sends two messages to the + /// console: one for the image's tap gesture handler, and the other from a + /// custom gesture handler attached to the enclosing vertical stack. + /// Tapping or clicking on the blue rectangle results only in the single + /// message to the console from the tap recognizer attached to the + /// ``VStack``: + /// + /// struct SimultaneousGestureExample: View { + /// @State private var message = "Message" + /// var isGestureEnabled: Bool + /// let newGesture = TapGesture().onEnded { + /// print("Gesture on VStack.") + /// } + /// + /// var body: some View { + /// VStack(spacing:25) { + /// Image(systemName: "heart.fill") + /// .resizable() + /// .frame(width: 75, height: 75) + /// .padding() + /// .foregroundColor(.red) + /// .onTapGesture { + /// print("Gesture on image.") + /// } + /// Rectangle() + /// .fill(Color.blue) + /// } + /// .simultaneousGesture( + /// newGesture, isEnabled: isGestureEnabled) + /// .frame(width: 200, height: 200) + /// .border(Color.purple) + /// } + /// } + /// + /// - Parameters: + /// - gesture: A gesture to attach to the view. + /// - name: A string that identifies the gesture. In iOS, the name can be + /// used to set up failure relationships between UIKit gesture + /// recognizers and this gesture. + /// - isEnabled: Whether the added gesture is enabled. The default value + /// is `true`. + nonisolated public func simultaneousGesture( + _ gesture: T, + name: String, + isEnabled: Bool = true + ) -> some View where T: Gesture { + modifier(SimultaneousGestureModifier( + gesture, + name: name, + gestureMask: isEnabled ? .all : .subviews + )) + } +} + // MARK: - GestureMask /// Options that control how adding a gesture to a view affects other gestures