阅读视图
有用的知识又增加了:为何无法编译某些 WWDC 官方视频中的代码?
iOS 17(SwiftUI 5.0)带来的图表(Charts)新类型:“大饼”与“甜甜圈”
SwiftUI 5.0(iOS 17)TipKit 让用户更懂你的 App
Swift 新并发模型中 isolated 和 nonisolated 关键字的含义看这篇就懂了!
概览
在 Swift 新 async/await 并发模型中,我们可以利用 Actor 来避免并发同步时的数据竞争,并从语义上简化代码。
Actor 伴随着两个独特关键字:isolated
和 nonisolated
,弄懂它们的含义、合理合规的使用它们是完美实现同步的必要条件。
那么小伙伴们真的搞清楚它们了吗?
在本篇博文中,您将学到如下内容:
- isolated 关键字
- nonisolated 关键字
- 没有被 async 修饰的方法也可以被异步等待!
闲言少叙,让我们即刻启航!
Let‘s go!!!;)
isolated 关键字
Actor 从本质上来说就是一个同步器
,它必须严格限制单个实例执行上下文以满足同步的语义。
这意味着在 Actor 中,所有可变属性、计算属性以及方法等默认都是被隔离执行的。
actor Foo {
let name: String
let age: Int
var luck = 0
init(name: String, age: Int, luck: Int = 0) {
self.name = name
self.age = age
self.luck = luck
}
func incLuck() {
luck += 1
}
var fullDesc: String {
"\(name)[\(age)] luck is *\(luck)*"
}
}
如上代码所示,Foo 中的 luck 可变属性、incLuck 方法以及 fullDesc 计算属性默认都被打上了 isolated 烙印。大家可以想象它们前面都隐式被 isolated 关键字修饰着,但这不能写出来,如果写出来就会报错:
在实际访问或调用这些属性或方法时,必须使用 await 关键字:
Task {
let foo = Foo(name: "hopy", age: 11)
await foo.incLuck()
print(await foo.luck)
print(await foo.fullDesc)
}
正是 await 关键字为 Foo 实例内容的同步创造了隔离条件,以摧枯拉朽之势将数据竞争覆巢毁卵。
nonisolated 关键字
但是在有些情况下 isolated 未免有些“防御过度”了。
比如,如果我们希望 Foo 支持 CustomStringConvertible 协议,那么势必需要实现 description 属性:
extension Foo: CustomStringConvertible {
var description: String {
"\(name)[\(age)]"
}
}
如果大家像上面这样写,那将会妥妥的报错:
因为 description 作为计算属性放在 Actor 中,其本身默认处在“隔离”状态,而 CustomStringConvertible 对应的 description 实现必须是“非隔离”状态!
大家可以这样理解:我们不能异步调用 foo.description!
extension Foo: CustomStringConvertible {
/*
var description: String {
"\(name)[\(age)]"
}*/
var fakeDescription: String {
"\(name)[\(age)]"
}
}
Task {
let foo = Foo(name: "hopy", age: 11)
// foo.description 不能异步执行!!!
print(await foo.fakeDescription)
}
大家或许注意到,在 Foo#description 中,我们只使用了 Foo 中的只读属性。因为 Actor 中只读属性都是 nonisolated 隐式修饰,所以这时我们可以显式用 nonisolated 关键字修饰 description 属性,向 Swift 表明无需考虑 Foo#description 计算属性内部的同步问题,因为里面没有任何可变的内容:
extension Foo: CustomStringConvertible {
nonisolated var description: String {
"\(name)[\(age)]"
}
}
Task {
let foo = Foo(name: "hopy", age: 11)
print(foo)
}
但是,如果 nonisolated 修饰的计算属性中含有可变(isolated)内容,还是会让编译器“怨声载道”:
没有被 async 修饰的方法也可以被异步等待!
最后,我们再介绍 isolated 关键字一个非常有用的使用场景。
考虑下面的 incLuck() 全局函数,它负责递增传入 Foo 实例的 luck 值,由于 Actor 同步保护“魔法”的存在,它必须是一个异步函数:
func incLuck(_ foo: Foo) async {
await foo.incLuck()
}
不过,如果我们能够保证 incLuck() 方法传入 Foo 实参的“隔离性”,则可以直接访问其内部的“隔离”(可变)属性!
如何保证呢?
很简单,使用 isolated 关键字:
func incLuck2(_ foo: isolated Foo) {
foo.luck += 1
}
看到了吗? luck 是 Foo 内部的“隔离”属性,但我们竟然可以在外部对其进行修改,是不是很神奇呢?
这里,虽然 incLuck2() 未用 async 修饰,但它仍是一个异步方法,我称之为全局“隐式异步”方法:
Task {
let foo = Foo(name: "hopy", age: 11)
await incLuck(foo)
await incLuck2(foo)
}
虽然 foo 是一个 Actor 实例,它包含一些外部无法直接查看的“隔离”内容,但我们仍然可以使用一些调试手段探查其内部,比如 dump 方法:
Task {
let foo = Foo(name: "hopy", age: 11)
await incLuck(foo)
await incLuck2(foo)
dump(foo)
}
输出如下:
over
hopy[11]
▿ hopy[11] #0
- $defaultActor: (Opaque Value)
- name: "hopy"
- age: 11
- luck: 2
通过 dump() 方法输出可以看到,foo 的 luck 值被正确增加了 2 次,棒棒哒!!!💯
总结
在本篇博文中,我们通过几个通俗易懂的例子让小伙伴们轻松了解到 Swift 新 async/await 并发模型中 isolated 与 nonisolated 关键字的精髓,并对它们做了进一步的深入拓展。
感谢观赏,再会!8-)
如何让异步序列(AsyncSequence)优雅的感知被取消(Cancel)
Swift 5.9 与 SwiftUI 5.0 中新 Observation 框架应用之深入浅出
0. 概览
Swift 5.9 一声炮响为我们带来全新的宏(Macro)机制,也同时带来了干霄凌云的 Observation 框架。
Observation 框架可以增强通用场景下的使用,也可以搭配 SwiftUI 5.0 而获得双剑合璧的更强威力。
在本篇博文,您将学到如下内容:
- @Observable 宏
- 通用情境下如何观察 Observable 对象?
- Observable 对象与 SwiftUI 珠联璧合
- 被“抛弃的” @EnvironmentObject
- 在视图中将不可变 Observable 对象转换为可变对象的妙招
那么,就让我们赶快进入 Observation 奇妙的世界吧!
Let‘s go!!!;)
1. @Observable 宏
简单来说,Observation 框架为我们提供了集鲁棒性(robust)、安全性、高性能等三大特性为一身的 Swift 全新观察者设计模式。
它的核心功能在于:监视对象状态,并在改变时做出反应!
在 Swift 5.9 中,我们可以非常轻松的通过 @Observable 宏将普通类“转化为”可观察(Observable)类。自然,它们的实例都是可观察的:
@Observable
final class Hero {
var name: String
var power: Int
init(name: String, power: Int) {
self.name = name
self.power = power
}
}
@Observable
final class Model {
var title: String
var createAt: Date?
var heros: [Hero]
init(title: String, heros: [Hero]) {
self.title = title
self.createAt = Date.now
self.heros = heros
}
}
如上代码所示,我们定义了两个可观察类 Model 和 Hero,就是这么简单!
2. 通用情境下如何观察 Observable 对象?
在一个对象成为可观察之后,我们可以通过 withObservationTracking() 方法随时监听它状态的改变:
我们可以将对象需要监听的属性放在 withObservationTracking() 的 apply 闭包中,当且仅当( Hero 中其它属性的改变不予理会)这些属性发生改变时其 onChange 闭包将会被调用:
let hero = Hero(name: "大熊猫侯佩", power: 5)
func watching() {
withObservationTracking({
NSLog("力量参考值:\(hero.power)")
}, onChange: {
NSLog("改变之前的力量!:\(hero.power)")
watching()
})
}
watching()
hero.name = "地球熊猫"
hero.power = 11
hero.power = 121
以上代码输出如下:
使用 withObservationTracking() 方法有 3 点需要注意:
- 它默认只会被调用 1 次,所以上面为了能够重复监听,我们在 onChange 闭包里对 watching() 方法再次进行了调用;
- withObservationTracking() 方法的 apply 闭包不管如何都会被调用 1 次,即使其监听的属性从未改变过;
- 在监听闭包中只能得到属性改变前的旧值;
目前,上面测试代码在 Xcode 15 的 Playground 中编译会报错,提示如下:
error: test15.playground:8:13: error: external macro implementation type 'ObservationMacros.ObservableMacro' could not be found for macro 'Observable()' final class Hero { ^
Observation.Observable:2:180: note: 'Observable()' declared here @attached(member, names: named(_$observationRegistrar), named(access), named(withMutation)) @attached(memberAttribute) @attached(extension, conformances: Observable) public macro Observable() = #externalMacro(module: "ObservationMacros", type: "ObservableMacro")
小伙伴们可以把它们放在 Xcode 的 Command Line Tool 项目中进行测试:
3. Observable 对象与 SwiftUI 珠联璧合
要想发挥 Observable 对象的最大威力,我们需要 SwiftUI 来一拍即合。
在 SwiftUI 中,我们无需再显式调用 withObservationTracking() 方法来监听改变,如虎添翼的 SwiftUI 已为我们自动完成了所有这一切!
struct ContentView: View {
let model = Model(title: "地球超级英雄", heros: [])
var body: some View {
NavigationStack {
Form {
LabeledContent(content: {
Text(model.title)
}, label: {
Text("藏匿点名称")
})
LabeledContent(content: {
Text(model.createAt?.formatted(date: .omitted, time: .standard) ?? "无")
}, label: {
Text("更新时间")
})
Button("刷新") {
// SwiftUI 会自动监听可观察对象的改变,并刷新界面
model.title = "爱丽丝仙境兔子洞"
model.createAt = Date.now
}
}.navigationTitle(model.title)
}
}
}
注意,上面代码中 model 属性只是一个普通的 let 常量,即便如此 model 的改变仍会反映到界面上:
4. 被“抛弃的” @EnvironmentObject
有了 Swift 5.9 中新 Observation 框架加入游戏,在 SwiftUI 5.0 中 EnvironmentObject 再无用武之地,我们仅用 Environment 即可搞定一切!
早在 SwiftUI 1.0 版本时,其就已经提供了 Environment 对应的构造器:
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
@frozen @propertyWrapper public struct Environment<Value> : DynamicProperty {...}
有了新 Observation 框架的入驻,结合其 Observable 可观察对象,Environment 可以再次大放异彩:
struct HeroListView: View {
@Environment(Model.self) var model
var body: some View {
List(model.heros) { hero in
HStack {
Text(hero.name)
.font(.headline)
Spacer()
Text("\(hero.power)")
.font(.subheadline)
.foregroundStyle(.gray)
}
}
}
}
struct ContentView: View {
@State var model = Model(title: "地球超级英雄", heros: [
.init(name: "大熊猫侯佩", power: 5),
.init(name: "孙悟空", power: 1000),
.init(name: "哪吒", power: 511)
])
var body: some View {
NavigationStack {
Form {
NavigationLink(destination: HeroListView().environment(model)) {
Text("查看所有英雄")
}
}.navigationTitle(model.title)
}
}
}
现在,即使跨越多重层级关系我们也可以只通过 @Environment 而不用 @EnvironmentObject 来完成状态的间接传递了,是不是很赞呢?👍🏻
5. 在视图中将不可变 Observable 对象转换为可变对象的妙招
介绍了以上这许多,就还剩一个主题没有涉及:Observable 对象的可变性!
为了能够在子视图中更改对应的可观察对象,我们可以用 @Bindable 修饰传入的 Observable 对象:
struct HeroView: View {
@Bindable var hero: Hero
var body: some View {
Form {
TextField("名称", text: $hero.name)
TextField("力量", text: .init(get: {
String(hero.power)
}, set: {
hero.power = Int($0) ?? 0
}))
}
}
}
不过,对于之前 @Environment 那个例子来说,如何达到子视图能够修改传入的 @Environment 可观察对象呢?
别急,我们可以利用称为“临时可变(Temporary Variable)”的技术将原先不可变的可观察对象改为可变:
extension Hero: Identifiable {
var id: String {
name
}
}
struct HeroListView: View {
@Environment(Model.self) var model
var body: some View {
// 在 body 内将 model 改为可变
@Bindable var model = model
VStack {
List(model.heros) { hero in
HStack {
Text(hero.name)
.font(.headline)
Spacer()
Text("\(hero.power)")
.font(.subheadline)
.foregroundStyle(.gray)
}
}.safeAreaInset(edge: .bottom) {
// 绑定可变 model 中的状态以修改英雄名称
TextField("", text: $model.heros[0].name)
.padding()
}
}
}
}
运行效果如下:
“临时可变”这一技术可以用于视图中任何化“不变”为“可变”的场景中,当然对于直接视图间对象的传递,我们可以使用 @Bindable 这一更为“正统”的方法。
6. 总结
在本篇博文中,我们讨论了在 Swift 5.0 和 SwiftUI 5.0 中大放异彩 Observation 框架的使用,并就诸多技术细节问题给与了详细的介绍,愿君喜欢。
感谢观赏,再会!8-)
SwiftUI 4.0:两种方式实现子视图导航功能
0. 概览
从 SwiftUI 4.0 开始,觉悟了的苹果毅然抛弃了已“药石无效”的 NavigationView,改为使用全新的 NavigationStack 视图。
诚然,NavigationStack 从先进性来说比 NavigationView 有不小的提升,若要如数家珍得单开洋洋洒洒的一篇来介绍。
关于 SwiftUI 中旧 NavigationView 视图种种人神共愤的“诟病”和弊端,请移步我的其它专题博文观赏:
- SwiftUI进入多重嵌套视图后如何一键退回到根视图
- iOS 16.2 在 SwiftUI 子视图中无法关闭弹出的(sheet)导航视图(NavigationView)之解决
- SwiftUI导航至子视图后状态改变导致导航栈提前弹出的原因及解决
- SwiftUI实现不同TabView标签页中任意导航层级视图之间自动相互跳转那些事儿
- SwiftUI中NavigationLink多层嵌套导航无法返回上一层的原因及解决
不过,这些都不是本篇的主旨。在本篇中我们将尝试一起另辟蹊径来完成两种截然不同的导航机制。
在本篇博文,您将学到如下内容:
- NavigationStack
- NavigationSplitView 导航之“假象”
- 洞若观火:在 iPad 上的比较
无需等待,Let’s go!!!;)
1. NavigationStack
从 SwiftUI 4.0 开始, 引入的新 NavigationStack 导航器终于不再以分散杂乱的数据作为导航触发媒介,而是将有序的数据集合作为导航跳转的核心来对待!(所以,一步跳回根视图成了雕虫小技)
其中,NavigationStack 导航器重要的组成部分 NavigationLink 除了为了兼容性暂时保留的传统跳转方式以外,主打以状态本身的值作为导航的基石。这样,对于不同类型状态触发的跳转,我们可以干净而从容的分别处理:
下面举一例。
首先,定义简单的数据结构,Alliance 中包含若干 Hero:
@Observable
final class Hero {
var name: String
var power: Int
init(name: String, power: Int) {
self.name = name
self.power = power
}
}
extension Hero: Identifiable {
var id: String {
name
}
}
extension Hero: Hashable {
static func == (lhs: Hero, rhs: Hero) -> Bool {
lhs.name == rhs.name && lhs.power == rhs.power
}
func hash(into hasher: inout Hasher) {
hasher.combine(name)
hasher.combine(power)
}
}
@Observable
final class Alliance: Hashable {
static func == (lhs: Alliance, rhs: Alliance) -> Bool {
lhs.title == rhs.title
}
func hash(into hasher: inout Hasher) {
hasher.combine(title)
}
var title: String
var createAt: Date?
var heros: [Hero]
init(title: String, heros: [Hero]) {
self.title = title
self.createAt = Date.now
self.heros = heros
}
}
接下来是与之对应的子视图,注意驱动 NavigationLink 导航的是 Hero 本身,而不是什么“莫名奇妙”的条件变量:
struct HeroDetailView: View {
let hero: Hero
var body: some View {
VStack {
Text("力量: \(hero.power)")
.font(.largeTitle)
}.navigationTitle(hero.name)
}
}
struct HeroListView: View {
@Environment(Alliance.self) var model
var body: some View {
VStack {
List(model.heros) { hero in
NavigationLink(value: hero) {
HStack {
Text(hero.name)
.font(.headline)
Spacer()
Text("\(hero.power)")
.font(.subheadline)
.foregroundStyle(.gray)
}
}
}
}
}
}
接着是主视图:
struct ContentView: View {
@State var model = Alliance(title: "地球超级英雄", heros: [
.init(name: "大熊猫侯佩", power: 5),
.init(name: "孙悟空", power: 1000),
.init(name: "哪吒", power: 511)
])
var body: some View {
NavigationStack {
Form {
NavigationLink("查看所有英雄", value: model)
}
.navigationDestination(for: Alliance.self) { model in
HeroListView()
.environment(model)
}
.navigationDestination(for: Hero.self) { hero in
HeroDetailView(hero: hero)
}
.navigationTitle(model.title)
}
}
}
从上面源代码中,我们可以看到几处有趣的地方:
- 子视图 HeroListView 和主视图 ContentView 都包含了 NavigationLink,但它们驱动状态的类型不一样(分别是 Hero 和 Model),这样不同的驱动源被清晰的区分开了;
- 放置 NavigationLink 和实际发生导航跳转的目标位置是分开的(通过 navigationDestination() 修改器),后者被放在了一起便于集中管理;
正是这些新的导航特性确保了导航逻辑代码清楚且集中,为日后自己或其它秃头码农来维护打下夯实基础:
以上就是第一种导航方法,即利用 NavigationStack + navigationDestination() 修改器方法来合作完成跳转功能。
2. NavigationSplitView 导航之“假象”
可能有的小伙伴们没太在意,SwiftUI 4.0 除了 NavigationStack 以外还新加入了另一个鲜为人知的导航器 NavigationSplitView!使用它,我们可以抛弃 navigationDestination() 去实现完全相同的导航功能。
我们对之前代码略作修改,看看能促成什么新奇的“玩法”:
struct HeroListView: View {
@Environment(Alliance.self) var model
@Binding var selection: Hero?
var body: some View {
VStack {
List(model.heros, selection: $selection) { hero in
NavigationLink(value: hero) {
HStack {
Text(hero.name)
.font(.headline)
Spacer()
Text("\(hero.power)")
.font(.subheadline)
.foregroundStyle(.gray)
}
}
}
}
}
}
struct ContentView: View {
@State var model = Alliance(title: "地球超级英雄", heros: [
.init(name: "大熊猫侯佩", power: 5),
.init(name: "孙悟空", power: 1000),
.init(name: "哪吒", power: 511)
])
@State private var selection: Hero?
var body: some View {
NavigationSplitView(sidebar: {
HeroListView(selection: $selection)
.environment(model)
.navigationTitle("新导航方式")
}, detail: {
if let selection {
HeroDetailView(hero: selection)
} else {
ContentUnavailableView("No Hero", systemImage: "person.badge.key.fill", description: Text("还未选中任何英雄!"))
}
})
}
}
可以看到,修改后的代码与之前有几处不同:
- 使用 NavigationSplitView 而不是 NavigationStack;
- 没有使用任何 navigationDestination() 修改器方法;
- 向 List 构造器传入了 selection 参数,以判断用户选择了哪个 Hero;
- 根据 selection 的值驱动 NavigationSplitView 构造器 detail 闭包完成跳转功能;
简单来说:当用户选中任意 Hero 后,通过设置 NavigationSplitView 构造器 detail 闭包中的视图,我们完成了导航机制。
代码执行结果和之前几乎完全相同,这么神奇!?
可惜,你们看到的全是“假象”!!!
3. 洞若观火:在 iPad 上的比较
其实,设置 NavigationSplitView 构造器 detail 闭包的内容原本并不会造成导航跳转,它的原意是在大屏设备上更方便的利用大尺寸屏幕来浏览内容。
编译上面 NavigationSplitView 的实现,在 iPad 上运行看看效果:
看到了吗?第二种导航机制在大屏设备上原本并不是真正用来导航跳转的,只是在 iPhone 等小屏设备上它的行为退化成了导航!
而第一种导航实现是彻头彻尾、如假包换的“真”导航:
到底哪种方法更好一些?小伙伴们自有“仁者见仁智者见智”的看法,欢迎大家随后的讨论。
至此,我们完成了文章开头的目标,棒棒哒!!!💯
4. 总结
在本篇博文中,我们在 SwiftUI 4.0 里通过两种不同方式实现了相同的子视图导航功能,任君选择。
感谢观赏,再会!8-)
SwiftUI 代码调试之都是“变心”惹的祸
0. 概览
这是一段非常简单的 SwiftUI 代码,我们将 Item 数组传递到子视图并在子视图中对其进行修改,修改的结果会立即在主视图中反映出来。
不幸的是,当我们修改 Item 名称时却发现不能连续输入:每次敲一个字符键盘都会立即收起并且原输入焦点会马上丢失,这是怎么回事呢?
在本篇博文中,您将学到以下内容:
- 不该发生的错误
- 无效的尝试:用子视图包装
- 寻根究底
- 解决之道
该问题这是初学者在 SwiftUI 开发中常常会犯的一个错误,不过看完本篇之后相信大家都会对此自胸有成竹!
废话不再,Let‘s fix it!!!;)
1. 不该发生的错误
照例我们先看一下源代码。
例子中我们创建了 Item 结构用来作为 Model 中的“真相之源”。
想要了解更多 SwiftUI 编程和“真相之源”奥秘的小伙伴们,请观赏我专题专栏中的如下文章:
注意,我们让 Item 遵守了 Identifiable 协议,这样可以更好的适配 SwiftUI 列表中的显示:
struct Item: Identifiable {
var id: String {
name
}
var name: String
var count: Int
}
let g_items: [Item] = [
.init(name: "宇宙魔方", count: 11),
.init(name: "宝石手套", count: 1),
.init(name: "大黄蜂", count: 1)
]
接下来是主视图 ItemListView,可以看到我们将 items 状态传递到子视图的 ForEach 循环中去了:
struct ItemListView: View {
@State var items = g_items
private var total: Int {
items.reduce(0) { $0 + $1.count}
}
private var desc: [String] {
items.reduce([String]()) { $0 + [$1.name]}
}
var body: some View {
NavigationStack {
// 子视图 ForEach 循环...
ForEach($items) { $item in
// 代码马上来...
}
VStack {
Text(desc.joined(separator: ","))
.font(.title3)
.foregroundStyle(.pink)
HStack {
Text("宝贝总数量:\(total)")
.font(.headline)
Spacer().frame(width: 20)
Button("所有 +1"){
for idx in items.indices {
guard items[idx].count < 100 else { continue}
items[idx].count += 1
}
}
.font(.headline)
.buttonStyle(.borderedProminent)
}
}.offset(y: 200)
}
}
}
最后是 ForEach 循环中的内容,如下所示我们用单个 item 的值绑定来实现修改其内容的目的:
ForEach($items) { $item in
HStack {
TextField("输入项目名称", text: $item.name)
.font(.title2.weight(.heavy))
Text("数量:\(item.count)")
.foregroundStyle(.gray)
Slider(value: .init(get: {
Double(item.count)
}, set: {
item.count = Int($0)
}), in: 0.0...100.0)
}
}
.padding()
这样一段看起来“天衣无缝”的代码为什么会出现在更改 Item 名称时键盘反复关闭、输入焦点丢失的问题呢?
2. 无效的尝试:用子视图包装
我们首先猜测是子视图中 Item 名称的更改导致了父视图的“冗余”刷新,从而引起键盘不正确被重置。
更多 SwiftUI 和 Swift 代码调试的例子,请观赏我专题专栏中的博文:
因为键盘所属的视图发生重建所以键盘本身也会被重置,那么如何验证我们的猜测呢?一种方式是使用如下的调试技术:
在这里我们假设病根果真如此。那么一种常用的解决办法立即浮现于脑海:我们可以将引起刷新的子视图片段包装在新的 View 结构中,这样做到原因是 SwiftUI 渲染器足够智能可以只刷新子视图而不是父视图中大段内容的更改。
更详细的原理请参考如下链接:
So,让我撸起袖子开动起来!
首先,将 ForEach 循环中编辑单个 Item 的 View 包装为一个新的视图 ItemEditView:
struct ItemEditView: View {
@Binding var item: Item
var body: some View {
HStack {
TextField("输入项目名称", text: $item.name)
.font(.title2.weight(.heavy))
Text("数量:\(item.count)")
.foregroundStyle(.gray)
Slider(value: .init(get: {
Double(item.count)
}, set: {
item.count = Int($0)
}), in: 0.0...100.0)
}
}
}
接着,我们将 ForEach 循环本身用一个新视图取代:
struct EditView: View {
@Binding var items: [Item]
var body: some View {
ForEach($items) { $item in
ItemEditView(item: $item)
}
.padding()
}
}
最后,我们所要做的就是将父视图 ItemListView 中的 ForEach 循环变为 EditView 视图:
NavigationStack {
EditView(items: $items)
// 其它代码不变...
}
再次运行代码...不幸的是问题依旧:
看来这并不是简单父视图“过度”刷新的问题,一定是有什么不应有的行为触发了父视图的刷新,到底是什么呢?
3. 寻根究底
问题一定出在 ForEach 循环里!
回顾之前 Item 的定义,我们用 Identifiable 协议满足 ForEach 对子项目唯一性的挑剔,我们用 Item.name 构建了 id 属性。
当 Model 元素遵守 Identifiable 协议时,应该确保在任意时刻所有 Item 的 id 属性值都是唯一的!从目前来看,上述代码在修改 Item 名称时并没有发生重名的情况(虽然可能发生),所以对于唯一性是没有问题的。
当然在实际代码中用户很可能会输入重复的 Item 名称,所以还是不可接收的。
不过,这段代码在这里只是作为例子来向大家展示解决问题的推理过程,所以不必深究 ;)
但是 id 还有另一个重要的特征:稳定性!
一般的,当 Identifiable 实体对象的 id 属性改变时,SwiftUI 会认为其不再是同一个对象,而立即刷新其所对应的视图界面。
所以,正如大家所看到的那样:每次用户输入 name 中的新字符时,键盘会被立即关闭焦点也随即丢失!
4. 解决之道
知道了问题原因,解决起来就很容易了。
我们只需要在 Item 生命周期中保证 id 的稳定性就可以了,这意味着不能再用 name 值作为 id 的“关联”值:
struct Item: Identifiable {
let id = UUID()
var name: String
var count: Int
}
如上代码所示,我们在 Item 创建时为 id 生成一个唯一的 UUID 对象,这可以保证两点:
- 任意时刻 Item 的唯一性;
- 任意 Item 在其生命周期中的稳定性;
有了如上修改之后,我们再来运行代码看看结果:
可以看到,现在我们可以毫无问题的连续输入 Item 的名字了,焦点不会再丢失,一切回归正常,棒棒哒!!!💯
总结
在本篇博文中,我们讨论了 SwiftUI 开发中一个非常常见的问题,并借助一步步溯本回原的推理找到症结根本之所在,最后一发入魂将其完美解决!相信小伙伴们都能由此受益匪浅。
感谢观赏,再会!8-)
Swift 和 Python 两种语言中带关联信息错误(异常)类型的比较
0. 概览
如果我们分别在平静如水、和谐感人的 Swift 和 Python 社区抛出诸如“Python 是天下最好的语言...” 和 “Swift 是宇宙第一语言...”之类的言论会有怎样的“下场”?
我们并不想对可能发生的“炸裂”景象做出什么预测,也无意比较 Swift 与 Python 的孰强孰弱。我们只是想聊聊 Swift 和 Python 语言中一个非常有趣的话题:看看谁实现带关联信息的错误(异常)类型更优雅?
在本篇博文中,您将了解到如下内容:
- “抛出?还是不抛出错误?这是个问题!”
- 带关联信息的错误
- Python 中的实现
- Swift 中实现
- 那么问题来了:谁更优雅呢?
还等什么呢?Let’s go!!!;)
1. “抛出?还是不抛出错误?这是个问题!”
在秃头码农们辛勤操劳的每一天都会遇到多如牛毛的代码错误,俗话说得好:“事无常形,人无完人”,没有错误的代码只能是还没有写出来的代码。
所幸的是几乎所有语言均对错误做出了良好和友善的支持,Swift 与 Python 自然也不例外!
在 Swift 中我们把代码能够抛出的某种“不安分”的东东称为错误(Error),而在 Python 中这被称之为异常(Exception)。其实从这种意义上来说它们是同一种东西,所以在本篇博文中统统称之为错误。
当函数或方法出现“不按常理出牌”的结果时,我们就有必要抛出一个错误。比如除以零:
enum AppError: Error {
case divisionByZero
}
func div(dividend: Int, divisor: Int) throws -> Int {
guard divisor != 0 else {
throw AppError.divisionByZero
}
return dividend / divisor
}
上面是 Swift 中对此的一个实现,在 Python 中有类似的表达:
class DivisionByZero(Exception):
pass
def div(dividend, divisor):
if divisor == 0:
raise DivisionByZero()
return dividend / divisor
2. 带关联信息的错误
在某些情况下,我们希望抛出的错误中携带一些有用的信息,这种错误称之为:带关联信息的错误。
比如当用户输入的文本太短不符合要求时,我们希望进一步了解到用户输入文本的实际长度以及最短需要的长度。
在 Swift 中我们可以通过枚举关联类型来实现带关联信息的错误,类似的在 Python 中我们可以用异常类的附加实例属性来达到这一相同目的。
关于更多 Swift 中枚举使用的奥秘,请小伙伴们移步我的专题视频观看:
如果小伙伴们还想进一步学习 Swift 语言的开发技巧,可以到我的专题专栏中继续研究:
下面我们就来分别看看它们的实现吧!
3. Python 中的实现
在 Python 中可以简单的为自定义异常类增加实例属性变量:
class TextTooShortException(Exception):
def __init__(self, actual, min):
super().__init__(self)
self.actual = actual
self.min = min
MIN_LEN = 3
try:
text = "wo"
if len(text) < MIN_LEN:
raise TextTooShortException(len(text), MIN_LEN)
except TextTooShortException as error:
print("错误:文本太短({}),希望不小于 {} 个字符!".format(len(text), MIN_LEN))
如上代码所示,我们创建了一个自定义异常类 TextTooShortException,其中分别用 actual 和 min 实例属性变量来存放输入文本实际和最短所需的长度。
我们看看运行结果:
4. Swift 中实现
在 Swift 中对于错误这种不需要实例化的对象,一般可以用枚举来表示(当然也可以用 struct,这要分情况而定)。
对于带关联信息的错误,我们只需要创建带关联类型的枚举元素即可:
enum MyError: Error {
case textTooShort(actual: Int, min: Int)
}
let text = "wo"
do {
if text.count < MIN_LEN {
throw MyError.textTooShort(actual: text.count, min: MIN_LEN)
}
} catch let my_error as MyError {
if case MyError.textTooShort(let actual, let min) = my_error {
print("输入文字长度(\(actual))太短了,不能小于(\(min))")
} else {
print("其它 MyError: \(my_error)")
}
} catch {
print("Other ERR: \(error)")
}
在上面的代码中,我们创建了一个 MyError 类型的错误,并添加了一个 textTooShort 枚举子项。其中我们为其嵌入了两个 Int 值作为关联类型,分别用来表示 actual 和 min 值。
以下是 Playground 中的运行结果:
5. 那么问题来了:谁更优雅呢?
看过上面两种实现,各位小伙伴们可能都会有自己的考量。
在 Swift 中使用类似于命名元组的枚举关联类型,显得更轻量级。表达 Error 这一概念无需动用 Class 这一重型武器,一个简单的值类型(enum)足以!
在 Python 中的实现也很简单,不过使用类来作为异常的载体显得更加“厚重”,更加中规中矩。
平心而论:Python 在捕获并处理异常时更加简洁,而 Swift 在定义错误时更轻量级,可惜关联类型枚举在错误解析时比较拖垮。
不过,谁说在 Swift 中不能用 Python 的方式自定义错误呢?;)
struct TextTooShortError: Error {
var actual: Int
var min: Int
}
let text = "wo"
do {
if text.count < MIN_LEN {
throw TextTooShortError(actual: text.count, min: MIN_LEN)
}
} catch let tooShortError as TextTooShortError {
print("输入文字长度(\(tooShortError.actual))太短了,不能小于(\(tooShortError.min))")
} catch {
print("Other ERR: \(error)")
}
总结
在本篇博文中,我们讨论了在 Swift 和 Python 两种语言中对于带关联信息的错误(异常)类型是如何实现的这一话题,并对哪种实现更优雅给出笔者自己的感悟。
感谢观赏,再会!8-)
ruby、Python 以及 Swift 语言关于 “Finally” 实现的趣谈
0. 概览
结构化代码语义是任何语言入门之必备基本功,想写出“意大利面条”似的美味代码么?直接干就对了!
虽然上面有些“话糙理不糙”,但不可否认的是现今几乎所有高级语言都对代码结构化语义提供了良好的支持。入门码农们的第一课都是先从顺序执行、跳转、条件走偏开始学起。
在顺序执行的结构化代码中,如果之前申请了资源,怎么确保随后无论发生什么事它们都可以被正确释放呢?
在本篇博文中,您将了解到如下内容:
- 什么是 “Finally” ?
- 从 ruby 谈起
- Python 中的实现
- “格格不入”的 Swift
- 轻量级 Finally 语法糖
有点“兴奋”了吗?那还等什么?
Let’s go!!!;)
1. 什么是 “Finally” ?
Finally 这个词很形象!它像一个会发出声音的感叹词:谢天谢地,这一切终于结束了!你终于来了!
一段代码在执行中可能出现各种意外情况:参数错误、文件损坏、有异常抛出、处理器罢工、新买的 M3 Max 纯黑丝 MBP 爱机变心了...
如果我们在代码中创建了一些资源,怎么确保在上面这些情况发生时它们还可以被正确释放掉呢?
#!/usr/bin/ruby
file = File.open("lazy.rb")
r = 1..63
for a in r do
// 可能的抛出异常点:内存不足!
for b in r do
// 可能的抛出异常点:处理器罢工!
for c in r do
// 可能的抛出异常点:爱机抑郁!
for d in r do
// 可能的抛出异常点:...
if a + b + c + d == 63
rlt = a*b + b*c + c*d
if rlt >= max
max = rlt
file.puts(max)
end
end
end
end
end
end
print("max is #{max}\n")
比如在上面的 ruby 代码里,我们崎岖前行中的代码可能会遇到各种“艰难险阻”,那么一旦“祸从天降”如何保证开头的 file 对象占据的资源被释放呢?
上帝说:要有 Finally ! 于是乎,Finally 机制应运而生了!
Finally 就是一位“超级保姆”、一位技术高超的 Cleaner!不管之前是多么的一塌糊涂,只要一经它手收拾就会变得的“窗明几净”,任何之前残留的痕迹都将一去不复返,正所谓:“两岸猿声啼不住,轻舟已过万重山!呜呼哉!”
Finally 是结构化开发中一道靓丽的“屏障”,不管发生神马,Finally 块中的代码一定会被执行!(你们确定一定以及肯定?)
2. 从 ruby 谈起
首先,我们来看看 ruby 对 Finally 的支持。(不知为何看到 ruby 总有种要发财的赶脚...金银财宝快过来...)
ruby 中使用 ensure 语句块来支持 Finally 的语义:
begin
file = File.open("lazy.rb")
# 其它可能会抛出异常的其它操作...
ensure
file.close
puts("文件已被释放!")
end
看到了吗?我就是不缩进!!!Python 你咬我啊!:)
当然,我们还可以加入 rescue 子句来处理异常:
begin
file = File.open("lazy.rb")
# 其它可能会抛出异常的其它操作...
rescue
puts("ERR: #{$!}")
ensure
file.close
puts("文件已被释放!")
end
3. Python 中的实现
不知 ruby 和 Python 这哥俩到底是谁“抄袭”的谁,Python 对 Finally 的实现几乎与 ruby 毫无二致:
f = None
try:
f = open("lazy.txt")
# 可能有其它“惊喜”等待着我们,但我们不怕!:)
except IOError:
print("无法打开 lazy.txt 文件")
except KeyboardInterrupt:
print("用户终止了输入,可能是因为着急去交水费!")
finally:
if f:
f.close()
print("成功关闭文件!")
从如上代码中可以发现,除了将 rescue 变为 except、把 ensure 改为 finally 一切几乎都没啥区别。
当然,还有层层缩进仿佛在诉说着什么...
4. “格格不入”的 Swift
如果说 ruby 和 Python 有点像兄弟俩,那么 Swift 更像一只“莫名其妙”的燕子(这句话更莫名其妙)。
有点“出乎意料”的是,在 Swift 语言中我们使用 defer 关键字来保证一段语句块中必须被 Finally 清理掉的内容:
func read() -> String {
let file = File.open(...)
defer {
file.close()
}
do {
return try file.readlines.join("\n")
} catch {
print("ERR: \(error)")
}
}
如上代码所示,不管最后代码流以何种方式离开 read() 方法,defer 语句块中的内容都会被执行!
5. 轻量级 Finally 语法糖
其实对于简单的 Finally 实现,ruby 和 Python 都支持更便捷的语法。
比如在 ruby 中我们可以在文件打开的附庸闭包中处理文件,当闭包结束时文件对象自然“灰飞烟灭”:
File.open("lazy.rb") {|file| file.readlines}
而在 Python 中也有对应的 with 语句欲“乘风而去”:
with open("lazy.txt") as file:
for line in file:
print(line, end='')
在上面两段代码片段中,被打开的 file 永远无法逃出其最终的宿命,一切尽在不言中!
那么,看到这里小伙伴们对哪种语言中 Finally 的实现更情有独钟呢?
喜欢这种天马行空无厘头写作风格的小伙伴们,请到如下链接观赏更“刺激”的文章:
请做好准备不要惊掉下巴哦!
总结
在本篇博文中,我们讨论了 ruby、Python 和 Swift 语言中对 Finally 机制的不同实现,并用诸多代码片段来演示它们实际的使用,小伙伴们值得拥有!
感谢观赏,再会!8-)
Swift 抛砖引玉:从数组访问越界想到的“可抛出错误”属性
0. 概览
了解 Swift 语言的小伙伴们都知道,我们可以很方便的写一个可能抛出错误的方法。不过大家可能不知道的是在 Swift 中对于结构或类的实例属性我们也可以让它抛出错误。
这称之为实效只读属性(Effectful Read-only Properties)。
那么,这种属性怎么创建?并且到底有什么用处呢?
在本篇博文中,您将学到如下内容:
- 什么是“实效只读属性”
- 怎么创建“实效只读属性”?
- 数组访问越界是个头疼的问题
- 拯救者:抛出错误的“实效只读属性”
- 更进一步
- 八卦一下:ruby 中更优雅的实现
相信看完文本后,小伙伴们的武器库中又会多了一件“杀手锏”!
那还等什么呢?Let‘s go!!!;)
1. 什么是“实效只读属性”
“实效只读属性” 英文名称为 Effectful Read-only Properties,它是 Swift 5.5+ 中对计算属性和下标操作(computed properties and subscripts)的增强功能。
在 Swift 5.5 之前,我们只能创建异步或可抛出错误的方法(或函数),而无法构建与此类似的实例属性。
对于有些情况,一个“异步”属性可以帮上大忙!
actor AccountManager {
// 注意: `getLastTransaction` 方法若在 AccountManager 外部调用将会“升级”为一个异步方法
func getLastTransaction() -> Transaction { /* ... */ }
func getTransactions(onDay: Date) async -> [Transaction] { /* ... */ }
}
class BankAccount {
private let manager: AccountManager?
var lastTransaction: Transaction {
get {
guard let manager = manager else {
throw BankError.NoManager
// ^~~~~ 错误: 普通计算属性中不能抛出错误!
}
return await manager.getLastTransaction()
// ^~~~~ 错误: 普通计算属性中不能调用异步方法
}
}
}
如上代码所示:在 BankAccount 类的 lastTransaction 实例属性访问过程中可能会抛出错误,并且需要等待返回一个异步方法的结果。这对于以往的实例属性来说是“不可能的任务”!
诚然,我们可以将 lastTransaction 实例属性变为一个方法:
class BankAccount {
private let manager: AccountManager?
//var lastTransaction: Transaction {}
func getLastTransaction() async throws -> Transaction {
guard let manager = manager else {
throw BankError.NoManager
}
return await manager.getLastTransaction()
}
}
但这显然有点“画蛇添足”的意味。
幸运的是, 倾听到了秃头码农们的殷切呼唤,从 Swift 5.5 开始我们便有了上面的“实效只读属性”。
想进一步了解“实效只读属性”的小伙伴们可以到 Swift 语言进化提案(swift-evolution proposals)中观赏更详细的内容:
2. 怎么创建“实效只读属性”?
从 Swift 5.5+ 开始,我们可以在实例属性的只读访问器(get)上应用 async 或 throws 关键字(效果说明符):
class BankAccount {
// ...
var lastTransaction: Transaction {
get async throws { // <-- Swift 5.5+: 效果说明符(effects specifiers)!
guard manager != nil else {
throw BankError.notInYourFavor
}
return await manager!.getLastTransaction()
}
}
subscript(_ day: Date) -> [Transaction] {
get async { // <-- Swift 5.5+: 与上面类似,我们也可以在下标的读操作上应用效果说明符。
return await manager?.getTransactions(onDay: day) ?? []
}
}
}
如上代码所示,我们不但可以在实例属性上应用 async 和 throws 效果说明符(effects specifiers),同样也可以在类或结构下标操作的读访问器上使用它们。
现在,我们可以这样访问 BackAccount#lastTransaction 实例属性和下标操作:
extension BankAccount {
func meetsTransactionLimit(_ limit: Amount) async -> Bool {
return try! await self.lastTransaction.amount < limit
// ^~~~~~~~~~~~~~~~
// 对该实例属性的访问是异步且可能抛出错误的!
}
}
func hadWithdrawlOn(_ day: Date, from acct: BankAccount) async -> Bool {
return await !acct[day].allSatisfy { $0.amount >= Amount.zero }
// ^~~~~~~~~
// 同样的,下标读操作也是异步的
}
3. 数组访问越界是个头疼的问题
秃头码农们都知道,在 Swift 中对于数组访问常常出现下标越界的情况。它会引起程序立即崩溃!
我们时常会想:如果在数组访问越界时抛出一个可捕获的错误就好了!
在过去,我们可以写一个新的“下标访问”方法来模拟这一“良好愿望”:
enum Error: Swift.Error {
case outOfRange
}
extension Array where Element: Equatable {
func getElemenet(at: Array.Index) throws -> Element {
guard at < endIndex else {
throw Error.outOfRange
}
return self[at]
}
}
do {
let ary = Array(1...100)
_ = try ary.getElemenet(at: 10000)
} catch let error as Error {
print("ERR: \(error.localizedDescription)")
}
但这种 .getElemenet(at:) 的“丑陋”写法真是让人“是可忍孰不可忍”!
不过,从 Swift 5.5 一切开始变得不同了。
4. 拯救者:抛出错误的“实效只读属性”
看到这,聪明的小伙伴们应该早就知道如何应对了。
我们可以使用 Swift 5.5 中的“实效只读属性”来“完美的”完成任务:
enum Error: Swift.Error {
case outOfRange
}
extension Array where Element: Equatable {
subscript(index: Array.Index) -> Element {
get throws {
guard index < endIndex else {
throw Error.outOfRange
}
var temp = self
temp.swapAt(0, index)
return temp.first!
}
}
}
do {
let ary = Array(1...100)
_ = try ary[10000]
} catch let error as Error {
print("ERR: \(error.localizedDescription)")
}
如上代码所示:我们使用可抛出错误的下标读访问器为 Array 下标操作“添妆加彩”。略微遗憾的是,我们需要在数组新下标操作中调用原来的下标操作,这对于结构(struct)类型的 Array 来说好似“难于上青天”,所以我们采用的是迂回战术。
对于类支持的类型来说,我们可以使用 Objc 存在的 Swizz 技术来得偿所愿。
在文章最后,我们将会看到同样问题在 ruby 语言中实现的是何其优雅。
5. 更进一步
在数组的下标访问中抛出错误还不算完,我们还可以利用 Swift 枚举的关联类型为错误添加进一步的信息。
想要了解更多 Swift 枚举的小秘密,请小伙伴们移步如下文章观赏:
更多 Swift 语言知识的系统介绍,请移步我的专栏文章进一步观看:
下面,我们为之前的越界错误增加关联类型,分别表示当前越界的索引和数组总长度:
enum Error: Swift.Error {
case outOfRange(accessing: Int, end: Int)
}
接着,修改抛出错误处的代码:
subscript(index: Array.Index) -> Element {
get throws {
guard index < endIndex else {
throw Error.outOfRange(accessing: index, end: count)
}
var temp = self
temp.swapAt(0, index)
return temp.first!
}
}
最后,是错误捕获时的代码:
do {
let ary = Array(1...100)
_ = try ary[10000]
} catch let error as Error {
if case Error.outOfRange(let accessing, let end) = error {
print("ERR: 数组访问越界[试图访问:\(accessing),数组末尾:\(end)]")
}
}
现在,当发生越界错误时我们可以清楚的知道事情的来龙去脉了,是不是了很赞呢:
6. 八卦一下:ruby 中更优雅的实现
上面我们提到过 Swift 结构类型的方法“重载”(结构没有重载之说,这里只是比喻)无法再使用“重载”前的方法了。
但是在某些动态语言中,我们可以非常方便的使用类似于“钩子”机制来访问旧方法,比如 ruby 里:
#!/usr/bin/ruby
class Array
alias :subscript :[]
def [](index)
puts "试图访问索引:#{index}"
subscript(index)
end
end
a = [1,2,3]
puts a[1]
如上所示,我们使用别名(alias)机制将原下标操作方法 :[] 用 :subscript 名称进行“缓存”,然后在新的 :[] 方法中我们可以直接调用旧方法。
运行结果如下所示:
试图访问索引:1
2
Swift 什么时候有这种“神奇”的能力呢?让我们翘首以盼!
总结
在本篇博文中,我们讨论了 Swift 5.5 中新增的“实效只读属性”(Effectful Read-only Properties),它有哪些用途?怎么用它来解决 Swift 数组访问越界的“老问题”?最后,我们用 ruby 代码举了一个更优雅的实现。
感谢观赏,再会!8-)
消失的它:摆脱 SwiftUI 中“嵌入视图数量不能超过 10 个”限制的秘密
概览
SwiftUI 带来了描述性界面布局的新玩法,让我们可以超轻松的创建复杂应用界面。但是在早期 SwiftUI 中有一个“著名”的限制大家知道么?那就是 @ViewBuilder 中嵌入子视图数量不能超过 10 个!
不过,从 Swift 5.9 开始这一“桎梏”已悄然消失的无影无踪。
这个限制为什么已然烟消云散?早期的限制又是如何产生的呢?
在本篇博文中,您将学到以下内容:
- 不能超过 10 个,你是来逗我的吗?
- “值与类型形参包”
- SwiftUI 的新实现
- 为何不能用泛型数组?
想知道事件的前因后果么?那还等什么呢?
Let‘s go!!!;)
1. 不能超过 10 个,你是来逗我的吗?
在 Swift 5.5 中增加了 some 关键字,让 SwiftUI 能够用简洁类型来描述海量复合视图。这还不算完,可能 觉得视图组合的手法还是太麻烦,随即又祭出 @ViewBuilder 来进一步简化 SwiftUI 的视图构建。
其实,SwiftUI 视图的 body 计算属性已被 @ViewBuilder 默默修饰着,我们能够轻松自在,全靠 @ViewBuilder 为我们负重前行:
@ViewBuilder var body: Self.Body { get }
更多 ViewBuilder 实现细节的讨论,请小伙伴们移步 Swift 官方社区观赏:
@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 语言超出了本文的范畴,感兴趣的小伙伴们可以移步下面的链接观赏进一步内容:
想了解更多 Swift 语言开发的知识,小伙伴们可以到我的专题专栏中进行系统性学习:
而对于简单 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+ 开始这一限制为什么最终消失了?
感谢观赏,再会!8-)