SwiftUI的结果构建器ViewBuilder解释

当我们新建一个 iOS 项目时,会在 ContentView.swift 看到类似这样的代码:

swift 复制代码
import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack {
            Image(systemName: "globe")
                .imageScale(.large)
                .foregroundStyle(.tint)
            Text("Hello, world!")
        }
        .padding()
    }
}

#Preview {
    ContentView()
}

我很好奇,为什么是这样的书写方式?Swift 是怎么创建视图的?于是我点击 VStack 进入源码,看到下面的代码。


💡 VStack 源码

swift 复制代码
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
@frozen public struct VStack<Content> : View where Content : View {

    /// Creates an instance with the given spacing and horizontal alignment.
    ///
    /// - Parameters:
    ///   - alignment: The guide for aligning the subviews in this stack. This
    ///     guide has the same vertical screen coordinate for every subview.
    ///   - spacing: The distance between adjacent subviews, or `nil` if you
    ///     want the stack to choose a default distance for each pair of
    ///     subviews.
    ///   - content: A view builder that creates the content of this stack.
    @inlinable public init(alignment: HorizontalAlignment = .center, spacing: CGFloat? = nil, @ViewBuilder content: () -> Content)

    /// The type of view representing the body of this view.
    ///
    /// When you create a custom view, Swift infers this type from your
    /// implementation of the required ``View/body-swift.property`` property.
    public typealias Body = Never
}

这里的初始化函数中定义了水平对齐方式、子视图之间的间距,还有一个闭包,闭包是初始化函数的最后一个参数,可以看到,示例将前面的参数默认不写,闭包写成了尾随闭包。但是,闭包前面还有一个 ViewBuilder 标记。标记有什么作用?


🧱 ViewBuilder 的作用

ViewBuilder 是结果构造器,可以将多个子视图构造成一个整体,

下面是源码:

swift 复制代码
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
@resultBuilder public struct ViewBuilder {

    /// Builds an expression within the builder.
    public static func buildExpression<Content>(_ content: Content) -> Content where Content : View

    /// Builds an empty view from a block containing no statements.
    public static func buildBlock() -> EmptyView

    /// Passes a single view written as a child view through unmodified.
    ///
    /// An example of a single view written as a child view is
    /// `{ Text("Hello") }`.
    public static func buildBlock<Content>(_ content: Content) -> Content where Content : View

    public static func buildBlock<each Content>(_ content: repeat each Content) -> TupleView<(repeat each Content)> where repeat each Content : View
}

⚙️ buildExpression 方法

buildExpression 方法负责处理闭包中的表达式。

下面有几个方法,前面两个 buildBlock 是视图为空和为一个视图的情况,最后的 buildBlock<each Content>(_ content: repeat each Content) 方法会将 content 中的多个视图构造成一个 TupleView。


🔎 其他辅助方法

另外还有三种语句:

✅ buildIf

swift 复制代码
public static func buildIf<Content>(_ content: Content?) -> Content? where Content : View

就是一个 if 判断。

swift 复制代码
VStack {
    if showTitle {
        Text("Title")
    }
}

✅ buildEither

swift 复制代码
public static func buildEither<TrueContent, FalseContent>(first: TrueContent) -> _ConditionalContent<TrueContent, FalseContent>
public static func buildEither<TrueContent, FalseContent>(second: FalseContent) -> _ConditionalContent<TrueContent, FalseContent>

其实就是条件语句 if else 和 switch,内部会通过 buildEither(first:)buildEither(second:) 把结果包装成 _ConditionalContent,然后在运行时根据条件选择。

示例:

swift 复制代码
VStack {
    if condition {
        Text("True")
    } else {
        Text("False")
    }
}

✅ buildLimitedAvailability

swift 复制代码
public static func buildLimitedAvailability<Content>(_ content: Content) -> AnyView where Content : View

可用性判断,方便根据不同版本显示不同视图。

示例:

swift 复制代码
if #available(iOS 15, *) {
    NewView()
} else {
    OldView()
}

🧑‍💻 自己实现的结果构建器

下面是我实现的一个结果构建器。用 @resultBuilder 标记的结构体就会告诉编译器这是一个结果构建器。当然,你也可以像官方一样在里面加入条件控制函数。

swift 复制代码
@resultBuilder
struct MyViewBuilder {
    static func buildBlock(_ components: some View...) -> [AnyView] {
        components.map { AnyView($0) }
    }
}

static func buildBlock(_ components: some View...) -> [AnyView]

  • buildBlock 是结果构建器必须实现的静态函数,用于把花括号里的视图「组合」起来。

  • components: some View...

    • 表示可以接受多个视图参数,数量不限制(就是变参)。
  • -> [AnyView]

    • 返回一个 AnyView 数组,原因是每个子视图具体类型可能不同,而 Swift 要求数组里元素同类型,所以用 AnyView 做类型擦除。

components.map { AnyView($0) }

  • 把所有传进来的视图都转换成 AnyView 并组成一个新数组返回。

📦 定义一个使用结果构建器的容器视图

