在 SwiftUI 中拖动手势连接 Circles

问题描述 投票:0回答:1

我正在尝试 POC 来实现圆圈之间的拖动(类似于当时的 Android 图案锁定屏幕),但在 SwiftUI 中,我正在学习如何使用 DragGestures() 来实现这一点。

我首先创建一个简单的 3 x 3 网格并将“正确路径拖动”序列设置为 (0, 3, 6),这是指网格中圆圈的索引位置,以便当用户将圆圈拖动到按照这个顺序,路径将连接(如下图所示),否则如果选择并拖动了错误的圆圈,路径将不会连接。 enter image description here enter image description here

我已经能够从每个圆圈的中心开始拖动,并定义了 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()
}
ios swift swiftui gesture
1个回答
0
投票

“是否可以检测当前哪个视图位于拖动手势的位置?”的答案说明了一种可用于检测形状何时处于拖动手势下的技术(这是我的答案)。 您正在使用类似的技术。但我建议更容易更新记录当前位置的状态变量,而不是构建将线延伸到每个圆的闭合的逻辑。然后,您可以在状态变量上使用

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)) } } } }

Animation

© www.soinside.com 2019 - 2024. All rights reserved.