如何在 SwiftUI 中识别和处理手势(Tap, Drag, Magnification 等)?
在 SwiftUI 中,处理手势的核心修饰符是 .gesture()。你可以将各种手势结构体(如 TapGesture, DragGesture 等)传递给这个修饰符。
以下是 SwiftUI 中常用手势的识别与处理方法的详细指南。
1. 点击手势 (Tap Gesture)
这是最简单的手势。SwiftUI 提供了一个简便的修饰符 .onTapGesture,也可以使用完整的 TapGesture 来处理双击或多击。
基础单击
plaintext
Text("点击我")
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
.onTapGesture {
print("被点击了!")
}
双击 (Double Tap)
plaintext
Image(systemName: "heart.fill")
.resizable()
.frame(width: 50, height: 50)
.foregroundColor(.red)
.gesture(
TapGesture(count: 2) // 设置点击次数
.onEnded {
print("双击检测成功!")
}
)
2. 长按手势 (Long Press Gesture)
用于检测用户按住视图一段时间。
plaintext
struct LongPressView: View {
@State private var isPressed = false
var body: some View {
Circle()
.fill(isPressed ? Color.red : Color.green)
.frame(width: 100, height: 100)
.gesture(
LongPressGesture(minimumDuration: 1.0) // 按住至少1秒
.onEnded { _ in
print("长按结束")
isPressed.toggle()
}
.onChanged { _ in
// 注意:LongPressGesture 的 onChanged 并不常用,
// 通常配合 onPressingChanged (iOS 14+) 使用
}
)
// iOS 14+ 推荐方式:监听按压状态变化
.onLongPressGesture(minimumDuration: 1.0, pressing: { pressing in
// pressing 为 true 表示正在按住,false 表示松开
if pressing {
print("开始按压...")
} else {
print("按压取消或完成")
}
}, perform: {
print("长按动作触发!")
})
}
}
3. 拖拽手势 (Drag Gesture)
用于移动视图或获取滑动的偏移量。通常配合 @State 或 @GestureState 使用。
plaintext
struct DragView: View {
@State private var offset = CGSize.zero
@State private var lastOffset = CGSize.zero // 记录上一次结束的位置
var body: some View {
RoundedRectangle(cornerRadius: 20)
.fill(Color.orange)
.frame(width: 100, height: 100)
.offset(x: offset.width, y: offset.height)
.gesture(
DragGesture()
.onChanged { value in
// value.translation 是从手势开始时的总位移
self.offset = CGSize(
width: lastOffset.width + value.translation.width,
height: lastOffset.height + value.translation.height
)
}
.onEnded { value in
// 手势结束,保存当前位置
self.lastOffset = self.offset
}
)
}
}
4. 缩放手势 (Magnification Gesture)
用于双指捏合缩放。
plaintext
struct ZoomView: View {
@State private var currentScale: CGFloat = 1.0
@State private var finalScale: CGFloat = 1.0
var body: some View {
Image(systemName: "photo")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 200, height: 200)
.scaleEffect(currentScale * finalScale)
.gesture(
MagnificationGesture()
.onChanged { value in
// value 是缩放倍数 (例如 1.5)
currentScale = value
}
.onEnded { value in
finalScale *= currentScale
currentScale = 1.0
}
)
}
}
5. 旋转手势 (Rotation Gesture)
用于双指旋转视图。
plaintext
struct RotateView: View {
@State private var currentAngle = Angle.zero
@State private var finalAngle = Angle.zero
var body: some View {
Rectangle()
.fill(Color.purple)
.frame(width: 150, height: 150)
.rotationEffect(currentAngle + finalAngle)
.gesture(
RotationGesture()
.onChanged { angle in
currentAngle = angle
}
.onEnded { angle in
finalAngle += currentAngle
currentAngle = .zero
}
)
}
}
6. 高级技巧:使用 @GestureState
@GestureState 是处理手势的利器。它的特点是:当手势结束时,它会自动重置为初始值。这非常适合制作“松手回弹”的效果。
plaintext
struct GestureStateView: View {
// 拖拽时更新 offset,松手后自动变回 CGSize.zero
@GestureState private var dragOffset = CGSize.zero
var body: some View {
Circle()
.fill(Color.pink)
.frame(width: 100, height: 100)
.offset(dragOffset)
.gesture(
DragGesture()
.updating($dragOffset) { value, state, transaction in
// value: 手势当前数据
// state: 对应的 dragOffset 属性 (inout)
// transaction: 动画事务
state = value.translation
}
)
.animation(.spring(), value: dragOffset) // 添加回弹动画
}
}
7. 组合手势 (Composing Gestures)
SwiftUI 允许你将多个手势组合在一起。主要有三种组合方式:
- Simultaneously (同时进行): 例如一边拖拽一边缩放。
- Sequenced (顺序进行): 例如先长按,然后才能拖拽。
- Exclusive (排他): 两个手势竞争,只有一个能生效。
示例:同时缩放和旋转
plaintext
struct CombinedGestureView: View {
@State private var scale: CGFloat = 1.0
@State private var angle: Angle = .zero
var body: some View {
let magnification = MagnificationGesture()
.onChanged { v in scale = v }
.onEnded { _ in scale = 1.0 }
let rotation = RotationGesture()
.onChanged { a in angle = a }
.onEnded { _ in angle = .zero }
// 使用 .simultaneously 组合
let combined = magnification.simultaneously(with: rotation)
Rectangle()
.fill(Color.blue)
.frame(width: 150, height: 150)
.scaleEffect(scale)
.rotationEffect(angle)
.gesture(combined)
}
}
总结与注意事项
- 点击区域 (
contentShape): 如果你给一个透明的视图(如Spacer或Color.clear)添加手势,默认可能无法触发。你需要添加.contentShape(Rectangle())来定义可点击区域。 - 冲突处理: 在
ScrollView或List中使用DragGesture可能会与滚动手势冲突。通常需要根据优先级调整。 - 状态更新:
onChanged会非常频繁地调用,尽量避免在其中进行过重的计算。