
引子
WWDC26 里,SwiftUI 新加入了一个看起来很低调、但其实很有意思的东西:@ContentBuilder。
第一次看到它,你可能会想:
又来一个 Builder?
@ViewBuilder还没彻底摸透,怎么又冒出来一个@ContentBuilder? SwiftUI 是不是觉得开发者头发还太多?
别急。@ContentBuilder 并不是一个全新的魔法系统,也不是 @ViewBuilder Pro Max Ultra。苹果文档里目前对它的定义非常朴素:
swift
typealias ContentBuilder = ViewBuilder
也就是说,从表面实现看,它就是 ViewBuilder 的别名。

但如果只看到这一层,就会误会它的意义。@ContentBuilder 的重点不在于"今天它是不是一个全新的类型",而在于:苹果正在重新整理 SwiftUI 中那些五花八门的 Builder,让它们从一堆专用入口,逐渐走向统一。
一句话概括:
@ViewBuilder关心的是"构建 View";@ContentBuilder关心的是"构建 SwiftUI 内容"。
这两个词看似只差一点点,但背后是 SwiftUI API 设计方向的一次收口。
闲话少说,让我们马上开始 @ContentBuilder 的奇妙冒险吧!;)

一、先说结论:@ContentBuilder 到底解决什么问题?
@ContentBuilder 的出现,主要解决两个问题。
第一个问题,是复杂 SwiftUI 代码的类型检查性能。
第二个问题,是SwiftUI Builder 家族过于分裂。
这两个问题对普通开发者来说,一个体感明显,一个体感不明显,但都挺重要。
1. 编译器:别让我猜了,我真的会累
SwiftUI 开发者大多见过这个错误:
swift
The compiler is unable to type-check this expression in reasonable time
翻译成人话就是:
你这段 SwiftUI 写得太绕了,我先躺一下。
为什么 SwiftUI 容易触发这种问题?因为 SwiftUI 的声明式写法看起来像是在搭积木,但编译器看到的不是积木,而是一大坨嵌套泛型、闭包、重载和 result builder。
比如你写:
swift
List {
Section("贴纸包") {
Group {
ForEach(packs) { pack in
NavigationLink {
StickerPackDetailView(pack: pack)
} label: {
Text(pack.name)
}
}
}
}
}
人类看这段代码:
一个列表,里面有个 Section,Section 里有一组贴纸包。
编译器看这段代码:
这个 Section 里的闭包到底返回 View? 还是 TableRowContent? Group 是 View 的 Group,还是别的内容类型? ForEach 生成的是一组 View?还是某种 row content? NavigationLink 又落在哪条重载路径上?
你以为它在编译,其实它在大型相亲节目里疯狂配对。
SwiftUI 过去有很多专用 Builder,比如:
swift
@ViewBuilder
@ToolbarContentBuilder
@CommandsBuilder
@TabContentBuilder
这些名字本身没错,但它们会让 API 表面越来越碎,也会让编译器在复杂嵌套结构里尝试更多路径。路径一多,类型检查就容易爆炸。
@ContentBuilder 的目标,就是把这些"我到底在构建哪种内容"的判断,统一到更通用的 SwiftUI 内容构建模型中。
换句话说,它不是让 SwiftUI 少写代码,而是让编译器少猜谜。

二、@ContentBuilder 和 @ViewBuilder 有什么不同?
最容易记住的区别是:
@ViewBuilder:我构建的是 View。@ContentBuilder:我构建的是 SwiftUI content,至于具体是什么,由上下文决定。
可以用一个表看清楚:
| 对比项 | @ViewBuilder |
@ContentBuilder |
|---|---|---|
| 名字含义 | 构建 View | 构建 SwiftUI 内容 |
| 语义重点 | 偏向视图层级 | 更通用,覆盖 View、ToolbarContent、Commands 等内容 |
| 当前实现 | SwiftUI 原有 result builder | 文档显示为 ViewBuilder 的 typealias |
| 主要价值 | 声明式组合 View | 统一 Builder API,减少类型推断分支 |
| 是否必须替换旧代码 | 不需要 | 新代码可以优先考虑 |
| 适合场景 | 明确只返回 View 的闭包 | 更通用的 SwiftUI 内容 API |
这里有个很重要的点:
@ContentBuilder 不是用来淘汰 @ViewBuilder 的。
如果你写的是一个明确只接收 View 的容器,@ViewBuilder 仍然完全合理。比如:
swift
struct Card<Content: View>: View {
let content: Content
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
var body: some View {
content
}
}
这段代码没有问题,不需要你连夜改成 @ContentBuilder。
但如果你在写一个更通用的 SwiftUI API,尤其这个 API 不一定只面对 View,而可能面对 toolbar、commands、tabs 这类内容,那么 @ContentBuilder 的语义就更准确。

