SwiftUI 手势层级(Gesture Hierarchy)详解

彻底理解 GestureMask、highPriorityGesture 和 simultaneousGesture

在学习 SwiftUI 手势时,很多开发者都会遇到这些问题:

  • 为什么点击子 View,父 View 的 onTapGesture 没有执行?
  • GestureMask.gesture.subviews.all 到底是什么意思?
  • highPriorityGesturesimultaneousGesture 有什么区别?
  • 为什么 ScrollViewDragGesture 经常发生冲突?

这些问题都与 Gesture Hierarchy(手势层级) 有关。

本文通过大量示例,彻底理解 SwiftUI 的手势事件是如何传递的。


一、SwiftUI 的手势为什么会有层级?

假设有这样一个界面:

swift 复制代码
VStack {
    Rectangle()
}

对应的视图结构:

text 复制代码
VStack
└── Rectangle

如果父、子 View 都添加了点击手势:

swift 复制代码
VStack {
    Rectangle()
        .onTapGesture {
            print("Rectangle")
        }
}
.onTapGesture {
    print("VStack")
}

那么点击 Rectangle 时,到底是谁响应?

text 复制代码
Rectangle?
还是 VStack?
还是两个一起?

SwiftUI 默认有一套手势优先级规则。


二、默认规则:子 View 优先

运行下面代码:

swift 复制代码
import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            Rectangle()
                .fill(.blue)
                .frame(width: 200, height: 200)
                .onTapGesture {
                    print("Rectangle")
                }
        }
        .frame(maxWidth: .infinity, maxHeight: .infinity)
        .background(.yellow)
        .onTapGesture {
            print("VStack")
        }
    }
}

界面如下:

text 复制代码
+----------------------+
|                      |
|    Rectangle         |
|                      |
+----------------------+

点击 Rectangle:

text 复制代码
👆

输出:

text 复制代码
Rectangle

而不是:

text 复制代码
VStack

为什么?

因为 SwiftUI 默认优先让距离用户最近的 View 处理手势。

事件的传递顺序可以理解为:

text 复制代码
用户点击
    │
    ▼
Rectangle
    │
    ▼
VStack

如果 Rectangle 已经成功识别手势,事件通常不会继续传递给 VStack。

如果点击黄色背景:

text 复制代码
👆

输出:

text 复制代码
VStack

因为这里只有父 View 可以响应。


三、GestureMask 到底是什么?

很多人第一次看到:

swift 复制代码
.gesture(
    TapGesture(),
    including: .gesture
)

都会疑惑:

这个 including 是什么意思?

其实:

including 用来指定 当前 Gesture 的作用范围(GestureMask)

它并不会修改手势类型,而是决定:

当前这个 Gesture 应该监听哪些 View。

SwiftUI 提供了四种 GestureMask:

swift 复制代码
.gesture
.subviews
.all
.none

下面分别介绍。


四、.gesture ------ 只监听当前 View

这是最容易误解的一个。

例如:

swift 复制代码
VStack {

    Rectangle()
        .fill(.blue)
        .frame(width: 200, height: 200)
        .onTapGesture {
            print("Rectangle")
        }

}
.gesture(
    TapGesture()
        .onEnded {
            print("VStack")
        },
    including: .gesture
)

很多人看到 .gesture,会误以为:

这是"普通 Gesture"。

其实不是。

它真正的意思是:

当前 View 自己参与手势识别,不把子 View 包含进来。

这里 Gesture 是添加到 VStack 上的,因此:

text 复制代码
VStack       ✅

Rectangle    ❌(不属于 VStack 这个 Gesture 的监听范围)

⚠️ 注意:

这里的 ❌ 并不是说 Rectangle 不能点击。

Rectangle 自己的 Gesture 仍然可以正常工作。

只是:

VStack 不会把 Rectangle 的区域也当成自己的 Gesture 区域。

因此:

点击 Rectangle:

text 复制代码
Rectangle

点击黄色背景:

text 复制代码
VStack

一句话总结:

.gesture = 当前 View,不包括子 View。


五、.subviews ------ 只监听子 View

修改代码:

swift 复制代码
VStack {

    Rectangle()
        .fill(.blue)
        .frame(width: 200, height: 200)

}
.gesture(
    TapGesture()
        .onEnded {
            print("VStack")
        },
    including: .subviews
)

此时:

text 复制代码
VStack      ❌

Rectangle   ✅

什么意思?

父 View 的 Gesture:

只监听子 View。

因此:

点击 Rectangle:

text 复制代码
VStack

点击黄色背景:

没有任何输出。

很多人第一次看到都会觉得奇怪。

其实就是:

text 复制代码
监听孩子

不监听自己

一句话总结:

.subviews = 只监听子 View。


