我正在尝试 POC 来实现圆圈之间的拖动(类似于当时的 Android 图案锁定屏幕),但在 SwiftUI 中,我正在学习如何使用 DragGestures() 来实现这一点。
我首先创建一个简单的 3 x 3 网格并将“正确路径拖动”序列设置为 (0, 3, 6),这是指网格中圆圈的索引位置,以便当用户将圆圈拖动到按照这个顺序,路径将连接(如下图所示),否则如果选择并拖动了错误的圆圈,路径将不会连接。
我已经能够从每个圆圈的中心开始拖动,并定义了 onChanged 和 onEnded 的逻辑,但我现在无法获得加入的路径,这里的一些帮助将不胜感激,这是我自己的实施如下:
import SwiftUI
struct Line: Hashable {
var start: CGPoint
var end: CGPoint
func hash(into hasher: inout Hasher) {
hasher.combine(start.x)
hasher.combine(start.y)
hasher.combine(end.x)
hasher.combine(end.y)
}
static func ==(lhs: Line, rhs: Line) -> Bool {
return lhs.start == rhs.start && lhs.end == rhs.end
}
}
//collect circle positions
struct CirclePositionsKey: PreferenceKey {
static var defaultValue: [Int: CGPoint] { [:] }
static func reduce(value: inout [Int : CGPoint], nextValue: () -> [Int : CGPoint]) {
value.merge(nextValue(), uniquingKeysWith: { $1 })
}
}
struct LineDragGesture: View {
@State private var lines: [Line] = []
@State private var currentDragPoint: CGPoint? = nil
@State private var selectedCircleIndex: Int?
@State private var selectedCirclePosition: CGPoint?
@State private var correctPaths: [Int] = [0, 3, 6] // Correct path sequence
@State private var currentPathIndex: Int = 0
@State private var circlePositions: [Int: CGPoint] = [:]
@State private var correctCircleIndex = 0
@State private var correctCirclePosition: CGPoint = .zero
@State private var nextCorrectCircleIndex = 0
@State private var nextCorrectCirclePosition: CGPoint = .zero
var sortedCoordinates: [(key: Int, value: CGPoint)] {
circlePositions.sorted(by: { $0.key < $1.key})
}
let columns: [GridItem] = [
GridItem(.flexible()),
GridItem(.flexible()),
GridItem(.flexible())
]
var body: some View {
ZStack {
// Draw each completed line
ForEach(lines, id: \.self) { line in
Path { path in
path.move(to: line.start)
path.addLine(to: line.end)
}
.stroke(Color.black, lineWidth: 6)
}
// Draw the line from the chosen circle to the current drag point
if let currentDragPoint = currentDragPoint, let startingIndex = selectedCircleIndex {
Path { path in
path.move(to: circlePositions[startingIndex] ?? .zero)
path.addLine(to: currentDragPoint)
}
.stroke(Color.black, lineWidth: 6)
}
LazyVGrid(columns: columns, spacing: 20) {
ForEach(0..<9) { index in
GeometryReader { geo in
let frame = geo.frame(in: .named("GridContainer"))
let center = CGPoint(x: frame.midX, y: frame.midY)
ZStack {
Circle()
.preference(key: CirclePositionsKey.self, value: [
index : center
])
.gesture(
DragGesture()
.onChanged { value in
// If the user starts dragging from the correct circle in the sequence
if selectedCircleIndex == nil {
selectedCircleIndex = index
selectedCirclePosition = sortedCoordinates[index].value
}
// Calculate the current drag point based on the initial circle's position
if let startingPos = selectedCirclePosition {
currentDragPoint = CGPoint(
x: startingPos.x + value.translation.width,
y: startingPos.y + value.translation.height
)
}
}
.onEnded { value in
guard let startingIndex = selectedCircleIndex,
let draggedPoint = currentDragPoint else { return }
// Check if the next point in the path is correct and if user is dragging from correct circle
if selectedCircleIndex == correctCircleIndex,
distance(from: draggedPoint, to: nextCorrectCirclePosition) <= 25 {
// Append line if correct next point is reached
lines.append(Line(start: circlePositions[startingIndex]!, end: nextCorrectCirclePosition))
removeCorrectlyGuessed()
setNextCorrectCircle()
}
// Reset the drag point and the starting circle index
currentDragPoint = nil
selectedCircleIndex = nil
selectedCirclePosition = nil
}
)
}
// .frame(width: 50, height: 50)
}
.frame(width: 50, height: 50)
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
}
.onPreferenceChange(CirclePositionsKey.self) { value in
circlePositions = value
}
.coordinateSpace(name: "GridContainer")
.onAppear {
let correctPoint = getFirstCorrectCircle()
if let correctPoint {
correctCircleIndex = correctPoint.0
correctCirclePosition = correctPoint.1
}
let nextCorrectPoint = getNextCircle()
if let nextCorrectPoint {
correctCircleIndex = nextCorrectPoint.0
correctCirclePosition = nextCorrectPoint.1
}
}
}
// Helper function to calculate distance between two points
private func distance(from: CGPoint, to: CGPoint) -> CGFloat {
return sqrt(pow(from.x - to.x, 2) + pow(from.y - to.y, 2))
}
//on load set first correct circle
private func getFirstCorrectCircle() -> (Int, CGPoint)? {
guard let pathNum = correctPaths.first else { return nil }
let position = sortedCoordinates[pathNum].value
return (pathNum, position)
}
private func getNextCircle() -> (Int, CGPoint)? {
guard correctPaths.count > 1 else { return nil }
let pathNum = correctPaths[1]
let position = sortedCoordinates[pathNum].value
return (pathNum, position)
}
private func setNextCorrectCircle() {
guard let nextPosition = getNextCircle() else { return }
correctCircleIndex = nextPosition.0
correctCirclePosition = nextPosition.1
}
private func removeCorrectlyGuessed() {
guard !correctPaths.isEmpty else { return }
correctPaths.removeFirst()
}
}
#Preview {
LineDragGesture()
}
“是否可以检测当前哪个视图位于拖动手势的位置?”的答案说明了一种可用于检测形状何时处于拖动手势下的技术(这是我的答案)。 您正在使用类似的技术。但我建议更容易更新记录当前位置的状态变量,而不是构建将线延伸到每个圆的闭合的逻辑。然后,您可以在状态变量上使用
didSet
setter 观察器函数来决定是否追加到已发现的圆数组中。
其他可能的简化:
GestureState
状态变量记录拖动位置。这会在拖动结束时自动重置。
Canvas
在连接的圆圈之间绘制线条。
GridItem
之间的水平间距非常重要,否则
LazyVGrid
将使用自己的默认间距。
struct LineDragGesture: View {
let circleSize: CGFloat = 50
let gridSpacing: CGFloat = 20
let correctPaths: [Int] = [0, 3, 6, 7, 8, 5, 1, 2, 4] // Correct path sequence
let columns: [GridItem]
@Namespace private var coordinateSpace
@GestureState private var currentDragPoint = CGPoint.zero
@State private var discoveredCircles = [Int]()
@State private var selectedCircleIndex: Int? {
didSet {
if let selectedCircleIndex {
if discoveredCircles.isEmpty {
if selectedCircleIndex == correctPaths.first {
discoveredCircles.append(selectedCircleIndex)
}
} else if discoveredCircles.count < correctPaths.count {
if selectedCircleIndex == correctPaths[discoveredCircles.count] {
discoveredCircles.append(selectedCircleIndex)
}
}
}
}
}
init() {
self.columns = [
GridItem(.flexible(), spacing: gridSpacing),
GridItem(.flexible(), spacing: gridSpacing),
GridItem(.flexible(), spacing: gridSpacing)
]
}
var body: some View {
VStack(spacing: 80) {
LazyVGrid(columns: columns, spacing: gridSpacing) {
ForEach(0..<9) { index in
Circle()
.frame(width: circleSize, height: circleSize)
.background { dragDetector(circleIndex: index ) }
}
}
.coordinateSpace(name: coordinateSpace)
.gesture(
DragGesture(minimumDistance: 0, coordinateSpace: .named(coordinateSpace))
.updating($currentDragPoint) { val, state, trans in
state = val.location
}
.onEnded { val in
selectedCircleIndex = nil
}
)
.background { joinedCircles }
Button("Reset") {
discoveredCircles.removeAll()
}
.buttonStyle(.bordered)
}
}
private func dragDetector(circleIndex: Int) -> some View {
GeometryReader { proxy in
let frame = proxy.frame(in: .named(coordinateSpace))
let isDragLocationInsideFrame = frame.contains(currentDragPoint)
let isDragLocationInsideCircle = isDragLocationInsideFrame &&
Circle().path(in: frame).contains(currentDragPoint)
Color.clear
.onChange(of: isDragLocationInsideCircle) { oldVal, newVal in
if currentDragPoint != .zero {
selectedCircleIndex = newVal ? circleIndex : nil
}
}
}
}
private var joinedCircles: some View {
Canvas { ctx, size in
let cellWidth = (size.width + gridSpacing) / 3
let cellHeight = (size.height + gridSpacing) / 3
if !discoveredCircles.isEmpty {
var isfirst = true
var path = Path()
for circleIndex in discoveredCircles {
let row = circleIndex / 3
let col = circleIndex % 3
let x = (cellWidth * CGFloat(col)) + (cellWidth / 2) - (gridSpacing / 2)
let y = (cellHeight * CGFloat(row)) + (cellHeight / 2) - (gridSpacing / 2)
let point = CGPoint(x: x, y: y)
if isfirst {
path.move(to: point)
isfirst = false
} else {
path.addLine(to: point)
}
}
if currentDragPoint != .zero {
path.addLine(to: currentDragPoint)
}
ctx.stroke(path, with: .color(.secondary), style: .init(lineWidth: 6, lineCap: .round))
}
}
}
}