三、例子一:普通 View 容器,@ContentBuilder 可以直接替代 @ViewBuilder
先看最常见的自定义容器。
过去我们通常这么写:
swift
struct Card<Content: View>: View {
let title: String
let content: Content
init(
_ title: String,
@ViewBuilder content: () -> Content
) {
self.title = title
self.content = content()
}
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text(title)
.font(.headline)
content
}
.padding()
.background(.thinMaterial)
.clipShape(RoundedRectangle(cornerRadius: 16))
}
}
调用方式:
swift
Card("今日任务") {
Label("写 SwiftUI 文章", systemImage: "pencil")
Label("修复 List 拖拽排序", systemImage: "arrow.up.arrow.down")
Label("喝咖啡续命", systemImage: "cup.and.saucer")
}
在 WWDC26 之后,你可以写成:
swift
struct Card<Content: View>: View {
let title: String
let content: Content
init(
_ title: String,
@ContentBuilder content: () -> Content
) {
self.title = title
self.content = content()
}
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text(title)
.font(.headline)
content
}
.padding()
.background(.thinMaterial)
.clipShape(RoundedRectangle(cornerRadius: 16))
}
}
这两个版本在这个例子里几乎没有区别,因为 Content 仍然被约束为 View。
那为什么还要写 @ContentBuilder?
因为它表达的是新语义:
这个闭包不是"专门构建某个 ViewBuilder 的东西",而是在构建 SwiftUI 内容。 只是这里的上下文要求它最终是一个 View。
有点像你去饭店,以前窗口上写着"米饭窗口""面条窗口""饺子窗口";现在统一写"取餐窗口"。你拿到的是米饭还是饺子,取决于你的订单,不取决于窗口名字。

四、例子二:复杂嵌套结构里,真正受益的是编译器
下面这段代码,你一定不会陌生:
swift
struct StickerListView: View {
let packs: [StickerPack]
var body: some View {
List {
Section("贴纸包") {
Group {
ForEach(packs) { pack in
NavigationLink {
StickerPackDetailView(pack: pack)
} label: {
HStack {
Image(pack.coverImageName)
.resizable()
.frame(width: 44, height: 44)
VStack(alignment: .leading) {
Text(pack.name)
Text("\(pack.stickers.count) 个贴纸")
.foregroundStyle(.secondary)
}
}
}
}
}
}
}
}
}
这段代码对开发者来说很自然:
List里有SectionSection里有GroupGroup里有ForEachForEach里有NavigationLinkNavigationLink里有 label 和 destination
写起来像搭乐高。
但对编译器来说,这更像一场"无限岔路口大逃杀"。
因为 SwiftUI 里很多类型都有多个重载版本。Section 可以用于普通 View,也可以用于列表行内容;Group 也可能出现在不同的内容上下文里;ForEach 更是常客。每一层都要尝试不同路径,嵌套越深,推断成本越高。
这就是为什么有时候你只是加了一个 if、一个 Group,或者多套了一层 ForEach,Xcode 就突然开始装死。
@ContentBuilder 的方向,是让这些常见 builder 共享更统一的内容构建入口。编译器不需要在一堆专用 builder 之间来回试探,而是先按更通用的 content builder 组合内容,再由上下文决定最终内容类型。
这不是那种"今天改一行代码,明天 App 起飞"的特性。它更像 SwiftUI 底层管道重新铺了一段更直的路。
平时你可能感觉不到它,但当项目里有大量复杂 SwiftUI 视图时,它就能减少一些编译器的内耗。

