SwiftUI 广泛使用了 Swift 中的一个强大功能——不透明返回类型(opaque return types)。当你写下 some View
时,这个功能就开始发挥作用了,它的意思是:“一个符合 View 协议的对象,但我们不想明确指出它是什么类型。”
返回 some View
意味着,即使我们不知道具体返回的视图类型,编译器却知道。这看似细微,但却有着重要的影响。
使用 some View
对性能至关重要。
SwiftUI 需要能够检查我们显示的视图并了解它们是如何变化的,以便正确更新用户界面。如果没有这种额外信息,SwiftUI 就需要花费大量时间去弄清楚到底发生了什么变化——每次小改动都可能导致整个视图重新渲染。
SwiftUI 的视图是通过堆叠 ModifiedContent
类型来构建的。让我们回顾一下之前的代码:
Button("Hello World") {
print(type(of: self.body))
}
.frame(width: 200, height: 200)
.background(.red)
这段代码定义了一个简单的按钮,并打印其确切的 Swift 类型。输出的类型中会包含几层 ModifiedContent
,比如:
ModifiedContent<ModifiedContent<Button<Text>, _FrameLayout>, _BackgroundStyleModifier<Color>>
Swift 的 View
协议带有一个关联类型(associated type),这意味着 View
本身并不是一个具体的类型——我们需要明确指定视图的具体类型。这与 Swift 数组的工作方式类似:Swift 不允许我们仅仅声明 “这是一个数组”,而是要求我们说明数组中存放的具体类型,比如 Array<String>
。
因此,这种写法是不合法的:
struct ContentView: View {
var body: View {
Text("Hello, world!")
}
}
因为 View
是一个“未填充的洞”,编译器需要知道具体的视图类型。然而,这种写法是合法的:
struct ContentView: View {
var body: Text {
Text("Hello, world!")
}
}
在这里,Text
填充了 View
的“洞”,让 Swift 知道了视图的具体类型。
some View
回到刚才的代码:
Button("Hello World") {
print(type(of: self.body))
}
.frame(width: 200, height: 200)
.background(.red)
如果我们希望返回这种视图,应该如何声明类型?
虽然可以尝试手动写出 ModifiedContent
结构的确切组合,但这极为繁琐且毫无意义,因为这些是 SwiftUI 的内部实现,开发者并不关心。
some View
允许我们这样写:“这将是一个视图,比如 Button 或 Text,但我不想明确指定具体类型。”因此,View
中的“洞”会由一个具体的视图对象填充,但我们无需手动写出确切的类型。
(1) VStack 的内容类型
如果我们在 VStack
中放入两个文本视图,SwiftUI 会默默地创建一个 TupleView
,用来包含这两个视图——这是一个特殊的视图类型,可以容纳正好两个视图。
当 VStack
中有三个文本视图时,它会变成包含三个视图的 TupleView
。如果有四个视图、八个视图,甚至十个视图,TupleView
就会继续扩展,以容纳所有视图。
(2) 多个视图直接返回
如果我们在 body
属性中直接返回多个视图,没有用堆叠视图(如 VStack
)包裹它们,Swift 会自动应用一个名为 @ViewBuilder
的特殊属性。这会将多个视图隐式地包装成一个 TupleView
容器,因此看似我们返回了多个视图,实际上它们被组合成了一个。
如果你右键点击 View
协议并选择 “Jump to Definition”,可以看到 body
属性的定义中带有 @ViewBuilder
属性:
@ViewBuilder @MainActor var body: Self.Body { get }
这个行为不是魔法,而是 SwiftUI 通过类型系统和编译器特性实现的。虽然 SwiftUI 并未在文档中明确定义这种行为,但这种灵活性对开发者来说非常有用。
总结来说,some View
是 SwiftUI 的核心工具,它让我们可以灵活返回符合 View
协议的视图,同时隐藏具体实现细节。这既提升了性能,也简化了开发者的代码书写体验。