当我们新建一个 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,重新渲染。
由上面代码知道,初始化时会实现这个闭包,然后改变数据时,也会重新将视图添加进数组中成为一个整体。
⭐ 自定义结果构建器优势
自定义结果构建器的优势在于,它可以让我们根据自己的需求,灵活地定义视图的组合和构建逻辑。虽然官方在现有视图(比如 VStack
、HStack
等)中已经内置了强大的 ViewBuilder
,并且也支持条件、循环等写法,我们平时直接使用就足够了。
但是,我们需要实现更复杂、更高度自定义的视图容器(例如一个表单 Form
,需要对表单项进行分组、条件渲染,甚至动态插入额外内容),或者想要封装一个可重复使用、可扩展的「专用 DSL」时,自定义结果构建器就能发挥很大的优势。这不仅让代码更简洁声明式,也能让复杂布局逻辑变得更直观、更易维护。