六、.all ------ 当前 View 和子 View 都监听

改成:

swift 复制代码
.gesture(
    TapGesture()
        .onEnded {
            print("VStack")
        },
    including: .all
)

此时:

text 复制代码
VStack      ✅

Rectangle   ✅

无论点击:

text 复制代码
黄色背景

还是:

text 复制代码
Rectangle

VStack 的 Gesture 都能够参与识别。

可以理解成:

text 复制代码
整个 View 树
都是我的监听范围

一句话总结:

.all = 当前 View + 子 View。


七、.none ------ 不监听任何区域

swift 复制代码
.gesture(
    TapGesture(),
    including: .none
)

表示:

当前 Gesture 完全失效。

无论点击哪里:

都不会响应。

一句话总结:

.none = 禁用当前 Gesture。


八、一张图理解 GestureMask

假设:

text 复制代码
VStack
│
├── Rectangle
└── Circle

.gesture

text 复制代码
VStack      ✅

Rectangle   ❌

Circle      ❌

.subviews

text 复制代码
VStack      ❌

Rectangle   ✅

Circle      ✅

.all

text 复制代码
VStack      ✅

Rectangle   ✅

Circle      ✅

.none

text 复制代码
VStack      ❌

Rectangle   ❌

Circle      ❌

最终可以总结成下面这张表:

GestureMask 当前 View 子 View
.gesture
.subviews
.all
.none

注意:

这里的"当前 View"指的是:

谁调用了 .gesture(..., including:),谁就是当前 View。

它并不一定表示父 View。


九、highPriorityGesture()

默认情况下:

一个 View 可以拥有多个 Gesture。

例如:

swift 复制代码
Rectangle()
    .gesture(
        TapGesture()
            .onEnded {
                print("普通 Tap")
            }
    )
    .highPriorityGesture(
        TapGesture()
            .onEnded {
                print("高优先级 Tap")
            }
    )

点击:

输出:

text 复制代码
高优先级 Tap

因为:

text 复制代码
highPriorityGesture

        ↑

gesture

highPriorityGesture 的优先级更高。

一句话理解:

它会优先参与手势识别。


十、simultaneousGesture()

如果希望两个 Gesture 同时执行:

swift 复制代码
Rectangle()
    .gesture(
        TapGesture()
            .onEnded {
                print("普通")
            }
    )
    .simultaneousGesture(
        TapGesture()
            .onEnded {
                print("同时")
            }
    )

点击:

输出:

text 复制代码
普通

同时

两个 Gesture 都会执行。

一句话理解:

允许多个 Gesture 同时识别。


十一、三种 Gesture 的区别

可以用下面这张图来记忆:

text 复制代码
gesture()

↓

正常排队
text 复制代码
highPriorityGesture()

↓

插队
text 复制代码
simultaneousGesture()

↓

大家一起

十二、总结

SwiftUI 手势层级其实只有三个核心知识点。

① 默认情况下,子 View 优先于父 View。


② GestureMask 决定当前 Gesture 的监听范围。

GestureMask 作用
.gesture 只监听当前 View
.subviews 只监听子 View
.all 当前 View + 子 View
.none 不监听任何区域

③ 多个 Gesture 冲突时:

  • gesture():正常竞争
  • highPriorityGesture():提高优先级
  • simultaneousGesture():同时识别

掌握了这三个知识点,就能够理解绝大多数 SwiftUI 手势冲突问题,例如:

  • ScrollViewDragGesture
  • 图片缩放(MagnificationGesture)
  • 地图拖拽(Map)
  • NavigationStack 的返回手势
  • 自定义侧滑菜单

这些看似复杂的场景,本质上都是 Gesture Hierarchy 的应用。

相关推荐
飘尘1 小时前
前端转型全栈(Java后端)的快速上手指引
前端·后端·全栈
一颗烂土豆2 小时前
Meshopt 压缩深度解析,为什么它比 Draco 更快
前端·javascript·webgl
浏览器工程师3 小时前
AI Agent 接浏览器任务,先别让它一路点到底
前端·后端
雨季mo浅忆3 小时前
VSCode自动格式化三要素
前端
爱勇宝3 小时前
深扒 Anthropic 1680 位工程师简历:应届生几乎没机会,AI 公司最缺的不是博士
前端·后端·程序员
kyriewen4 小时前
同事每天催我 Code Review,我写了个脚本让 AI 替我 review PR——现在他反过来催 AI 了
前端·javascript·ai编程
user20585561518136 小时前
Windows 项目安装时报 `node-sass` 错误,如何快速处理
前端
LiaCode6 小时前
Redis 在生产项目的使用
前端·后端
LiaCode6 小时前
一天学完 redis 的爽翻版核心知识总结
前端·后端