概览
SwiftUI 带来了描述性界面布局的新玩法,让我们可以超轻松的创建复杂应用界面。但是在早期 SwiftUI 中有一个“著名”的限制大家知道么?那就是 @ViewBuilder 中嵌入子视图数量不能超过 10 个!
不过,从 Swift 5.9 开始这一“桎梏”已悄然消失的无影无踪。
这个限制为什么已然烟消云散?早期的限制又是如何产生的呢?
在本篇博文中,您将学到以下内容
- 概览
- 1. 不能超过 10 个,你是来逗我的吗?
- 2. “值与类型形参包”
- 3. SwiftUI 的新实现
- 4. 为何不能用泛型数组?
- 总结
想知道事件的前因后果么?那还等什么呢?
Let‘s go!!!😉
1. 不能超过 10 个,你是来逗我的吗?
在 Swift 5.5 中增加了 some 关键字,让 SwiftUI 能够用简洁类型来描述海量复合视图。这还不算完,可能 觉得视图组合的手法还是太麻烦,随即又祭出 @ViewBuilder 来进一步简化 SwiftUI 的视图构建。
其实,SwiftUI 视图的 body 计算属性已被 @ViewBuilder 默默修饰着,我们能够轻松自在,全靠 @ViewBuilder 为我们负重前行:
@ViewBuilder var body: Self.Body { get }
更多 ViewBuilder 实现细节的讨论,请小伙伴们移步 Swift 官方社区观赏:
- SwiftUI @ViewBuilder Result is a TupleView, How is Apple Using It And Able to Avoid Turning Things Into AnyVIew?
@ViewBuilder 其实是结果构建器(Result Builder,Swift 5.4)在 SwiftUI 中的一个实现。结果构建器可以被视为一种嵌入式领域特定语言(DSL),用于将收集的内容组合成最终的结果。
这就是我们可以这样创建 SwiftUI 复合视图的原因:
@ViewBuilder func lot(_ needDetails: Bool) -> some View {
Text("Hello World")
.font(.title)
if needDetails {
Text("大熊猫侯佩 @ csdn")
.font(.headline)
.foregroundStyle(.gray)
}
}
那么,ViewBuilder 在内部是如何处理传入不定数量视图的呢?
ViewBuilder 为了满足 Result Builder 的语义,必须实现其规定的一系列方法:
取决于大家要实现 DSL 语言的完整性和复杂性,我们可以选择实现尽可能少或全部这些方法。
讨论如何用 Result Builder 来实现自己的 DSL 语言超出了本文的范畴,感兴趣的小伙伴们可以移步下面的链接观赏进一步内容:
- Result builders in Swift explained with code examples
想了解更多 Swift 语言开发的知识,小伙伴们可以到我的专题专栏中进行系统性学习:
- Swift 语言开发精讲(文章平均质量分 97)
而对于简单 View 的合成,ViewBuilder 竟然采用了一种最“蠢”的方式:为每种“可能”的情况手动定义一个方法。
于是乎,就有了下面这一大坨泛型方法:
正如小伙伴们所猜的那样,这些方法中最大可传入的参数数量就是 10 (c0-c9),所以这就是“桎梏”的根本原因:我们在 @ViewBuilder 中最多只能包含 10 个子视图。
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 {
return .init((c0, c1, c2, c3, c4, c5, c6, c7, c8, c9))
}
对于超过 10 个视图的情况,我们只能“八仙过海各显神通”的尝试绕过它。
比如一种办法是:将 10 个以上的视图塞到多个 Group 中去。
2. “值与类型形参包”
从 Swift 5.9 开始,苹果似乎认识到之前的做法比较“二”,所以推出了新的“值与类型形参包”(Value and Type parameter packs)机制。
该机制专门用于处理不确定数量泛型参数的方法:
func eachFirst<each T: Collection>(_ item: repeat each T) -> (repeat (each T).Element?) {
return (repeat (each item).first)
}
比如在上面代码中,我们用 each 和 repeat each 分别修饰了泛型参数的形参和结果部分。
eachFirst() 方法的作用是将所有传入集合的第一个元素组成一个新的元组。现在 eachFirst() 泛型方法可以接受任意个类型为 Collection 的参数,同时返回同样数量 Collection.Element? 类型元素的元组。
我们可以这样调用 eachFirst() 方法:
let numbers = [0, 1, 2]
let names = ["Antoine", "Maaike", "Sep"]
let firstValues = eachFirst(numbers, names)
print(firstValues)
// (Optional(0), Optional("Antoine"))
看到了么?不管传入参数有多少个、不管它们是什么类型(至少必须是 Collection),eachFirst() 方法都可以正常工作。
有了“值与类型形参包”,我们处理泛型方法的灵活性提升一个新层级!
3. SwiftUI 的新实现
在 Swift 5.9 中,SwiftUI 用新“值与类型形参包”机制重写了 ViewBuilder 的实现。
不像之前每种情况“傻傻的”写一个对应的 buildBlock() 方法,现在只需一个带 each/repeat each 的 buildBlock() 方法足矣:
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
@resultBuilder public struct ViewBuilder {
public static func buildBlock<each Content>(_ content: repeat each Content) -> TupleView<(repeat each Content)> where repeat each Content : View
}
如上代码所示,我们现在可以向 @ViewBuilder 传递任意数量的视图了:
struct ContentView: View {
var body: some View {
Group {
Text("1")
Text("2")
Text("3")
Text("4")
Text("5")
Text("6")
Text("7")
Text("8")
Text("9")
Text("10")
Text("11")
Text("12")
}
.foregroundStyle(.white)
.background {
Circle()
.fill(Color.blue.opacity(0.5))
.frame(width: 35)
}
.shadow(radius: 5.0)
.padding()
.font(.title2.weight(.bold))
}
}
是不是很赞呢?棒棒哒💯
4. 为何不能用泛型数组?
有些小伙伴可能觉得,为什么之前 eachFirst() 方法不能用泛型数组的方式来实现呢?用泛型数组不就可以传入任意数量的集合参数了吗?
我们来试一下:
func eachFirst<T: Collection>(collections: [T]) -> [T.Element?] {
collections.map(\.first)
}
实际运行就会发现,如果用泛型数组则无法传入不同类型元素的集合:
这就是为什么上面代码报错的原因了。
有时候我们希望 eachFirst() 泛型方法中至少要带一个形参,这可以用类似下面的方式来实现:
func eachFirst<FirstT: Collection, each T: Collection>(_ firstItem: FirstT, _ item: repeat each T) -> (FirstT.Element?, repeat (each T).Element?) {
return (firstItem.first, repeat (each item).first)
}
let numbers = [0, 1, 2]
let names = ["Antoine", "Maaike", "Sep"]
let booleans = [true, false, true]
let doubles = [3.3, 4.1, 5.6]
let firstValues = eachFirst(numbers, names, booleans, doubles)
print(firstValues)
// (Optional(0), Optional("Antoine"), Optional(true), Optional(3.3))
总结
在本篇博文中,我们讨论了 SwiftUI 中“嵌入视图数量不能超过 10 个”这一限制的原因,并介绍了从 Swift 5.9+ 开始这一限制为什么最终消失了?
感谢观赏,再会!😎