#4 DragGesture

10 阅读2分钟

功能

为 View 添加“单指拖拽”手势,实时返回手指在屏幕上的位移向量(CGSizeCGPoint),支持变化跟踪与结束复位,常用于卡片滑动、抽屉升降、拖拽排序等场景。

参数

  • translation:相对起始点的累计位移(CGSize)
  • location:当前点在屏幕的绝对坐标(CGPoint)
  • startLocation:手势起始点坐标(CGPoint)
  • velocity:松开瞬间的速度向量(CGSize)

示例

struct DragGestureBootcamp: View {
    
    @State var offset: CGSize = .zero
    
    var body: some View {
        
        ZStack {
            VStack {
                Text("\(offset.width)")
                Spacer()
                
            }
            
            RoundedRectangle(cornerRadius: 16)
                .frame(width: 320, height: 480)
                .offset(offset)
                .scaleEffect(getScaleAmount())
                .rotationEffect(Angle(degrees: getRotationAmount()))
                .gesture(
                    DragGesture()
                        .onChanged({ value in
                            withAnimation(.spring()) {
                                offset = value.translation
                            }
                        })
                        .onEnded({ value in
                            withAnimation(.spring()) {
                                offset = .zero
                            }
                        })
                )
        }
    }
    
    private func getScaleAmount() -> CGFloat {
        // max of the width control can go
        let max = UIScreen.main.bounds.width / 2
        let currentAmount = abs(offset.width)
        let percentage = currentAmount / max
        
        return 1.0 - min(percentage, 0.5) * 0.5
    }
    
    private func getRotationAmount() -> Double {
        let max = UIScreen.main.bounds.width / 2
        let currentAmount = offset.width
        let percentage = currentAmount / max
        let percentageAsDouble = Double(percentage)
        let maxAngle: Double = 10
        
        return percentageAsDouble * maxAngle
    }
}


struct DragGestureBootcamp2: View {
    
    @State var startingOffsetY: CGFloat = UIScreen.main.bounds.height * 0.84
    @State var currentDragOffsetY: CGFloat = 0
    @State var endingOffsetY: CGFloat = 0
    
    var body: some View {
        ZStack {
            Color.green.ignoresSafeArea()
            
            MySignUpView()
                .offset(y: startingOffsetY)
                .offset(y: currentDragOffsetY)
                .offset(y: endingOffsetY)
                .gesture(
                    DragGesture()
                        .onChanged({ value in
                            withAnimation(.spring()) {
                                currentDragOffsetY = value.translation.height
                            }
                        })
                        .onEnded({ value in
                            withAnimation(.spring()) {
                                if currentDragOffsetY < -150 {
                                    endingOffsetY = -startingOffsetY
                                } else if endingOffsetY != 0 && currentDragOffsetY > 150 {
                                    endingOffsetY = 0
                                }
                                
                                currentDragOffsetY = 0
                            }
                        })
                )
            
            Text("\(currentDragOffsetY)")
        }
        .ignoresSafeArea(edges: .bottom)
    }
}

struct MySignUpView: View {
    var body: some View {
        VStack {
            Image(systemName: "chevron.up")
                .padding()
            Text("Sign up")
                .font(.headline)
                .fontWeight(.semibold)
            Image(systemName: "flame.fill")
                .resizable()
                .scaledToFit()
                .frame(width: 108, height: 108)
            Text("This is the description for our app. This is my favoriate SwiftUI course and I recommand to all of my friends to subscribe to Swiftful Thinking!")
                .multilineTextAlignment(.center)
            Text("CREATE AN ACCOUNT")
                .foregroundStyle(.white)
                .font(.headline)
                .padding()
                .padding(.horizontal)
                .background(.black)
                .cornerRadius(8)
            
            Spacer()
        }
        .frame(maxWidth: .infinity)
        .background(.white)
        .cornerRadius(32)
    }
}

注意事项

  1. translation 为累计位移,若需相对上一次增量,请保存 startLocation 自行计算。
  2. 结束回调里做动画,务必包 withAnimation,否则视图会瞬间跳回。
  3. TapGestureLongPressGesture 共存时,使用 .highPriorityGesture.simultaneousGesture 控制识别优先级。
  4. 耗时业务(网络、数据库)请放到 onEnded 的异步闭包,避免阻塞主线程。