五、例子三:ToolbarContent 场景,@ContentBuilder 的意义开始明显
如果只是构建 View,@ContentBuilder 看起来像 @ViewBuilder 的换皮。
但到了 toolbar 场景,它的语义就出来了。
以前如果我们包装一个 toolbar 内容,可能会这么写:
swift
struct EditorToolbar<Content: ToolbarContent>: ToolbarContent {
private let content: Content
init(@ToolbarContentBuilder content: () -> Content) {
self.content = content()
}
var body: some ToolbarContent {
content
}
}
现在可以改成:
swift
struct EditorToolbar<Content: ToolbarContent>: ToolbarContent {
private let content: Content
init(@ContentBuilder content: () -> Content) {
self.content = content()
}
var body: some ToolbarContent {
content
}
}
使用方式:
swift
struct EditorView: View {
var body: some View {
TextEditor(text: .constant("Hello"))
.toolbar {
EditorToolbar {
ToolbarItemGroup {
Button("Undo") {
// undo
}
Button("Redo") {
// redo
}
}
ToolbarItem(placement: .topBarTrailing) {
Button("Share") {
// share
}
}
}
}
}
}
这里最关键的点是:
swift
Content: ToolbarContent
也就是说,这个闭包构建的并不是普通 View,而是 ToolbarContent。
如果这里叫 @ViewBuilder,语义上就别扭了:你明明不是在构建 View,却挂了个 ViewBuilder 的牌子。就像一个卖煎饼的摊位,招牌写着"高端量子计算服务中心",不是不能卖,但总感觉哪里不对。
@ContentBuilder 在这种地方就更自然:
我构建的是 SwiftUI content。 当前上下文要求它是 ToolbarContent。 那它就应该产出 ToolbarContent。

六、例子四:Commands 场景,Content 真的不等于 View
macOS App 里常见的 Commands 更能说明问题。
Commands 不是 View。它描述的是菜单命令。
过去你可能会写:
swift
struct AppCommandGroup<Content: Commands>: Commands {
private let content: Content
init(@CommandsBuilder content: () -> Content) {
self.content = content()
}
var body: some Commands {
content
}
}
现在可以写成:
swift
struct AppCommandGroup<Content: Commands>: Commands {
private let content: Content
init(@ContentBuilder content: () -> Content) {
self.content = content()
}
var body: some Commands {
content
}
}
使用:
swift
@main
struct StickerApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.commands {
AppCommandGroup {
CommandMenu("Stickers") {
Button("Import Sticker Pack") {
// import
}
Button("Export Current Page") {
// export
}
}
CommandGroup(after: .newItem) {
Button("New From Photo") {
// new document from photo
}
}
}
}
}
}
这个例子里,如果继续叫 @CommandsBuilder,当然也能理解。但从 SwiftUI 的整体方向看,苹果显然更希望开发者把这些内容看成同一套声明式内容系统:
- View 是内容
- ToolbarContent 是内容
- Commands 是内容
- TabContent 也是内容
它们不都是 View,但它们都属于 SwiftUI 的 declarative content。
这就是 @ContentBuilder 名字背后的真正含义。

七、例子五:拆分辅助视图时,也可以使用 @ContentBuilder
SwiftUI 代码写长之后,我们经常会把某些视图拆成属性或方法。
以前常见写法是:
swift
@ViewBuilder
private var stickerGrid: some View {
if stickers.isEmpty {
ContentUnavailableView(
"还没有贴纸",
systemImage: "sparkles",
description: Text("先导入一个贴纸包吧。")
)
} else {
LazyVGrid(columns: [.init(.adaptive(minimum: 80))]) {
ForEach(stickers) { sticker in
Image(sticker.imageName)
.resizable()
.scaledToFit()
}
}
.padding()
}
}
现在也可以写成:
swift
@ContentBuilder
private var stickerGrid: some View {
if stickers.isEmpty {
ContentUnavailableView(
"还没有贴纸",
systemImage: "sparkles",
description: Text("先导入一个贴纸包吧。")
)
} else {
LazyVGrid(columns: [.init(.adaptive(minimum: 80))]) {
ForEach(stickers) { sticker in
Image(sticker.imageName)
.resizable()
.scaledToFit()
}
}
.padding()
}
}
完整一点:
swift
struct StickerLibraryView: View {
let stickers: [Sticker]
var body: some View {
ScrollView {
stickerGrid
}
}
@ContentBuilder
private var stickerGrid: some View {
if stickers.isEmpty {
ContentUnavailableView(
"还没有贴纸",
systemImage: "sparkles",
description: Text("先导入一个贴纸包吧。")
)
} else {
LazyVGrid(columns: [.init(.adaptive(minimum: 80))]) {
ForEach(stickers) { sticker in
Image(sticker.imageName)
.resizable()
.scaledToFit()
}
}
.padding()
}
}
}
这里它和 @ViewBuilder 依然很像,因为返回值还是 some View。
所以这种场景不必纠结:
- 你继续用
@ViewBuilder,没问题。 - 你用
@ContentBuilder,也符合新方向。 - 团队代码风格统一,比"为了新而新"更重要。

