SwiftUI 手势层级(Gesture Hierarchy)详解

21 阅读1分钟

彻底理解 GestureMask、highPriorityGesture 和 simultaneousGesture

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

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

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

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


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

假设有这样一个界面:

VStack {
    Rectangle()
}

对应的视图结构:

VStack
└── Rectangle

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

VStack {
    Rectangle()
        .onTapGesture {
            print("Rectangle")
        }
}
.onTapGesture {
    print("VStack")
}

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

Rectangle?
还是 VStack?
还是两个一起?

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


二、默认规则:子 View 优先

运行下面代码:

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

界面如下:

+----------------------+
|                      |
|    Rectangle         |
|                      |
+----------------------+

点击 Rectangle:

👆

输出:

Rectangle

而不是:

VStack

为什么?

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

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

用户点击
    │
    ▼
Rectangle
    │
    ▼
VStack

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

如果点击黄色背景:

👆

输出:

VStack

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


三、GestureMask 到底是什么?

很多人第一次看到:

.gesture(
    TapGesture(),
    including: .gesture
)

都会疑惑:

这个 including 是什么意思?

其实:

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

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

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

SwiftUI 提供了四种 GestureMask:

.gesture
.subviews
.all
.none

下面分别介绍。


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

这是最容易误解的一个。

例如:

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 上的,因此:

VStack       ✅

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

⚠️ 注意:

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

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

只是:

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

因此:

点击 Rectangle:

Rectangle

点击黄色背景:

VStack

一句话总结:

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


五、.subviews —— 只监听子 View

修改代码:

VStack {

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

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

此时:

VStack      ❌

Rectangle   ✅

什么意思?

父 View 的 Gesture:

只监听子 View。

因此:

点击 Rectangle:

VStack

点击黄色背景:

没有任何输出。

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

其实就是:

监听孩子

不监听自己

一句话总结:

.subviews = 只监听子 View。


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

改成:

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

此时:

VStack      ✅

Rectangle   ✅

无论点击:

黄色背景

还是:

Rectangle

VStack 的 Gesture 都能够参与识别。

可以理解成:

整个 View 树
都是我的监听范围

一句话总结:

.all = 当前 View + 子 View。


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

.gesture(
    TapGesture(),
    including: .none
)

表示:

当前 Gesture 完全失效。

无论点击哪里:

都不会响应。

一句话总结:

.none = 禁用当前 Gesture。


八、一张图理解 GestureMask

假设:

VStack
│
├── Rectangle
└── Circle

.gesture

VStack      ✅

Rectangle   ❌

Circle      ❌

.subviews

VStack      ❌

Rectangle   ✅

Circle      ✅

.all

VStack      ✅

Rectangle   ✅

Circle      ✅

.none

VStack      ❌

Rectangle   ❌

Circle      ❌

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

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

注意:

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

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

它并不一定表示父 View。


九、highPriorityGesture()

默认情况下:

一个 View 可以拥有多个 Gesture。

例如:

Rectangle()
    .gesture(
        TapGesture()
            .onEnded {
                print("普通 Tap")
            }
    )
    .highPriorityGesture(
        TapGesture()
            .onEnded {
                print("高优先级 Tap")
            }
    )

点击:

输出:

高优先级 Tap

因为:

highPriorityGesture

        ↑

gesture

highPriorityGesture 的优先级更高。

一句话理解:

它会优先参与手势识别。


十、simultaneousGesture()

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

Rectangle()
    .gesture(
        TapGesture()
            .onEnded {
                print("普通")
            }
    )
    .simultaneousGesture(
        TapGesture()
            .onEnded {
                print("同时")
            }
    )

点击:

输出:

普通

同时

两个 Gesture 都会执行。

一句话理解:

允许多个 Gesture 同时识别。


十一、三种 Gesture 的区别

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

gesture()

↓

正常排队
highPriorityGesture()

↓

插队
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 的应用。