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 学习笔记 - SwiftUI 和 简单布局
ios·swift
2501_915918413 天前
有没有Xcode 替代方案?在快蝎 IDE 中完成 iOS 开发的过程
ide·vscode·ios·个人开发·xcode·swift·敏捷流程
songgeb3 天前
Compositional layout in iOS
ios·swift·设计
1024小神4 天前
记录xcode项目swiftui配置APP加载启动图
前端·ios·swiftui·swift
wjm0410065 天前
ios学习路线-- swift基础2
学习·ios·swift
游戏开发爱好者86 天前
如何使用Instruments和Keymob进行Swift应用性能优化分析
开发语言·ios·性能优化·小程序·uni-app·iphone·swift
游戏开发爱好者87 天前
新的 iOS 开发工具体验,在快蝎 IDE 里完成应用开发与真机调试
ide·vscode·ios·objective-c·个人开发·swift·敏捷流程
东坡肘子7 天前
50 岁的苹果和 51 岁的我 -- 肘子的 Swift 周报 #127
人工智能·swiftui·swift
denggun123458 天前
Sendable 协议-Swift 结构化并发的核心安全保障
ios·swift
denggun123458 天前
结构化并发(Structured Concurrency)
开发语言·ios·swift