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
40 changes: 40 additions & 0 deletions Example/Shared/Event/Gesture/DragGestureExample.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
//
// DragGestureExample.swift
// Shared

#if OPENSWIFTUI
import OpenSwiftUI
#else
import SwiftUI
#endif

struct DragGestureExample: View {
@State private var position: CGSize = .zero
@GestureState private var dragOffset: CGSize = .zero

var drag: some Gesture {
DragGesture()
.updating($dragOffset) { value, state, _ in
state = value.translation
}
.onEnded { value in
position.width += value.translation.width
position.height += value.translation.height
}
}

var body: some View {
Circle()
.fill(dragOffset == .zero ? Color.blue : Color.green)
.frame(width: 100, height: 100)
.offset(
x: position.width + dragOffset.width,
y: position.height + dragOffset.height
)
.gesture(drag)
}
}

#Preview {
DragGestureExample()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
//
// DragGestureInteractionTests.swift
// OpenSwiftUIInteractionTests

#if OPENSWIFTUI
import OpenSwiftUI
#else
import SwiftUI
#endif
import OpenSwiftUITestsSupport
import SnapshotTesting
import Testing

@MainActor
@Suite(.serialized, .tags(.interaction), .snapshots(record: .never, diffTool: diffTool))
struct DragGestureInteractionTests {
private struct ContentView: View {
var minimumDistance: Double
var onChanged: (DragGesture.Value) -> Void
var onEnded: (DragGesture.Value) -> Void

var body: some View {
Color.red
.gesture(
DragGesture(minimumDistance: minimumDistance)
.onChanged { onChanged($0) }
.onEnded { onEnded($0) }
)
}
}

@Test
func dragReportsTranslation() async throws {
var changedCount = 0
var endedValue: DragGesture.Value?
let content = ContentView(
minimumDistance: 10.0,
onChanged: { _ in changedCount += 1 },
onEnded: { endedValue = $0 }
)
try await withInteractionTestHost(of: content) { host in
try await host.drag(by: CGSize(width: 60, height: 0))
try await host.waitUntil(endedValue != nil)
let value = try #require(endedValue)
#expect(changedCount > 0)
#expect(abs(value.translation.width - 60) < 1)
#expect(abs(value.translation.height) < 1)
#expect(abs(value.location.x - value.startLocation.x - 60) < 1)
}
}

@Test
func dragStartsAfterMinimumDistance() async throws {
var changedValues: [DragGesture.Value] = []
var endedValues: [DragGesture.Value] = []
let content = ContentView(
minimumDistance: 10.0,
onChanged: { changedValues.append($0) },
onEnded: { endedValues.append($0) }
)
try await withInteractionTestHost(of: content) { host in
// A drag shorter than the default minimum distance must not start the gesture.
try await host.drag(by: CGSize(width: 4, height: 0), duration: 0.1)
try await host.drag(by: CGSize(width: 60, height: 0))
try await host.waitUntil(!endedValues.isEmpty)
#expect(endedValues.count == 1)
let firstChange = try #require(changedValues.first)
#expect(firstChange.translation.width >= 10)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,22 @@ struct InteractionTestHost {
#endif
}

/// Drags from the center of the hosted view by `translation`, in view coordinates.
func drag(by translation: CGSize, duration: TimeInterval = 0.3) async throws {
#if os(macOS)
let start = try view.windowHitPoint(for: events)
// AppKit window coordinates use a bottom-left origin.
let end = CGPoint(x: start.x + translation.width, y: start.y - translation.height)
try await events.mouseDrag(from: start, to: end, duration: duration)
#else
try await onMainRunLoop {
let start = try view.windowHitPoint(for: events)
let end = CGPoint(x: start.x + translation.width, y: start.y + translation.height)
try events.fingerDrag(from: start, to: end, duration: duration)
}
#endif
}

func waitUntil(
_ condition: @autoclosure @escaping () -> Bool,
timeout: TimeInterval = 3
Expand Down
3 changes: 2 additions & 1 deletion Example/Tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@ The default content size is 200×200 points, shared with UI tests through
`defaultSize` in [Shared/SnapshotTesting.swift](Shared/SnapshotTesting.swift).
Pass `size` only when a test needs another content size. The hosted view and its
snapshots use that size on both macOS and iOS, independently of the screen size.
Call the helper, `tap()`, and `waitUntil(_:timeout:)` with `try await`.
Call the helper, `tap()`, `drag(by:duration:)`, and `waitUntil(_:timeout:)` with
`try await`.
`assertSnapshot(named:)` captures the current hosted view and preserves its
state for later interactions:

Expand Down
6 changes: 3 additions & 3 deletions Example/Tuist/Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Example/Tuist/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ var dependencies: [PackageDescription.Package.Dependency] = [
.package(url: "https://github.com/OpenSwiftUIProject/equatable.git", branch: "main"),
.package(url: "https://github.com/OpenSwiftUIProject/SymbolLocator.git", from: "0.2.0"),
.package(url: "https://github.com/OpenSwiftUIProject/swift-snapshot-testing.git", exact: "1.19.3"),
.package(url: "https://github.com/OpenSwiftUIProject/Hammer.git", exact: "0.19.2"),
.package(url: "https://github.com/OpenSwiftUIProject/Hammer.git", exact: "0.19.3"),
]

if openCombineCondition {
Expand Down
Loading
Loading