SwiftUI 手势笔记

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

阈值设置:加入阈值后,需达到指定角度才会触发旋转。

swift 复制代码
RotateGesture(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()
}

阈值设置:加入阈值后,需达到最小移动距离才会触发拖动。

swift 复制代码
DragGesture(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
}
相关推荐
金銀銅鐵2 小时前
[Python] 从《千字文》中随机挑选汉字
后端·python
橙子家2 小时前
浏览器缓存之【结构化数据库与缓存】: IndexedDB、Cache storage 和 Storage buckets
前端
user20585561518132 小时前
X6 中边悬浮置顶,规避 `mouseleave` 事件丢失问题
前端
李明卫杭州2 小时前
CSS aspect-ratio 属性完全指南
前端
Pedantic4 小时前
SwiftUI 手势层级(Gesture Hierarchy)详解
前端
飘尘4 小时前
前端转型全栈(Java后端)的快速上手指引
前端·后端·全栈
一颗烂土豆4 小时前
Meshopt 压缩深度解析,为什么它比 Draco 更快
前端·javascript·webgl
浏览器工程师5 小时前
AI Agent 接浏览器任务,先别让它一路点到底
前端·后端