SwiftUI 的声明式语法之所以优雅,一大功臣是隐藏在幕后的 ViewBuilder
。它让我们可以在 body
或 HStack
、VStack
等容器的闭包里随意组合多个视图,而无需手动把它们包进 Group
或 TupleView
ViewBuilder 是什么?
ViewBuilder
是一个 结果构建器(Result Builder),负责把 DSL(领域特定语言)中的多条表达式"构建"成单个视图。它最常出现的场景:
swift
VStack {
Image(systemName: "star")
Text("Hello, world!")
}
我们并没有显式写 ViewBuilder.buildBlock(...)
,却能在 VStack
的尾随闭包里放两个视图,这就是 @ViewBuilder
的魔力。
实际上,View
协议已经把 body
标记成了 @ViewBuilder
:
swift
@ViewBuilder var body: Self.Body { get }
所以下面这样写也完全合法:
swift
var body: some View {
if user != nil {
HomeView(user: user!)
} else {
LoginView(user: $user)
}
}
即使 if
的两个分支返回不同类型,ViewBuilder
也能通过 buildEither
等内部方法把它们擦除为 AnyView
或 _ConditionalContent
,最终呈现出单一根视图。
给自己的 API 加上 @ViewBuilder:自定义容器
想让自定义容器也支持 DSL 语法?只需在属性或闭包参数前加 @ViewBuilder
。
基本用法:把属性变成视图构建闭包
swift
struct Container<Header: View, Content: View>: View {
@ViewBuilder var header: Header
@ViewBuilder var content: Content
var body: some View {
VStack(spacing: 0) {
header
.frame(maxWidth: .infinity)
.padding()
.foregroundStyle(.white)
.background(.blue)
ScrollView { content.padding() }
}
}
}
调用方式立即变得"SwiftUI 味儿":
swift
Container(header: {
Text("Welcome")
}, content: {
if let user {
HomeView(user: user)
} else {
LoginView(user: $user)
}
})
让 header 可选:两种做法
做法 A:带约束的扩展
swift
extension Container where Header == EmptyView {
init(@ViewBuilder content: () -> Content) {
self.init(header: EmptyView.init, content: content)
}
}
现在可以这样写:
swift
Container {
LoginView(user: $user)
}
做法 B:默认参数 + 手动调用闭包
swift
struct Container<Header: View, Content: View>: View {
private let header: Header
private let content: Content
init(@ViewBuilder header: () -> Header = EmptyView.init,
@ViewBuilder content: () -> Content) {
self.header = header()
self.content = content()
}
var body: some View { ... }
}
优点:
- 不需要额外扩展;
- 可以在未来继续添加默认参数;
- 闭包在
init
就被执行,避免body
反复求值带来的性能损耗。
多条表达式与隐式 Group
当闭包里出现多条顶层表达式时,ViewBuilder
会把它们当成 Group
的子视图。例如:
swift
Container(header: {
Text("Welcome")
NavigationLink("Info") { InfoView() }
}, content: { ... })
实际上得到的是两个独立的 header 视图,各自撑满宽度,而不是一个整体。解决方式:
- 在容器内部用显式
VStack
再包一层:
swift
VStack(spacing: 0) {
VStack { header } // 👈 统一布局
.frame(maxWidth: .infinity)
.padding()
.background(.blue)
ScrollView {
VStack { content }.padding()
}
}
- 或者在调用方显式组合:
swift
private extension RootView {
func header() -> some View {
VStack(spacing: 20) {
Text("Welcome")
NavigationLink("Info") {
InfoView()
}
}
}
}
小建议:
如果函数/计算属性返回"一个整体"视图,最好显式用 VStack
、HStack
等包装,而不是依赖 @ViewBuilder
隐式 Group。语义更清晰,布局也更稳定。
把 ViewBuilder 当"代码组织工具"
当 body
越来越复杂时,可以把子区域拆成私有的 @ViewBuilder
方法:
swift
struct RootView: View {
@State private var user: User?
var body: some View {
Container(header: header, content: content)
}
}
private extension RootView {
@ViewBuilder
func content() -> some View {
if let user {
HomeView(user: user)
} else {
LoginView(user: $user)
}
}
}
注意:如果 header()
需要返回多个兄弟视图,则推荐返回显式容器,而不是 @ViewBuilder
:
swift
func header() -> some View {
VStack(spacing: 20) { ... }
}
要点回顾
场景 技巧 让属性支持 DSL 在属性前加 @ViewBuilder
让参数支持 DSL 在闭包参数前加 @ViewBuilder
,并在 init
内手动执行 可选组件 使用 EmptyView.init
作为默认值或约束扩展 多条表达式 记住隐式 Group 行为,必要时显式包一层容器 代码组织 用 @ViewBuilder
拆分 body
,但根视图最好显式容器
结语
ViewBuilder
把 SwiftUI 的声明式语法推向了"像写普通 Swift 代码一样自然"的高度。当我们为自定义容器、可复用组件也加上 @ViewBuilder
时,API 就能与系统控件保持一致的体验,既易读又易维护。
下次写 SwiftUI 时,不妨问问自己:"这段代码能不能也让调用者用 ViewBuilder 的语法糖?" 如果答案是肯定的,就把 @ViewBuilder
加上去吧!