7-18.【高级特性】SwiftUI 中为什么很多 API 采用 some View 而不是 View?

1 阅读3分钟

在 SwiftUI 中使用 some View 而不是直接使用协议名 View(或者 any View),主要涉及三个核心维度的考量:类型系统的完整性性能优化的极致要求以及视图身份(Identity)的稳定性


1. 解决“泛型爆炸”:隐藏恐怖的真实类型

SwiftUI 的视图系统是基于高度嵌套的泛型构建的。当你写一个简单的布局时,其实际的类型名称会变得极其复杂。

Swift

var body: some View {
    VStack {
        Text("Hello")
        Button("Click") { }
    }
    .padding()
}

如果你不使用 some View,而是试图写出它的真实类型,你可能需要写成这样:

ModifiedContent<VStack<TupleView<(Text, Button<Text>)>>, _PaddingLayout>

  • 开发者体验:如果 API 要求写出具体类型,代码将无法维护。
  • some View 的作用:它像一个“类型遮罩”。它告诉编译器:“这里确实有一个具体的、极其复杂的类型,但我不希望在代码中手动声明它,请你帮我推断并锁定它。”

2. 核心:静态分发与性能优化

View 协议定义了一个关联类型 associatedtype Body : View。这使得 View 成为一个“带有协议约束的泛型”。

  • 为什么不用 View(协议本身)?

    在 Swift 5.7 之前,你甚至不能直接返回一个含有关联类型的协议。即使现在可以(作为 any View),它也会引入动态分发(Dynamic Dispatch)

  • 为什么用 some View

    some View 保证了静态分发。编译器在编译期就知道 body 返回的到底是什么。这意味着:

    1. 内联优化:编译器可以直接把视图的构建代码插入调用处,消除函数调用开销。
    2. 无需内存装箱:不需要 any View 那种 40 字节的 Existential Container,直接按需分配内存。

3. 视图身份 (Identity) 与 Diffing 算法

这是 SwiftUI 渲染引擎的“灵魂”。SwiftUI 之所以快,是因为它只更新发生变化的部分(增量更新)。

  • 稳定的类型信息

    当一个函数的返回值是 some View 时,编译器保证了它的类型在运行期间是静态稳定的。

  • Diffing 效率

    SwiftUI 的底层引擎会对比新旧两棵视图树。如果类型是 some View,引擎看到的是 VStack 变成了 VStack,它知道结构没变,只需要对比属性。

  • 如果使用 any View

    由于 any View 抹除了类型信息,渲染引擎在对比时会非常吃力,甚至可能认为整个视图树已经改变,从而销毁旧视图并重新创建(重绘),导致动画失效和性能剧降。


4. 总结对比

维度some View (当前选择)any View / 协议名
类型解析编译期确定(不透明)运行期解析(动态)
分发方式静态分发(极快)动态分发(较慢)
视图树稳定性(Identity 稳定,利于 Diff)(Identity 模糊,可能导致重绘)
递归嵌套支持(View 协议要求的递归结构)在早期 Swift 版本中难以实现

简单来说

SwiftUI 采用 some View 是为了达成一个平衡:既要拥有像动态语言一样的简洁代码(不用写复杂的泛型),又要拥有像底层语言一样的静态执行性能和类型安全。

它是 SwiftUI 能够实现“声明式 UI”且保持高性能的底层基石。

既然 some View 要求返回类型必须唯一,那你是否好奇 SwiftUI 是如何处理那些包含 if-else 分支、看起来会返回不同视图类型的代码块的?