SwiftUI 是由什么构成的
SwiftUI 建立在三项 Swift 语言特性之上:结果构建器、不透明返回类型,以及值类型的视图树。一旦底层基础变得清晰可见,那些让开发者感到意外的 SwiftUI 部分(AnyView、Group、ViewBuilder、@ViewBuilder 参数,以及令人头疼的 some View vs any View 错误)就不再神秘。
一个 SwiftUI 视图是符合单一协议且只有单一要求的值类型。框架的其余部分都建立在 SwiftUI 之外的 Swift 语言特性之上:结果构建器、不透明类型、带约束的泛型、属性包装器。如果您理解这些语言特性,框架读起来就像普通的 Swift API。如果您不理解,框架读起来就像偶尔会反咬一口的魔法。
这篇文章将逐步剖析底层基础。这里没有 LiveActivityManager,也没有 Get Bananas 的截图。重点是框架本身,而非某个项目;一旦框架变得清晰易读,整个系列中每篇已发布代码的文章都会更易理解。
TL;DR
- SwiftUI 视图是符合
View协议的 Swift 值类型。该协议只有一个要求:var body: some View { get }。其余一切都建立在 Swift 语言特性之上。 @ViewBuilder是一个结果构建器。每个View的 body 都是一个结果构建器。结果构建器通过编译器合成的调用,将以逗号分隔的表达式转换为单个返回值。some View是一种不透明返回类型。编译器知道具体类型;调用者不知道。不透明类型让视图 body 在编译期和运行时都很高效;AnyView则是当不透明性行不通时的类型擦除应急方案。Group、EmptyView、TupleView、_ConditionalContent是结果构建器合成的实现类型。它们有文档记录,但很少需要手动编写。
一切的起点:协议
View 协议只有一个要求:1
public protocol View {
associatedtype Body : View
@ViewBuilder var body: Self.Body { get }
}
理解 SwiftUI 其余部分时,该协议有两个要点至关重要。
关联类型 Body : View。 视图的 body 本身也是一个视图。这种递归正是框架可组合的根源。每个 View 都从其 body 返回另一个 View,如此类推,直到链条终止于框架的某个基本视图(如 Text、Color、Image、EmptyView),它们的 Body 是 Never。基本视图是树的叶子;您编写的视图则是树枝。
body 上的 @ViewBuilder 属性。 每个 body 都是一个结果构建器闭包。结果构建器是一项 Swift 语言特性,记录于 SE-0289(在 Swift 5.4 中正式确立为 @resultBuilder),它允许编译器通过合成方法调用,将带有一系列表达式的闭包转换为单个返回值。2 正是这种转换让 SwiftUI body 内部那种无逗号、语句形态的语法得以工作。
该协议的形态有两点不寻常之处。
首先,要求是计算属性,而非方法。当 SwiftUI 判定视图状态发生变化时,会在每次渲染过程中重新计算视图的 body。框架将 body 视为调用成本低廉的属性;在 body 内部进行长时间计算属于反模式,因为它们会在每次渲染时运行。
其次,Self.Body 是关联的,而非擦除的。视图的具体 body 类型在编译期就是其签名的一部分。Text("Hello") 的 body 类型是 Never;自定义视图的 body 类型则是 @ViewBuilder 为该 body 合成的任何类型。这种关联类型设计让编译器能够在没有运行时类型检查的情况下优化视图树。这也是当自定义视图返回条件内容时产生 some View 要求的原因。
结果构建器:无逗号 DSL
结果构建器是一项 Swift 语言特性,它通过插入编译器合成的方法调用,将闭包转换为单个返回值。@ViewBuilder 就是一个结果构建器。每个 SwiftUI 视图的 body 都是它的闭包。2
考虑以下视图:
struct ExampleView: View {
var body: some View {
Text("Title")
Text("Subtitle")
Image(systemName: "star")
}
}
该 body 包含三条语句,没有任何分隔符。在普通 Swift 中,这是一个编译错误:闭包只能返回一个值。结果构建器在编译之前重写闭包。经过 @ViewBuilder 展开之后,编译器实际看到的代码大致如下:
struct ExampleView: View {
var body: some View {
ViewBuilder.buildBlock(
Text("Title"),
Text("Subtitle"),
Image(systemName: "star")
)
}
}
ViewBuilder.buildBlock(_:_:_:) 是一个静态方法,接收三个视图,返回 TupleView<(Text, Text, Image)>。该 body 返回这个单一的元组视图值。早期的 SwiftUI 提供了一组固定的 buildBlock 重载,支持 1、2、3 直至 10 个子视图;当前的 SwiftUI 使用 Swift 的可变泛型支持(buildBlock<each Content>),因此包含 11 个或更多兄弟视图的 body 不再是特殊情况。
同样的模式也处理控制流。带有 if 语句的视图 body 如下所示:
struct ConditionalView: View {
let isActive: Bool
var body: some View {
if isActive {
Text("Active")
} else {
Text("Inactive")
}
}
}
@ViewBuilder 通过 buildEither(first:) / buildEither(second:) 调用对其进行重写,生成 _ConditionalContent<Text, Text>。即使在任何特定渲染中只有一个分支会运行,编译器在编译期也知道结果类型。
if let、switch、可选解包以及其他几种结构由结果构建器的各种 buildXxx 静态方法处理。3 重复内容是该语言特性通过 buildArray 支持但 @ViewBuilder 不支持的一个值得注意的情况:在 body 中使用原始的 for 循环会失败,提示 “closure containing control flow statement cannot be used with result builder ‘ViewBuilder’.” SwiftUI 形态的答案是 ForEach,它接收一个 RandomAccessCollection 和一个内容闭包,将迭代合成为单个值类型视图。这个 DSL 并非定制;它就是 Swift 的结果构建器,针对视图进行了配置。
some View 与不透明性问题
自定义视图的 body 通常返回 some View。该关键字是不透明返回类型,在 Swift 5.1 中加入。4
some View 表示:”我返回一个符合 View 的特定类型,但我不告诉您是哪一个。” 编译器在内部跟踪具体类型用于优化;视图的调用者只看到协议见证。这种模式让视图的 body 能够返回像 VStack<TupleView<(Text, Image, Spacer)>> 这样的复杂类型,而不需要您在源代码中写出该类型。
关于 some View,有两点会让 SwiftUI 新手感到困惑:
some View 是一个特定类型,即使您返回的是不同的东西。 表达式 if condition { Text("A") } else { Image("b") } 在 @ViewBuilder body 中是允许的,因为结果构建器将两个分支都包装在 _ConditionalContent 中,生成单一的具体类型。但表达式 if condition { return Text("A") } else { return Image("b") } 在结果构建器之外则是编译错误:两个分支返回不同的具体类型,而 some View 只允许一个。结果构建器正是让条件返回形态得以工作的关键;显式 return 会丢失结果构建器转换。
some View 与 any View 不同。 some View 是不透明的(一个特定类型,被隐藏);any View 是存在类型(一个可以容纳任何符合类型的盒子,带有运行时开销)。自由函数或属性可以合法地返回 any View。但 View 协议的 body 不行:该协议要求 associatedtype Body: View,而 any View 本身不符合 View,因此 var body: any View 无法满足该协议,编译器会建议使用 some View。实际规则是:视图 body 使用 some View,对于运行时变化的视图类型则使用 AnyView(类型擦除包装器)。错误信息 “function declares an opaque return type but the return statements in its body do not have matching underlying types” 几乎总是意味着您试图从 some View 函数返回不同的具体类型,您需要的要么是结果构建器分支,要么是 AnyView。
AnyView:应急方案
AnyView 是一个类型擦除的视图包装器。构造方式为 AnyView(myView)。该包装器可以容纳任何符合的视图,SwiftUI 在期望 View 的地方都接受它。5
应急方案的使用场景是:函数根据运行时数据返回不同的具体类型,且无法通过结果构建器分支表达:
func viewForKind(_ kind: Kind) -> AnyView {
switch kind {
case .text: return AnyView(Text("hello"))
case .image: return AnyView(Image("photo"))
case .custom: return AnyView(MyCustomView())
}
}
AnyView 的代价是底层类型不再是视图静态身份的一部分。Apple 的文档直接描述了这一后果:当 AnyView 内部包装的类型在多次渲染之间发生变化时,现有视图层级会被销毁,并在原位置创建新的层级,这意味着状态丢失、动画重启以及身份丢失。重新包装相同的具体类型不会触发这种销毁,但无论哪种方式,框架偏好的基于静态类型的差异比较都不再可用。
正确的规则是:条件视图(if、switch、for)优先使用 @ViewBuilder 结果构建器分支,对于变化的类型优先使用参数化视图,仅在两者都行不通时才使用 AnyView。返回 AnyView 的 func viewForKind 通常意味着您应该让 viewForKind 返回 some View,并把 switch 放在结果构建器闭包内部。
Group、EmptyView、TupleView:实现类型
结果构建器合成特定的具体视图类型。其中三种值得认识:6
Group 是透明容器。它最多接受十个视图作为内容,并将它们作为兄弟节点呈现给父布局。容器本身不添加视觉结构;内容的渲染方式与单独渲染时完全相同。其使用场景是在期望单一视图的上下文中包装多个视图(.if 修饰符、条件返回、生成”一个视图”的函数)。Group { Text("A"); Text("B") } 是一个包含两个视图的单一视图;它是结果构建器隐式所做之事的显式形式。
EmptyView 是一个不渲染任何内容的视图。当 if 没有 else 时,结果构建器将其用作条件假分支。从您自己的代码返回 EmptyView() 是一种在不改变函数返回类型的情况下选择不渲染的方式。
TupleView 是当 body 包含多个兄弟视图时,结果构建器生成的具体类型。本文开头返回三个兄弟视图的表达式,实际上返回的是 TupleView<(Text, Text, Image)>。您几乎从不直接编写 TupleView;您是在错误信息中读到它。
_ConditionalContent(带有前导下划线)是处理 if/else 分支的类型。该类型出现在 ViewBuilder 的公开接口中,但下划线名称表示”不要随意编写针对它的代码”;让结果构建器从 if/else 中合成它,而不是手动构造。
在您自己的函数上使用 @ViewBuilder
结果构建器不仅用于 body。任何函数或闭包参数都可以标注 @ViewBuilder,相同的 DSL 语法在其内部就变得合法。2
struct Card<Content: View>: View {
let content: Content
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
var body: some View {
VStack(alignment: .leading, spacing: 8) {
content
}
.padding()
.background(.regularMaterial)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
}
// Usage: callers get the result-builder DSL inside the closure.
Card {
Text("Title")
Text("Subtitle")
Image(systemName: "star")
}
这种模式正是 SwiftUI 自身的 VStack、HStack、ZStack、List、Form、Section、Group 和 NavigationStack 接受多个子视图的方式。这些类型每一个都接收一个 @ViewBuilder content: () -> Content 参数。识别这种模式意味着您可以编写自己的容器视图,提供与框架同样的人体工学,无需特殊编译器支持。
您之所以编写 init(@ViewBuilder content:) 而不是 init(content:),是因为参数上的属性才能在调用者传入的闭包 body 中激活结果构建器转换。没有该属性,Card { Text("A"); Text("B") } 就是编译错误,因为闭包包含两条语句而没有 @ViewBuilder 来转换它们。
状态、绑定与属性包装器层
以上内容都是关于视图树的形态。SwiftUI 的另一半是状态,而那一半建立在 Swift 属性包装器之上。7
与视图编写最相关的属性包装器:
@State 在单个视图内拥有一段值类型状态。读取该属性会读取底层存储;为其赋值会触发视图重新渲染。该包装器适用于简单的、视图本地的状态(开关的开/关、文本字段的草稿字符串)。
@Binding 是对另一个视图状态的双向引用。需要读写父视图状态的子视图接收一个 Binding<T> 参数。父视图通过 $state(@State 上的美元符号投影)构造该绑定。
@Observable(iOS 17+)是一个宏,替代了较早的 ObservableObject 一致性模式。应用于类时,该宏会生成 Observation 框架的跟踪代码,使得当类的属性在 body 内被读取并随后被修改时,能够触发视图重新渲染。视图侧对 @Observable 类的所有权从 @StateObject 转移到普通的 @State;需要双向句柄的下游视图使用 @Bindable 而非 @ObservedObject。
@Environment 从环境链中读取依赖注入的值。SwiftUI 提供内置的环境键(locale、配色方案、dismiss action);应用为特定领域的依赖注入添加自定义键。
属性包装器层让视图的 body 能够在状态变化时重新执行。SwiftUI 通过两种不同的机制跟踪 body 内部的读取:AttributeGraph(Apple 私有的依赖图,支撑 @State、@Binding 和 @Environment)用于较早的属性包装器路径,以及标准库的 Observation 框架(withObservationTracking,在 iOS 17+ 上公开)用于 @Observable 类型。8 当被跟踪的属性发生变化时,相应的 body 会被重新执行,差异比较机制计算出最小的视图树变更。
这两半(视图树层和状态层)是松耦合的。视图树是值类型的,重新计算很快。状态层对于 @Observable 是引用类型,对于 @State 是带存储指针的值类型,并跟踪读取。两者共同产生了框架的”将屏幕上应有的内容描述为状态的函数,框架来计算差异”模型。
现在您能在错误信息中识别什么
在底层基础变得可见之后,阅读 SwiftUI 编译错误:
“Function declares an opaque return type, but the return statements in its body do not have matching underlying types.” 在 some View 函数中存在两条返回不同具体类型的 return 语句。修复:使用 @ViewBuilder 让结果构建器将两者都包装在 _ConditionalContent 中,或将两个 return 都包装在 AnyView 中。
“The compiler is unable to type-check this expression in reasonable time.” 带有许多修饰符的长 body 链耗尽了类型检查器的资源。修复:将 body 拆分为更小的计算属性或子视图;每个返回 some View 的部分都简化了类型推断工作。
“Cannot convert value of type ‘TupleView<…>’ to expected type ‘some View’.” 期望单个视图的函数收到了没有 @ViewBuilder 的多语句 body 的结果。修复:为接受多语句内容的闭包参数添加 @ViewBuilder。
“Generic parameter ‘Content’ could not be inferred.” 自定义容器接收 @ViewBuilder content: () -> Content,而调用处提供了空闭包。修复:结果构建器需要至少一个表达式来推断 Content;如果调用处显式提供,空闭包会回退为 EmptyView()。
错误信息之所以不友好,是因为底层基础不可见。在底层基础可见的情况下阅读它们,大多数错误都会变成”啊,结果构建器无法转换这个”或”啊,我需要分支或 AnyView“。
何时跨出底层基础
底层基础不能干净处理的几种模式:
可变的具体类型。 函数为每个分支返回不同的 View 类型,且无法用结果构建器分支包装时,需要 AnyView。接受其代价(差异比较丢失、无动画),并在调用处加以说明。
跨平台条件视图。 编译期的 #if os(iOS) 在 @ViewBuilder body 内部可以工作,但会限制结果构建器的分支数量;多平台条件 body 有时会触及”表达式过于复杂”的限制。修复方法是将各平台的子视图提取到独立函数中,每个函数都返回 some View。
命令式视图构造。 框架期望视图是表达式,而非先构造再修改的对象。UIKit 风格的”创建标签、设置文本、添加到子视图”无法迁移;SwiftUI 的等价物是从 body 返回值类型的 Text("...")。需要命令式构造的模式通常意味着该工作应该放在通往 UIKit 的 UIViewRepresentable 桥接中。
该模式对在 iOS 26+ 上发布的应用意味着什么
三点要义。
-
SwiftUI 是 Swift,不是魔法。 结果构建器、不透明返回类型和属性包装器都在 Swift 语言参考中。将框架作为 Swift 代码而非特殊 DSL 来阅读,会让令人意外的部分变得可预测。
-
some View和AnyView解决不同的问题。 不透明返回类型是默认选择;类型擦除是应急方案。使用AnyView应该是罕见情况;使用some View加结果构建器分支应该是常见情况。 -
结果构建器就是整个 DSL。 在任何函数或参数为
@ViewBuilder的地方,无逗号、语句形态的语法都可用。编写自己的、与VStack同样人体工学的容器视图,只需要一个属性和一个闭包参数。
可将本文与该系列的已发布代码文章配套阅读:跨平台 SwiftUI 发布(Return 在五个平台上以同一个共享的 SwiftUI 核心运行);Liquid Glass 视觉层;iOS 上的 Live Activities 状态机;Apple Watch 上的 watchOS 运行时 契约。系列中心位于 Apple Ecosystem Series。如需更广泛的 iOS 与 AI 智能体上下文,请参阅 iOS Agent Development guide。
FAQ
SwiftUI 中的 View 协议是什么?
View 协议只有一个要求:var body: some View { get }。每个 SwiftUI 视图都是符合 View 的 Swift 值类型,带有一个返回另一个视图的 body 计算属性(对于像 Text、Color、Image、EmptyView 这样的基本视图,则返回 Never)。该 body 标注了 @ViewBuilder,因此可以使用 SwiftUI 的无逗号 DSL 语法。
some View 是什么意思?
some View 是不透明返回类型(Swift 5.1+)。编译器知道具体类型;调用者只看到协议见证。不透明类型让视图 body 能够返回像 VStack<TupleView<(Text, Image, Spacer)>> 这样的复杂类型而无需将其拼写出来,同时保留编译期优化。some View 是一个特定类型,即使该类型在调用处不可见。
何时应使用 AnyView?
仅当 @ViewBuilder 结果构建器分支(if、switch、for)或参数化泛型都无法解决问题时才使用 AnyView。当被包装的具体类型在多次渲染之间发生变化时,现有视图层级会被销毁,并在原位置创建新的层级;那一刻动画会重启,视图状态会重置。重新包装相同的具体类型不会触发这种销毁,但无论哪种方式,框架偏好的基于静态类型的差异比较都不再可用。如果您发现自己经常使用 AnyView,需要改变的模式在更上游:优先使用参数化视图,或将条件推入结果构建器 body。
@ViewBuilder 是什么?我可以在哪里使用它?
@ViewBuilder 是一个结果构建器(Swift 语言特性)。它通过插入编译器合成的 buildBlock、buildEither、buildOptional 等调用,将带有多个表达式的闭包转换为单个返回值。每个 SwiftUI 视图的 body 默认就是 @ViewBuilder。您可以将 @ViewBuilder 应用于任何函数或闭包参数,让调用者获得相同的 DSL 语法;VStack、Card 和 Section 都使用相同的模式来接受多个子视图。
为什么我的视图 body 在我没预料到的时候重新渲染?
每当 body 读取的任何状态属性发生变化时,SwiftUI 都会重新运行 body。属性包装器(@State、@Binding、@Observable、@Environment)跟踪读取并在写入时触发重新渲染。意外的重新渲染通常追溯到父视图的状态变化、环境值变化,或 @Observable 对象的被读取属性被修改。然后框架的差异比较计算出最小的树变更。
参考资料
-
Apple Developer,“View” 与 “Configuring views”。
View协议、Body关联类型,以及body上的@ViewBuilder属性。 ↩ -
Swift Evolution,“SE-0289: Result builders”。正式确立结果构建器的语言提案(在 5.1 中作为
_functionBuilder引入,在 5.4 中正式确立为@resultBuilder)。定义了buildBlock、buildEither、buildOptional、buildArray、buildExpression、buildFinalResult等。 ↩↩↩ -
Apple Developer,“ViewBuilder” 与 “ForEach”。SwiftUI 用于视图 body 的结果构建器类型(可变泛型
buildBlock、buildEither、可选解包)。ViewBuilder不暴露buildArray,因此ForEach是在集合上重复视图的迭代基本元素。 ↩ -
Swift Evolution,“SE-0244: Opaque result types”。在 Swift 5.1 中加入的不透明返回类型
some关键字。 ↩ -
Apple Developer,“Group”、“EmptyView” 与 “TupleView”。结果构建器合成的实现类型。 ↩
-
Apple Developer,“State and Data Flow”。属性包装器层:
@State、@Binding、@Observable、@Environment。SwiftUI 的观察系统以及 iOS 17+ 的@Observable宏。 ↩ -
Apple Developer,“Observation” 与 “Migrating from the Observable Object protocol to the Observable macro”。标准库 Observation 框架,包括
withObservationTracking(_:onChange:),以及从ObservableObject到@Observable的 iOS 17 迁移路径。 ↩