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」时,自定义结果构建器就能发挥很大的优势。这不仅让代码更简洁声明式,也能让复杂布局逻辑变得更直观、更易维护。

相关推荐
LaoZhangAI1 小时前
Kiro vs Cursor:2025年AI编程IDE深度对比
前端·后端
止观止1 小时前
CSS3 粘性定位解析:position sticky
前端·css·css3
爱编程的喵1 小时前
深入理解JavaScript单例模式:从Storage封装到Modal弹窗的实战应用
前端·javascript
lemon_sjdk1 小时前
Java飞机大战小游戏(升级版)
java·前端·python
G等你下课1 小时前
如何用 useReducer + useContext 构建全局状态管理
前端·react.js
欧阳天羲1 小时前
AI 增强大前端数据加密与隐私保护:技术实现与合规遵
前端·人工智能·状态模式
慧一居士1 小时前
Axios 和Express 区别对比
前端
I'mxx2 小时前
【html常见页面布局】
前端·css·html
万少2 小时前
云测试提前定位和解决问题 萤火故事屋 上架流程
前端·harmonyos·客户端