var body: some View {
VStack {
Text("Hello")
Image(systemName: "star")
}
}
这里的 VStack 的闭包其实是由 ViewBuilder来定义的。
ViewBuilder早期用 @_functionBuilder 标记定义,@_functionBuilder是 SwiftUI 刚发布时的私有实现,不建议直接使用。
Swift 5.4 以后,官方推出了@resultBuilder,这是公开的、推荐的语法,功能更强大,语法更友好。@resultBuilder如何使用呢?
@resultBuilder
struct MyBuilder {
static func buildBlock(_ components: String...) -> [String] {
components
}
}
//注意这里的content是一个闭包
func makeStrings(@MyBuilder _ content: () -> [String]) -> [String] {
content()
}
let arr = makeStrings {
"A"
"B"
"C"
}
// arr == ["A", "B", "C"]
这段代码真的非常难懂,我们需要先明白一个基础知识,swift 的可变参数:
在 Swift 中,可变参数(Variadic Parameters)允许你在函数或方法中接收任意数量的某种类型的参数。用法是在参数类型后面加 ...
func sum(numbers: Int...) -> Int {
var total = 0
for number in numbers {
total += number
}
return total
}
numbers 在函数体内表现为 [Int] 数组
- 每个函数只能有一个可变参数,且必须是参数列表的最后一个。
- 可变参数可以为空,即你可以不传任何参数。
如果你已经有一个数组,想传递给可变参数,需要用 ... 展开
let nums = [1, 2, 3]
sum(numbers: nums...) // 正确
在理解@resultBuilder的道路上,怎么会只有可变参数这一个考点呢!
上述例子中的 闭包{"A" "B" "C"} makeStrings的参数是怎么被转化为数组的呢?我们在来看一个非常具体的例子
import SwiftUI
@resultBuilder
struct StringBuilder {
static func buildBlock(_parts: String...) -> String {
parts.map{"⭐️"+$0+"🌈"}.joined(separator: " ")
}
}
struct ContentView: View {
@State
private var internalCounter = 0
var body: someView {
VStack {
Button("按钮") {
print(getStrings()) // ⭐️喜羊羊🌈 ⭐️美羊羊🌈 ⭐️灰太狼🌈
}
}
.padding()
.border(Color.gray)
}
@StringBuilder
func getStrings() -> String {
"喜羊羊"
"美羊羊"
"灰太狼"
}
}
其中被@StringBuilder修饰的方法getStrings会被翻译器转换为
func getStrings() -> String {
return StringBuilder.buildBlock("喜羊羊", "美羊羊", "灰太狼")
}
首先需要注意这里我们使用@StringBuilder修饰的是一个方法,后期我们还会看到用@StringBuilder修饰闭包的例子
在这个例子中方法原本是要返回"喜羊羊" "美羊羊""灰太狼"这样的内容,这种没有用逗号而是换行隔开的字符串,被转为buildBlock的可变参数,之后调用buildBlock,将buildBlock的返回值原原本本的用作@StringBuilder修饰的方法getStrings的返回值,
这操作完成了两个动作:
1、将"喜羊羊" "美羊羊""灰太狼"形式的内容,转为buildBlock的可变参数
2、完成了一次方法实现的转换,我们本来调用的是getStrings方法,但是在其返回值返回前,又对返回值进行了一次buildBlock方法处理
所以StringBuilder的buildBlock的返回值类型必须要与@StringBuilder修饰的getString方法的返回值类型相同,如果您将 buildBlock 的返回类型改为 [String],那么 getStrings 也必须相应地改为返回 [String],否则编译器会在getStirngs方法的声明处报错“Cannot convert return expression of type '[String]' to return type 'String'”
现在我们了解了resultBuilder对方法调用的转换为了理解ViewBuilder,我们还需要一个更贴近的例子
import SwiftUI
// 1. 定义 Result Builder
@resultBuilder
struct EmojiStringBuilder {
// 基本构建方法
static func buildBlock(_ components: String...) -> String {
components.map { "⭐️" + $0 + "🌈" }.joined(separator: " ")
}
// 支持可选值
static func buildOptional(_ component: String?) -> String {
component ?? "❌"
}
// 支持 if-else
static func buildEither(first component: String) -> String {
"✅ " + component
}
static func buildEither(second component: String) -> String {
"❌ " + component
}
}
// 2. 使用 Result Builder 修饰闭包参数
struct ContentView: View {
// 定义一个接受 Result Builder 闭包的函数,
//闭包也是函数,这里相当于@EmojiStringBuilder修饰的content函数
//想象一下如果我们调用content()闭包,实际调用的是哪个函数呢
func buildEmojiString(@EmojiStringBuilder _ content: () -> String) -> String {
content()
}
// 定义一个接受 Result Builder 闭包的视图
struct EmojiTextView: View {
let text: String
init(@EmojiStringBuilder _ content: () -> String) {
self.text = content()
}
var body: some View {
Text(text)
.font(.title)
.padding()
.background(Color.yellow.opacity(0.2))
.cornerRadius(10)
}
}
var body: some View {
VStack(spacing: 20) {
// 使用 Result Builder 闭包
let result = buildEmojiString {
"Hello"
"World"
if true {
"Swift"
}
if false {
"Objective-C"
} else {
"SwiftUI"
}
}
Text("Result: \(result)")
.font(.headline)
// 使用 Result Builder 视图
EmojiTextView {
"Welcome"
"to"
"SwiftUI"
}
// 另一个例子
EmojiTextView {
"Learning"
"Result"
"Builders"
}
}
.padding()
}
}
buildEmojiString接受的是一个闭包content,这个闭包被@EmojiStringBuilder修饰了,在buildEmojiString中调用了content() ,content()的返回值会被buildBlock函数再次处理可以想象一下编译器会把@EmojiStringBuilder修饰的content变成什么呢?
如果你能想明白说明你基本搞定@resultBuilder了
接下来看一下HStack的定义
首先我们知道@ViewBuilder是用@resultBuilder标记定义的
然后我们关注HStack init中的@ViewBuilder,它修饰了一个:()->Content 的闭包,那么我们可以猜到,Viewbuilder定义中的buildBlock方法,一定再次处理了()->Content 闭包的返回值,也就是Content
@frozen public struct HStack<Content> : View where Content : View {
/// Creates a horizontal stack with the given spacing and vertical 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: VerticalAlignment = .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.
@available(iOS 13.0, tvOS 13.0, watchOS 6.0, macOS 10.15, *)
public typealias Body = Never
}
接下来看下swift 6.0中ViewBuilder的定义
@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
}
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
extension ViewBuilder {
/// Produces an optional view for conditional statements in multi-statement
/// closures that's only visible when the condition evaluates to true.
public static func buildIf<Content>(_ content: Content?) -> Content? where Content : View
/// Produces content for a conditional statement in a multi-statement closure
/// when the condition is true.
public static func buildEither<TrueContent, FalseContent>(first: TrueContent) -> _ConditionalContent<TrueContent, FalseContent> where TrueContent : View, FalseContent : View
/// Produces content for a conditional statement in a multi-statement closure
/// when the condition is false.
public static func buildEither<TrueContent, FalseContent>(second: FalseContent) -> _ConditionalContent<TrueContent, FalseContent> where TrueContent : View, FalseContent : View
}
@available(iOS 14.0, macOS 11.0, tvOS 14.0, watchOS 7.0, *)
extension ViewBuilder {
/// Processes view content for a conditional compiler-control
/// statement that performs an availability check.
public static func buildLimitedAvailability<Content>(_ content: Content) -> AnyView where Content : View
}
可以看出其主要还是对content进行一个处理,然后还是返回一个content
具体如何处理,这里不细究,但其中一定包括了对content的布局
到这里基本本文的内容就结束了,后边的内容做一个补充:
看下关键方法buildBlock,这里还有宝藏要小挖一下:
public static func buildBlock<each Content>(_ content: repeat each Content) -> TupleView<(repeat each Content)> where repeat each Content : View
类型参数包(Type Parameter Packs)
通过在泛型参数前添加 each 关键字,可以声明一个类型参数包
- each Content: 声明一个可变参数泛型包,其中content需要遵循sequence协议。
- repeat each Content: 表示“重复包中的每一个类型/值”。
这个“可变参数泛型”解决了以前需要如下定义导致的问题:代码冗余,并且容器最多只能直接包含 10 个子视图。超过 10 个,编译器就会报错
static func buildBlock() -> EmptyView
static func buildBlock<Content>(Content) -> Content
static func buildBlock<C0, C1>(C0, C1) -> TupleView<(C0, C1)>
static func buildBlock<C0, C1, C2>(C0, C1, C2) -> TupleView<(C0, C1, C2)>
static func buildBlock<C0, C1, C2, C3>(C0, C1, C2, C3) -> TupleView<(C0, C1, C2, C3)>
static func buildBlock<C0, C1, C2, C3, C4>(C0, C1, C2, C3, C4) -> TupleView<(C0, C1, C2, C3, C4)>
...
最后我们来思考一个略傻的问题:@ViewBuilder是propertyWrapper吗
不是的,
@ViewBuilder是一个@resultBuilder标记定义的语法糖
@resultBuilder其实也是一个语法糖
@propertyWrapper 是声明属性包装器的语法糖
@State是使用属性包装器的语法糖
resultBuilder不是为了包装属性,而是为了结果构建。
如果本文对你有帮助,可以点个在看支持一下,这将对我有很大的帮助