swift 复制代码
struct CustomStack: View {
    let views: [AnyView]

    init(@MyViewBuilder content: () -> [AnyView]) {
        self.views = content()
    }

    var body: some View {
        VStack {
            ForEach(0..<views.count, id: .self) { index in
                views[index]
            }
        }
    }
}

使用示例

swift 复制代码
struct ContentView: View {
    var body: some View {
        CustomStack {
            Text("Hello")
                .font(.largeTitle)
            Divider()
            Image(systemName: "star.fill")
                .foregroundColor(.yellow)
            Text("This is a custom stack!")
        }
        .padding()
    }
}

🔁 添加条件控制

下面的示例代码添加了控制语句。

swift 复制代码
import SwiftUI

@resultBuilder
struct MyViewBuilder {
    static func buildBlock(_ components: [AnyView]...) -> [AnyView] {
        components.flatMap { $0 }
    }

    static func buildExpression(_ expression: some View) -> [AnyView] {
        [AnyView(expression)]
    }

    static func buildOptional(_ component: [AnyView]?) -> [AnyView] {
        component ?? []
    }

    static func buildEither(first component: [AnyView]) -> [AnyView] {
        component
    }

    static func buildEither(second component: [AnyView]) -> [AnyView] {
        component
    }

    static func buildArray(_ components: [[AnyView]]) -> [AnyView] {
        components.flatMap { $0 }
    }
}

结构体的代码不变

swift 复制代码
struct CustomStack: View {
    let views: [AnyView]

    init(@MyViewBuilder content: () -> [AnyView]) {
        self.views = content()
    }

    var body: some View {
        VStack {
            ForEach(0..<views.count, id: .self) { index in
                views[index]
            }
        }
        .padding()
        .border(Color.blue, width: 2)
    }
}

使用循环条件的视图

swift 复制代码
struct ContentView: View {
    @State private var showStars = true

    var body: some View {
        VStack(spacing: 20) {
            Button("Toggle Stars") {
                showStars.toggle()
            }

            CustomStack {
                Text("Hello, World!")
                    .font(.largeTitle)

                if showStars {
                    Image(systemName: "star.fill")
                        .foregroundColor(.yellow)
                    Text("Stars are visible ✨")
                } else {
                    Text("Stars are hidden 🌑")
                }

                ForEach(1..<4) { i in
                    Text("Item (i)")
                        .foregroundColor(.purple)
                }

                Text("End of CustomStack")
            }
        }
        .padding()
    }
}

💬 官方结果构建器示例

如果直接使用官方的结果构建器将会是这样的,这里我没有把闭包写出来。

swift 复制代码
struct Card<Content: View>: View {
    let content: Content

    init(@ViewBuilder content: () -> Content) {
        self.content = content()
    }

    var body: some View {
        VStack {
            content
        }
        .padding()
        .background(Color.white)
        .cornerRadius(12)
        .shadow(color: .black.opacity(0.1), radius: 8, x: 0, y: 4)
    }
}

使用示例

swift 复制代码
struct ContentView: View {
    var body: some View {
        VStack(spacing: 20) {
            // ✅ 使用 Card 组件包裹
            Card(content:{ViewBuilder.buildBlock(
                Image(systemName: "globe")
                    .imageScale(.large)
                    .foregroundStyle(.tint),
                Text("Hello, world!")
                    .font(.headline),
                Text("欢迎来到卡片视图")
                    .font(.subheadline)
                    .foregroundColor(.gray)
                )
            })

            Card {
                Text("SwiftUI 自定义卡片")
                    .font(.title3)
                Image(systemName: "star.fill")
                    .foregroundColor(.yellow)
                Text("这是另一个示例")
            }
        }
        .padding()
        .background(Color(UIColor.systemGroupedBackground))
    }
}

⚡ 最后补充

计算属性 body 是每一个 View 都必须写的,计算属性不存储值,而是在需要的时候计算,SwiftUI 会根据 body 的定义来渲染内容,SwiftUI 会监听数据,每当数据发生变化时,就会重新调用 body,重新渲染。

由上面代码知道,初始化时会实现这个闭包,然后改变数据时,也会重新将视图添加进数组中成为一个整体。


⭐ 自定义结果构建器优势

自定义结果构建器的优势在于,它可以让我们根据自己的需求,灵活地定义视图的组合和构建逻辑。虽然官方在现有视图(比如 VStackHStack 等)中已经内置了强大的 ViewBuilder,并且也支持条件、循环等写法,我们平时直接使用就足够了。

但是,我们需要实现更复杂、更高度自定义的视图容器(例如一个表单 Form,需要对表单项进行分组、条件渲染,甚至动态插入额外内容),或者想要封装一个可重复使用、可扩展的「专用 DSL」时,自定义结果构建器就能发挥很大的优势。这不仅让代码更简洁声明式,也能让复杂布局逻辑变得更直观、更易维护。

相关推荐
崔庆才丨静觅3 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60614 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了4 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅4 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅5 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅5 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment5 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅5 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊5 小时前
jwt介绍
前端
爱敲代码的小鱼5 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax