彻底理解 GestureMask、highPriorityGesture 和 simultaneousGesture
在学习 SwiftUI 手势时,很多开发者都会遇到这些问题:
- 为什么点击子 View,父 View 的
onTapGesture没有执行?GestureMask的.gesture、.subviews、.all到底是什么意思?highPriorityGesture和simultaneousGesture有什么区别?- 为什么
ScrollView和DragGesture经常发生冲突?这些问题都与 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 手势冲突问题,例如:
ScrollView与DragGesture- 图片缩放(MagnificationGesture)
- 地图拖拽(Map)
NavigationStack的返回手势- 自定义侧滑菜单
这些看似复杂的场景,本质上都是 Gesture Hierarchy 的应用。