八、@ContentBuilder 不是万能胶
看到这里,容易产生一个误解:
既然叫 ContentBuilder,那是不是所有 SwiftUI 内容都能随便混着写?
不能。
@ContentBuilder 不会破坏 Swift 的类型系统。它不是"类型安全破解器",也不是 SwiftUI 的免死金牌。
比如下面这段代码依然不合理:
swift
struct BadView: View {
var body: some View {
VStack {
ToolbarItem {
Button("Save") {}
}
}
}
}
为什么?
因为 VStack 需要的是 View 内容,而 ToolbarItem 是 ToolbarContent。你不能因为二者都叫 SwiftUI content,就把它们硬塞进同一个地方。
这就像"饭"和"饮料"都属于餐饮,但你不能把奶茶倒进电饭锅里,然后说它们都是食物。
@ContentBuilder 做的是统一构建入口,不是取消上下文约束。
真正决定闭包里能写什么的,仍然是外部 API 对返回类型的要求。

九、要不要把旧代码全部改成 @ContentBuilder?
不用。
真的不用。
如果你的代码已经这样写:
swift
struct Container<Content: View>: View {
let content: Content
init(@ViewBuilder content: () -> Content) {
self.content = content()
}
var body: some View {
content
}
}
它没有错,也不落后。
@ContentBuilder 更适合这些场景:
- 你正在写新的 SwiftUI 自定义容器。
- 你希望 API 名字更通用,不想把所有内容都叫 View。
- 你的泛型内容可能是
ToolbarContent、Commands、TabContent等。 - 你希望代码风格更贴近 Xcode 27 之后 SwiftUI 的方向。
一个比较稳妥的迁移策略是:
swift
// 明确只构建 View,旧代码可以继续保留
@ViewBuilder
private var bodyContent: some View {
...
}
// 新写的通用 SwiftUI 内容 API,可以优先使用
@ContentBuilder
private var content: some View {
...
}
不要为了追新,把一个稳定项目里的所有 @ViewBuilder 全局替换成 @ContentBuilder。那不是现代化,那是给自己安排加班。

十、最实用的判断标准
你可以用下面这套标准来决定该用谁。
用 @ViewBuilder 的情况
如果你的 API 明确只接收或返回 View,用 @ViewBuilder 完全可以:
swift
struct MyContainer<Content: View>: View {
init(@ViewBuilder content: () -> Content) {
...
}
}
这类场景里,@ViewBuilder 语义清楚,历史包袱小,团队也熟悉。
用 @ContentBuilder 的情况
如果你写的是更通用的 SwiftUI 内容构建 API,用 @ContentBuilder 更合适:
swift
struct MyToolbar<Content: ToolbarContent>: ToolbarContent {
init(@ContentBuilder content: () -> Content) {
...
}
}
或者:
swift
struct MyCommands<Content: Commands>: Commands {
init(@ContentBuilder content: () -> Content) {
...
}
}
这时候 ContentBuilder 的名字更准确,因为你构建的不一定是 View。

十一、总结:SwiftUI Builder 家族终于开始"归队"了
@ContentBuilder 看起来只是一个小改动,甚至文档里目前还是:
swift
typealias ContentBuilder = ViewBuilder
但它释放的信号很明确:
SwiftUI 不想继续让每种内容都有一套独立 Builder 名字。 苹果正在把这些声明式内容统一到更通用的 Content 概念之下。
它带来的价值主要有三点:
-
统一 SwiftUI API 设计语言 不再让 View、Toolbar、Commands、Tabs 各自为政。
-
减轻复杂嵌套结构下的类型检查压力 让编译器少走一些岔路,少一点"reasonable time"式崩溃。
-
让自定义 SwiftUI API 的命名更准确 不是所有 SwiftUI 内容都是 View,但它们都可以是 Content。
最后可以记住这句话:
@ViewBuilder是"构建视图"。@ContentBuilder是"构建 SwiftUI 内容"。 当内容刚好是 View 时,它们看起来很像; 当内容变成 toolbar、commands、tabs 时,@ContentBuilder的意义就真正出来了。
所以,@ContentBuilder 不是一把新锤子,而是苹果给 SwiftUI Builder 家族换的一块新门牌。
以前门口写着:
ViewBuilder、ToolbarContentBuilder、CommandsBuilder、TabContentBuilder......
现在苹果想慢慢改成:
ContentBuilder。
简单点,挺好。毕竟 SwiftUI 已经够抽象了,能少一个名字,就少一个开发者半夜怀疑人生的理由。
最后,感谢宝子们的观赏,我们下次再会!8-)