swiftUI中各View的叠叠乐是怎么实现的

459 阅读5分钟
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不是为了包装属性,而是为了结果构建。

如果本文对你有帮助,可以点个在看支持一下,这将对我有很大的帮助