SwiftUI 手势笔记
涵盖三种基础手势(旋转、拖动、缩放)的基本用法、阈值设置、累计计算、@GestureState 的优雅写法,以及三种复合手势(互斥、共存、顺序)。
- 一、基础手势
- [1. 旋转 RotateGesture](#1. 旋转 RotateGesture "#1-%E6%97%8B%E8%BD%AC-rotategesture")
- [2. 拖动 DragGesture](#2. 拖动 DragGesture "#2-%E6%8B%96%E5%8A%A8-draggesture")
- [3. 缩放 MagnifyGesture](#3. 缩放 MagnifyGesture "#3-%E7%BC%A9%E6%94%BE-magnifygesture")
- 二、复合手势
- [1. 互斥手势 ExclusiveGesture](#1. 互斥手势 ExclusiveGesture "#1-%E4%BA%92%E6%96%A5%E6%89%8B%E5%8A%BF-exclusivegesture")
- [2. 共存手势 SimultaneousGesture](#2. 共存手势 SimultaneousGesture "#2-%E5%85%B1%E5%AD%98%E6%89%8B%E5%8A%BF-simultaneousgesture")
- [3. 顺序手势 SequenceGesture](#3. 顺序手势 SequenceGesture "#3-%E9%A1%BA%E5%BA%8F%E6%89%8B%E5%8A%BF-sequencegesture")
- [三、@GestureState 详解](#三、@GestureState 详解 "#%E4%B8%89gesturestate-%E8%AF%A6%E8%A7%A3")
- [附录:CGSize 结构](#附录:CGSize 结构 "#%E9%99%84%E5%BD%95cgsize-%E7%BB%93%E6%9E%84")
一、基础手势
1. 旋转 RotateGesture
基本用法
swift
import SwiftUI
struct RotateGestureDemo: View {
@State private var angle: Angle = .zero
var body: some View {
RoundedRectangle(cornerRadius: 20)
.fill(.blue)
.frame(width: 200, height: 120)
.rotationEffect(angle)
.gesture(
RotateGesture()
.onChanged { value in
angle = value.rotation
}
)
}
}
#Preview {
RotateGestureDemo()
}
阈值设置:加入阈值后,需达到指定角度才会触发旋转。
swiftRotateGesture(minimumAngleDelta: .degrees(10))
连续旋转(累计角度)
单次手势结束后,角度会从 0 重新开始计算,因此需要用 lastAngle 保存上一次结束时的角度,下一次手势在此基础上累加:
swift
import SwiftUI
struct RotateGestureDemo2: View {
@State private var angle: Angle = .zero
@State private var lastAngle: Angle = .zero
var body: some View {
RoundedRectangle(cornerRadius: 20)
.fill(.orange)
.frame(width: 200, height: 120)
.rotationEffect(angle)
.gesture(
RotateGesture()
.onChanged { value in
angle = lastAngle + value.rotation
}
.onEnded { _ in
lastAngle = angle
}
)
}
}
2. 拖动 DragGesture
基本用法(松手回弹)
swift
import SwiftUI
struct DragGestureDemo: View {
@State private var offset: CGSize = .zero
var body: some View {
Circle()
.fill(.blue)
.frame(width: 120, height: 120)
.offset(offset)
.gesture(
DragGesture()
.onChanged { value in
offset = value.translation
}
.onEnded { _ in
withAnimation(.spring()) {
offset = .zero
}
}
)
}
}
#Preview {
DragGestureDemo()
}
阈值设置:加入阈值后,需达到最小移动距离才会触发拖动。
swiftDragGesture(minimumDistance: 30)
连续拖动(累计偏移)
同旋转手势一样,用 lastOffset 保存上一次结束时的位置,下一次拖动在此基础上累加:
swift
import SwiftUI
struct DragGestureDemo2: View {
@State private var offset: CGSize = .zero
@State private var lastOffset: CGSize = .zero
var body: some View {
Circle()
.fill(.orange)
.frame(width: 120, height: 120)
.offset(offset)
.gesture(
DragGesture()
.onChanged { value in
offset = CGSize(
width: lastOffset.width + value.translation.width,
height: lastOffset.height + value.translation.height
)
}
.onEnded { _ in
lastOffset = offset
}
)
}
}
#Preview {
DragGestureDemo2()
}
使用 @GestureState 实现更优雅的临时拖拽
相比上面手动维护 lastOffset,用 @GestureState 表示"本次手势进行中的临时偏移量",手势结束后自动归零,逻辑更清晰:
swift
import SwiftUI
struct DragGestureDemo3: View {
@GestureState private var dragOffset: CGSize = .zero
@State private var position: CGSize = .zero
var body: some View {
Circle()
.fill(.green)
.frame(width: 120, height: 120)
.offset(
CGSize(
width: position.width + dragOffset.width,
height: position.height + dragOffset.height
)
)
.gesture(
DragGesture()
.updating($dragOffset) { value, state, _ in
state = value.translation
}
.onEnded { value in
position.width += value.translation.width
position.height += value.translation.height
}
)
}
}
#Preview {
DragGestureDemo3()
}
思路对比:
| 方案 | 状态变量 | 手势结束后 |
|---|---|---|
| 手动累计(Demo2) | offset + lastOffset(均为 @State) |
需在 onEnded 手动把 offset 存入 lastOffset |
@GestureState(Demo3) |
dragOffset(@GestureState)+ position(@State) |
dragOffset 自动归零,无需手动复位 |
3. 缩放 MagnifyGesture
基本用法
swift
import SwiftUI
struct MagnifyGestureDemo: View {
@State private var scale: CGFloat = 1.0
var body: some View {
Image(systemName: "photo")
.resizable()
.frame(width: 150, height: 150)
.scaleEffect(scale)
.gesture(
MagnifyGesture()
.onChanged { value in
scale = value.magnification
}
)
}
}
#Preview {
MagnifyGestureDemo()
}
magnification表示缩放倍率(基准值为 1.0)。
连续缩放(累计倍率)
累计缩放需要保存上一次结束时的倍率 lastScale,下一次缩放在此基础上相乘:
swift
import SwiftUI
struct MagnifyGestureDemo2: View {
@State private var scale: CGFloat = 1.0
@State private var lastScale: CGFloat = 1.0
var body: some View {
Image(systemName: "photo")
.resizable()
.frame(width: 150, height: 150)
.scaleEffect(scale)
.gesture(
MagnifyGesture()
.onChanged { value in
scale = lastScale * value.magnification
}
.onEnded { _ in
lastScale = scale
}
)
}
}
二、复合手势
1. 互斥手势 ExclusiveGesture
两个手势同时存在,谁先触发就只执行谁,二者是互斥关系:一旦某个手势胜出,另一个就不再有机会触发。
swift
struct ContentView: View {
@State private var color: Color = .blue
@State private var scale: CGFloat = 1.0
var body: some View {
Capsule()
.fill(color)
.frame(width: 200, height: 100)
.scaleEffect(scale)
.gesture(
ExclusiveGesture(
TapGesture()
.onEnded { color = .orange },
LongPressGesture(minimumDuration: 1.0)
.onEnded { _ in scale = 1.5 }
)
)
.animation(.easeInOut, value: scale)
}
}
- 快速轻点 →
TapGesture胜出,变橙色 - 按住 1 秒以上 →
LongPressGesture胜出,放大到 1.5 倍
2. 共存手势 SimultaneousGesture
拖动、缩放、旋转等手势可以同时进行,互不阻塞。
swift
struct ContentView: View {
@State private var offset = CGSize.zero
@State private var angle = Angle.zero
var body: some View {
Rectangle()
.fill(.blue)
.frame(width: 150, height: 150)
.offset(offset)
.rotationEffect(angle)
.gesture(
DragGesture()
.onChanged { value in offset = value.translation }
.simultaneously(with:
RotateGesture()
.onChanged { value in angle = value.rotation }
)
)
}
}
.simultaneously(with:) 原理
.simultaneously(with:) 是 Gesture 协议的方法,把两个手势合并成一个新的复合手势 ,两者同时独立识别、同时触发各自回调,对外暴露的是一个元组类型 的 Value。
swift
DragGesture()
.simultaneously(with: RotateGesture())
// → SimultaneousGesture<DragGesture, RotateGesture>
其 Value 定义大致如下,两个字段均为可选(Optional),因为两个手势不一定同时都"有数据":
swift
struct SimultaneousGesture<First: Gesture, Second: Gesture>: Gesture {
struct Value {
var first: First.Value?
var second: Second.Value?
}
}
写法一:子手势各自带 .onChanged(逻辑独立,互不干扰)
swift
DragGesture()
.onChanged { value in offset = value.translation }
.simultaneously(with:
RotateGesture()
.onChanged { value in angle = value.rotation }
)
写法二:在外层统一处理(需手动解包元组,适合联动计算)
swift
.gesture(
DragGesture()
.simultaneously(with: RotateGesture())
.onChanged { value in
if let drag = value.first {
offset = drag.translation
}
if let rotate = value.second {
angle = rotate.rotation
}
}
)
三个及以上手势的嵌套组合
.simultaneously(with:) 每次只能合并两个手势,但可链式嵌套:
swift
DragGesture()
.simultaneously(with: RotateGesture())
.simultaneously(with: MagnificationGesture())
此时 Value 变为嵌套元组:value.first.first(drag)、value.first.second(rotate)、value.second(magnification)。解包会比较繁琐,实际项目中三个以上手势同时识别,通常会拆成多次 .simultaneousGesture() 视图修饰符,而非一直嵌套。
完整示例:拖拽 + 缩放 + 旋转 三合一
swift
struct ContentView: View {
@State private var offset = CGSize.zero
@State private var scale: CGFloat = 1.0
@State private var angle = Angle.zero
var body: some View {
Rectangle()
.fill(.blue)
.frame(width: 150, height: 150)
.offset(offset)
.scaleEffect(scale)
.rotationEffect(angle)
.gesture(
DragGesture()
.onChanged { value in offset = value.translation }
.simultaneously(with:
MagnificationGesture()
.onChanged { value in scale = value }
)
.simultaneously(with:
RotateGesture()
.onChanged { value in angle = value.rotation }
)
)
}
}
3. 顺序手势 SequenceGesture
先完成某个手势,才能触发下一个手势。
swift
struct ContentView: View {
@GestureState private var isPressed = false
@State private var offset = CGSize.zero
var body: some View {
Circle()
.fill(isPressed ? .orange : .blue)
.frame(width: 100, height: 100)
.offset(offset)
.gesture(
LongPressGesture(minimumDuration: 0.5)
.sequenced(before: DragGesture())
.updating($isPressed) { value, state, _ in
switch value {
case .first(true): state = true
case .second(true, _): state = true
default: state = false
}
}
.onChanged { value in
if case .second(true, let drag?) = value {
offset = drag.translation
}
}
)
}
}
before的含义 :拖动手势被作为参数传给长按手势调用,before可理解为"调用方(长按)排在拖动之前",即A.sequenced(before: B)表示 A 先发生、B 后发生。
case分支可做的操作 :在switch value的每个分支里,除了给state赋值外,还可以执行任意代码,例如触发震动反馈、播放动画、调用业务函数等。
三、@GestureState 详解
@GestureState 是专门为"手势进行过程中的临时状态"设计的属性包装器(property wrapper)。
核心特性 :手势一结束(无论成功、失败还是被取消),它包装的值会自动重置回初始值,不需要手动写代码复位。
例如顺序手势示例中的
state,存放的就是"是否处于按下/拖拽中"的判断结果:按下时为true,手势结束自动变回false。
swift
struct ContentView: View {
@GestureState private var isLongPressed = false
var body: some View {
Circle()
.fill(.blue)
.frame(width: 100, height: 100)
.scaleEffect(isLongPressed ? 1.3 : 1.0)
.animation(.easeInOut, value: isLongPressed)
.gesture(
LongPressGesture(minimumDuration: 0.3)
.updating($isLongPressed) { value, state, _ in
state = value // value 是 Bool,直接赋给 state
}
)
}
}
按住 0.3 秒后圆放大,手指一松开,isLongPressed 由框架自动设回 false,圆自动缩回------无需手动写 onEnded { isLongPressed = false }。
附录:CGSize 结构 {#附录cgsize-结构}
CGSize 表示一个二维尺寸,包含宽度(width)和高度(height):
swift
struct CGSize {
var width: CGFloat
var height: CGFloat
}