SwiftUI @ViewBuilder详解

4,337 阅读3分钟

首先,当你见到code里使用到它,或者你想使用它的时候。我们大概率可以推断出,基本的SwiftUI元素和组件已经没办法满足我们项目复杂需求或者定制化的要求了,我们要DIY一下扩展性更强的View。

今天我们来好好捋捋这个@ViewBuilder

import SwiftUI

struct ContentView: View {
    var body: some View {
        HStack{
            Text("hello")
            Text("world")
        }
    }
}

很简单的hello world级别的SwiftUI代码对吧。首先,在SwiftUI的世界里,万物都是View,但你有没有相过,为什么我们在HStack这个View里放了两个Text View,它们就能够水平对齐呢,为什么Hstack可以接受两个View呢?我们点击HStack的definition里看看:

@inlinable public init(
    alignment: VerticalAlignment = .center,
    spacing: CGFloat? = nil,
    @ViewBuilder content: () -> Content
)

从源码中我们可看到,init方法中有一个被@ViewBuilder修饰的闭包content,这意味着这个闭包内部的表达式需要由@ViewBuilder处理。它怎么处理呢,Swift在编译被@ViewBuilder修饰的闭包的时候,会先尝试查找@ViewBuilder结构中的静态buildBlock方法,这个方法有两个View作为参数,我们先来看一下:

@available(iOS 13.0, OSX 10.15, tvOS 13.0, watchOS 6.0, *)
extension ViewBuilder {
    public static func buildBlock<C0, C1>(_ c0: C0, _ c1: C1) -> TupleView<(C0, C1)> where C0 : View, C1 : View
}

从这个ViewBuilder的源码中我们可以看到,它接收两个View作为输入参数,返回一个联合了两个View的TupleView。

你往下阅读源码,会发现还有其他的声明:

@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
extension ViewBuilder {

    public static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7, C8, C9>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6, _ c7: C7, _ c8: C8, _ c9: C9) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7, C8, C9)> where C0 : View, C1 : View, C2 : View, C3 : View, C4 : View, C5 : View, C6 : View, C7 : View, C8 : View, C9 : View
}

这告诉我们,在目前的SwiftUI版本下,@ViewBuilder修饰的闭包内只能接受十个View.

什么叫做TupleView呢?

TupleView是根据视图值的快速元组创建的视图。 TupleView内部没有任何逻辑。 它只保留视图。 TupleView完全透明,其行为类似于其父视图。 这意味着当你将其放入HStack中时,TupleView会将来自元组的视图放置在水平方向上。

OK,现在我们已经明白了@ViewBuilder的作用,我们来想想怎么使用它。

假设我们要给我们的app做notification功能,首先我们想到这个notification应该组件化,但我们notification的内容应该是多样化的,定制化的。这时候,我们可以使用@ViewBuilder

import SwiftUI

struct NotificationView<Content: View>: View {
    let content: Content

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

    var body: some View {
        content
            .padding()
            .background(Color(.tertiarySystemBackground))
            .cornerRadius(16)
            .transition(.move(edge: .top))
            .animation(.spring())
    }
}

我们在这里写了一个NotificationView。在这个View里,我们绘制出了notification基本的样式,但是content的类型是一个被@ViewBuilder修饰的闭包。这就给了我们后面自定义推送内容打下了基础。

当我们想使用这个NotificationView时,我们可以这样:

import SwiftUI

struct ContentView: View {
    @State private var notificationShown = false

    var body: some View {
        VStack {
            if self.notificationShown {
                NotificationView {
                    Text("notification")
                }
            }

            Spacer()

            Button("toggle") {
                self.notificationShown.toggle()
            }

            Spacer()
        }
    }
}