WWDC26 新 @ContentBuilder 修饰符:SwiftUI 终于有了户口本

引子

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 里有 Section
  • Section 里有 Group
  • Group 里有 ForEach
  • ForEach 里有 NavigationLink
  • NavigationLink 里有 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 内容,而 ToolbarItemToolbarContent。你不能因为二者都叫 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 更适合这些场景:

  1. 你正在写新的 SwiftUI 自定义容器。
  2. 你希望 API 名字更通用,不想把所有内容都叫 View。
  3. 你的泛型内容可能是 ToolbarContentCommandsTabContent 等。
  4. 你希望代码风格更贴近 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 概念之下。

它带来的价值主要有三点:

  1. 统一 SwiftUI API 设计语言 不再让 View、Toolbar、Commands、Tabs 各自为政。

  2. 减轻复杂嵌套结构下的类型检查压力 让编译器少走一些岔路,少一点"reasonable time"式崩溃。

  3. 让自定义 SwiftUI API 的命名更准确 不是所有 SwiftUI 内容都是 View,但它们都可以是 Content。

最后可以记住这句话:

@ViewBuilder 是"构建视图"。 @ContentBuilder 是"构建 SwiftUI 内容"。 当内容刚好是 View 时,它们看起来很像; 当内容变成 toolbar、commands、tabs 时,@ContentBuilder 的意义就真正出来了。

所以,@ContentBuilder 不是一把新锤子,而是苹果给 SwiftUI Builder 家族换的一块新门牌。

以前门口写着:

ViewBuilder、ToolbarContentBuilder、CommandsBuilder、TabContentBuilder......

现在苹果想慢慢改成:

ContentBuilder。

简单点,挺好。毕竟 SwiftUI 已经够抽象了,能少一个名字,就少一个开发者半夜怀疑人生的理由。

最后,感谢宝子们的观赏,我们下次再会!8-)

相关推荐
东坡肘子13 小时前
当 Linux 成为“空气”:容器、Agent 与不再重要的“桌面之争” -- 肘子的 Swift 周报 #143
人工智能·swiftui·swift
wabil2 天前
【LVGL】滑动切换页面的界面优化实践
开发语言·ios·swift
东坡肘子8 天前
SPI 加入 Apple,Swift 迈向自举 -- 肘子的 Swift 周报 #142
人工智能·swiftui·swift
神奇的程序员13 天前
开发了一个进阶版Apple健康
swiftui·apple·apple watch
东坡肘子15 天前
Swift 还让你 Excited 吗?-- 肘子的 Swift 周报 #141
人工智能·swiftui·swift
sweet丶17 天前
Swift 元编程-Macro
swift
影寂ldy20 天前
WinForm PictureBox控件 + ImageList组件 完整笔记
开发语言·笔记·swift
Deepzz21 天前
macOS 上调教第三方鼠标的一些经验:从滚动顺滑到输入法自动切换
macos·swift·鼠标
大熊猫侯佩21 天前
WWDC26:SwiftUI 8 的 @State 全新“懒加载”机制与最佳实践
性能优化·swiftui·observable·懒加载·state·swift宏·实例初始化