SwiftUI @ViewBuilder 的魔法

定义

先看一下ViewBuilder的定义,实际上这是一个@resultBuilder 的 struct。

swift 复制代码
@resultBuilder public struct ViewBuilder {

    public static func buildBlock() -> EmptyView

    public static func buildBlock<Content>(_ content: Content) -> Content where Content : View
}

@resultBuilder 属性封装具体用法查看官方文档

用于函数参数的用法

下面是一个简单的例子,将 @ViewBuilder 用于参数

less 复制代码
func contextMenu<MenuItems: View>(@ViewBuilder menuItems: () -> MenuItems) -> some View

在调用的时候可以指定多个 View,而且不需要逗号分割,

scss 复制代码
myView.contextMenu {
    Text("Cut")
    Text("Copy")
    Text("Paste")
    if isSymbol {
        Text("Jump to Definition")
    }
}

多个Text是因为 buildBlock 的多参数重载实现,最多到 C9:

swift 复制代码
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)>
static func buildBlock<C0, C1, C2, C3, C4, C5>(C0, C1, C2, C3, C4, C5) -> TupleView<(C0, C1, C2, C3, C4, C5)>
static func buildBlock<C0, C1, C2, C3, C4, C5, C6>(C0, C1, C2, C3, C4, C5, C6) -> TupleView<(C0, C1, C2, C3, C4, C5, C6)>
static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7>(C0, C1, C2, C3, C4, C5, C6, C7) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7)>
static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7, C8>(C0, C1, C2, C3, C4, C5, C6, C7, C8) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7, C8)>
static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7, C8, C9>(C0, C1, C2, C3, C4, C5, C6, C7, C8, C9) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7, C8, C9)>

上面的if支持是因为ViewBuilder实现了buildEither(second:) 静态方法,还有其他更多的写法,比如For循环。

用于返回值的用法

先来梳理一下问题,当你创建一个函数,返回类型是View时,如果编译器不能在编译阶段就确定类型,那就会出现泛型无法推断类型的编译错误。

比如下面的例子,只能在运行期才能确定返回值类型。

scss 复制代码
func showTextOrImage(isImage: Bool) -> some View {

    if !isImage {
        Text("This is a title")
            .foregroundColor(.red)
    }
    else {
        Image(systemName: "square.and.arrow.up")
            .foregroundColor(.blue)
    }
}

有几种方式解决这个问题,核心就是再包一层,比如容易想到的就是自定义一个 View:

swift 复制代码
struct ShowTextOrImage: View {
    let isImage: Bool

    var body: some View {
        if !isImage {
            Text("This is a title")
                .foregroundColor(.red)
        }
        else {
            Image(systemName: "square.and.arrow.up")
                .foregroundColor(.blue)
        }
    }
}

这种方式不好的地方就是需要另写一个 struct,更好的方式是在 struct 内部通过函数就可以得到需要的 View,我们可以使用Group来实现:

scss 复制代码
// 使用 Group 包装以下
func groupDemo(isImage: Bool) -> some View {
    Group {
        if !isImage {
            Text("This is a title")
                .foregroundColor(.red)
        }
        else {
            return AnyView(Image(systemName: "square.and.arrow.up")
                .foregroundColor(.blue))
        }
    }
}

或者 转成AnyView擦除类型具体的类型:

swift 复制代码
// AnyView 擦除类型
func anyViewDemo(isImage: Bool) -> some View {
    if !isImage {
        return AnyView(Text("This is a title")
            .foregroundColor(.red))
    }
    else {
        return AnyView(Image(systemName: "square.and.arrow.up")
            .foregroundColor(.blue))
    }
}

最后一种方式就是使用 @ViewBuilder 属性封装,也可以达到目的。

scss 复制代码
@ViewBuilder
func viewBuilderDemo(isImage: Bool) -> some View {
    if !isImage {
        Text("This is a title")
            .foregroundColor(.red)
    }
    else {
        Image(systemName: "square.and.arrow.up")
            .foregroundColor(.blue)
    }
}

这里不会报错的原因,也是@resultBuilder的作用,因为ViewBuilder实现了buildEither(second:),支持 if-else 语法

用于属性

当你想实现一个自定义的VStack时,可以这么做:

css 复制代码
struct CustomVStack<Content: View>: View {
    let content: () -> Content

    var body: some View {
        VStack {
            // custom stuff here
            content()
        }
    }
}

但是这种方式只能接收单个View,无法传入多个 View:

scss 复制代码
CustomVStack {
    Text("Hello")
    Text("Hello")
}

为了达到原生VStack的效果,就必须增加一个构造函数:

less 复制代码
init(@ViewBuilder content: @escaping () -> Content) {
    self.content = content
}

每次定义容器 View 时,都得这么写的话就很啰嗦,所以有人向官方提建议,看是否能把@ViewBuilder直接用于属性。

最终这个提案通过了,发布在 Swift 5.4 版本:

css 复制代码
struct CustomVStack<Content: View>: View {
    @ViewBuilder let content: Content

    var body: some View {
        VStack {
            content
        }
    }
}

其他

相关推荐
大熊猫侯佩1 天前
在肖申克监狱玩转 iOS 26:安迪的 Liquid Glass 复仇计划
ios·swiftui·swift
大熊猫侯佩1 天前
用最简单的方式让 SwiftUI 画一颗爱你的小红心
swiftui·swift·apple
HarderCoder1 天前
Swift 初探:从变量到并发,一文带你零基础读懂官方 Tour
swift
HarderCoder1 天前
SwiftUI Binding 深坑指南:为什么 `Binding(get:set:)` 会让你的视图疯狂重绘?
swift
QWQ___qwq2 天前
My Swift笔记
swift
小蕾Java3 天前
IDEA快速上手指南!
java·intellij-idea·swift
山顶夕景4 天前
【LLM】基于ms-Swift大模型SFT和RL的训练实践
大模型·微调·swift·强化学习
HarderCoder5 天前
SwiftUI redraw 机制全景解读:从 @State 到 Diffing
swift