功能
为 View 添加“单指拖拽”手势,实时返回手指在屏幕上的位移向量(CGSize 或 CGPoint),支持变化跟踪与结束复位,常用于卡片滑动、抽屉升降、拖拽排序等场景。
参数
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)
}
}
注意事项
translation为累计位移,若需相对上一次增量,请保存startLocation自行计算。- 结束回调里做动画,务必包
withAnimation,否则视图会瞬间跳回。 - 与
TapGesture、LongPressGesture共存时,使用.highPriorityGesture或.simultaneousGesture控制识别优先级。 - 耗时业务(网络、数据库)请放到
onEnded的异步闭包,避免阻塞主线程。