作为一个严重依赖 SwiftUI 的开发者,同视图打交道是最平常不过的事情了。从第一次接触 SwiftUI 的声明式编程方式开始,我便喜欢上了这种写代码的感觉。但接触地越多,碰到的问题也越多。起初,我单纯地将很多问题称之为灵异现象,认为大概率是由于 SwiftUI 的不成熟导致的。随着不断地学习和探索,发现其中有相当部分的问题还是因为自己的认知不够所导致的,完全可以改善或避免。
我将通过上下两篇博文,对构建 SwiftUI 视图的 ViewBuilder 进行探讨。上篇将介绍 ViewBuilder 背后的实现者 —— result builders ; 下篇将通过对 ViewBuilder 的仿制,进一步地探寻 SwiftUI 视图的秘密。
访问我的博客 www.fatbobman.com[1] 可以获得更好的阅读体验
本文希望达成的目标
希望在阅读完两篇文章后能消除或减轻你对下列疑问的困惑:
- 如何让自定义视图、方法支持 ViewBuilder
- 为什么复杂的 SwiftUI 视图容易在 Xcode 上卡死或出现编译超时
- 为什么会出现 “Extra arguments” 的错误提示(仅能在同一层次放置有限数量的视图)
- 为什么要谨慎使用 AnyView
- 如何避免使用 AnyView
- 为什么无论显示与否,视图都会包含所有选择分支的类型信息
- 为什么绝大多数的官方视图类型的 body 都是 Never
- ViewModifier 同特定视图类型的 modifier 之间的区别
- SwiftUI 的隐式标识和显式标识之间的区别
什么是 Result builders
介绍
result builders 允许某些函数通过一系列组件中隐式构建结果值,按照开发者设定的构建规则对组件进行排列。通过对函数语句应用构建器进行转译,result builders 提供了在 Swift 中创建新的领域特定语言( DSL )的能力(为了保留原始代码的动态语义,Swift 有意地限制了这些构建器的能力)。
与常见的使用点语法实现的类 DSL 相比,使用 result builders 创建的 DSL 使用更简单、无效内容更少、代码更容易理解(在表述具有选择、循环等逻辑内容时尤为明显),例如:
使用点语法( Plot[2] ):
代码语言:javascript复制.div(
.div(
.forEach(archiveItems.keys.sorted(by: >)) { absoluteMonth in
.group(
.ul(
.forEach(archiveItems[absoluteMonth]) { item in
.li(
.a(
.href(item.path),
.text(item.title)
)
)
}
),
.if( show,
.text("hello"),
else: .text("wrold")
),
)
}
)
)
通过 result builders 创建的构建器 ( swift-html[3] ):
代码语言:javascript复制Div {
Div {
for i in 0..<100 {
Ul {
for item in archiveItems[i] {
li {
A(item.title)
.href(item.path)
}
}
}
if show {
Text("hello")
} else {
Text("world")
}
}
}
}
历史与发展
自 Swift 5.1 开始,result builders 便随着 SwiftUI 的推出隐藏在 Swift 语言之中(当时名为 function builder)。随着 Swift 与 SwiftUI 的不断进化,最终被正式纳入到 Swift 5.4 版本之中。目前苹果在 SwiftUI 框架中大量地使用了该功能,除了最常见的视图构建器(ViewBuilder)外,其他还包括:AccessibilityRotorContentBuilder、CommandsBuilder、LibraryContentBuilder、SceneBuilder、TableColumnBuilder、TableRowBuilder、ToolbarContentBuilder、WidgetBundleBuilder 等。另外,在最新的 Swift 提案中,已出现了 Regex builder DSL[4] 的身影。其他的开发者利用该功能也创建了不少的 第三方库[5]。
基本用法
创建构建器类型
一个结果构建器类型必须满足两个基本要求。
- 它必须通过
@resultBuilder
进行标注,这表明它打算作为一个结果构建器类型使用,并允许它作为一个自定义属性使用。 - 它必须至少实现一个名为
buildBlock
的类型方法
例如:
代码语言:javascript复制@resultBuilder
struct StringBuilder {
static func buildBlock(_ parts: String...) -> String {
parts.map{"⭐️" $0 "