Section 19 : Combine and Edit Data - 使用 Combine 和编辑数据(12’38")
Learn how to manipulate external data with built-in functions.
学习使用内置函数处理外部数据。
1. 关于 Combine
Combine 是 Swift 中提供的框架,通过组合事件处理操作符自定义异步事件的处理。
Combine框架提供了一个声明性的 Swift API,用于处理随时间变化的值。这些值可以表示多种异步事件。Combine 声明*publisher(发布者)以公开随时间变化的值,而subscriber(订阅者)*从发布者接收这些值。
Publisher
协议声明了一种类型,这种类型可以随时间传递一系列值。Publisher 用“操作符(operator)”作用于从上游 publisher 那里收到的值并重新发布它们。- 在发布链的末端,
Subscriber
在接收元素时对其进行操作。Publisher 仅在 subscriber 显式请求时才发出值。这将使 subscriber 代码参与控制从所连接的发布者接收事件的速度。
某些基础类型通过发布者暴露功能,包括Time
、NotificationCenter
和URLSession
等。Combine 还为符合键值观察的任何属性提供一个内置发布程序。
可以组合多个发布者的输出并协调它们的交互。例如,可以从 Text Field 的发布者订阅更新,并使用文本执行URL请求。然后,可以使用另一个发布者来处理响应,并用来更新应用程序。
通过采用Combine,可以将事件处理代码集中起来,并消除诸如嵌套闭包和基于约定的回调等麻烦的技术,从而使代码更易于阅读和维护。
2. 关于 ObservableObject
ObervableObject 是一个协议,表示在对象更改之前由发布者发出的对象类型。
默认情况下,ObservableObject
对象合成一个 objectWillChange
发布者,后者会将其使用 @Published
声明的属性发生变化之前发布变动的值。
class Contact: ObservableObject {
@Published var name: String
@Published var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
func haveBirthday() -> Int {
age += 1
return age
}
}
let john = Contact(name: "John Appleseed", age: 24)
john.objectWillChange.sink { _ in print("\(john.age) will change") }
print(john.haveBirthday())
// Prints "24 will change"
// Prints "25"
3. 定义一个类来管理更新信息
(1)新建文件 UpdateStore。保留第一行代码,然后删除其他内容。
(2)引入 Combine
(3)声明一个 类 Class
,名字为 UpdateStore,类型为 ObservableObject
。
(4)在类中,使用 @Published
声明一个 publisher,名字为 updates,类型是 Update 数组,默认值是 updateData。
import SwiftUI
import Combine // 导入 Combine
class UpdateStore: ObservableObject { // 定义用于存储更新信息的类
@Published var updates: [Update] = updateData // 声明publisher,类型为 Update 数组,默认值为 updateData
}
4. 使用 store 来管理更新信息列表
(1)在 UpdateList 文件的 body 之前,声明一个 UpdateStore 的对象 store,注意这里使用的声明关键字@ObservedObject
,与类定义中的类型关键字是不同的。
@ObservedObject var store = UpdateStore() // 声明 UpdateStore 类对象
(2)修改 List 组件中的参数,使用刚刚声明的对象。
List(store.updates) { update in
// 代码略
}
现在预览一下,可以看到显示正常,但实际上数据已经由 store 接管了。
5. 实现新增更新信息
(1)先要在 body 外面编写一个函数,实现新增更新信息的功能。
// 增加一条新的更新信息
func addUpdate() {
store.updates.append( // 使用数组追加函数
Update(image: "WXAvatar", title: "新增课程", text: "课程详细信息", date: "1月1日")
)
}
(2)为 List 组件添加 navigationBarItems 修饰,添加按钮调用上面的函数。
List(store.updates) { update in
// 代码略
}
.navigationBarTitle(Text("课程更新信息")) // 导航视图的标题
.navigationBarItems(leading: Button(action: addUpdate){ // 在导航标题上方增加按钮,单击调用 addUpdate 函数
Text("增加")
})
预览一下,可以看见在标题的上方左侧(因为用的是 leading
,右侧貌似应该用 trailing
)出现一个按钮,单击后列表会增加一项新的更新信息。
6. 完善列表功能
(1)现在来实现滑动删除功能。但是这个功能没法在 List 中实现。所以要使用 ForEach 将导航项目重复列出,再用 List 包裹(保留导航标题和新增功能)。对应代码修改后如下:
NavigationView { // 导航视图
List {
ForEach(store.updates) { update in // 使用 ForEach 遍历 store
NavigationLink(destination: UpdateDetail(update: update)) { // 导航项目
// 代码略
}
}
}
.navigationBarTitle(Text("课程更新信息")) // 导航视图的标题
.navigationBarItems(leading: Button(action: addUpdate){ // 新增按钮
Text("新增")
})
}
(2)为 NavigationLink 组件增加 onDelete 修饰,传入当前元素的数组下标(索引),执行移除,这样数据就被删除了。
NavigationLink(destination: UpdateDetail(update: update)) { // 导航项目
// 代码略
}
.onDelete { index in
self.store.updates.remove(at: index.first!)
}
这里要注意的是,index.first
可能为 nil
(空值),所以需要使用!
进行强行解包。这么做有一定的风险,不过这里貌似肯定是有值的,所以不必额外编写那些判断是否为空的处理。(源码中说 onDelete 如果传了空值意味着删除无效)
预览一下,现在可以看到向左滑动更新信息会出现删除按钮,如果一直滑动到最左侧,则会直接删除。但是那个删除按钮写的英文,如果是要中文……本地化构建吗?
- 让内置的文字变为中文
(1)打开项目导航栏,快捷键是
⌘ + 1
(2)单击第一行,即项目配置文件
(3)在编辑器单击 Info 标签
(4)在
Custom iOS Target Properties
列表的key
中找到Localization native development region
,单击对应的value
列后面的箭头,选择China
。(5)无论是预览还是在模拟器上的运行都显示为中文的删除了。注意,模拟器可能需要先设定语言为中文哦,对了,要先把
SceneDelegate.swift
中的ContentView()
改成UpdateList()
,不然模拟器上显示的是 ContentView。别忘记测试完了要改回来。这肯定不是一劳永逸的方法,如果需要 app 同时支持多种语言,一定不是这么简单粗暴的处理,看看后面是否有讲
7. 增加编辑按钮
与增加新增按钮一样,只需要在 navigationBarItems 中添加一个参数,trailing(好像刚才我还在说这个),记得在leading的新增按钮结束花括号后面先加上一个逗号,。使用内置的函数 EditButton() 就可以创建好这个按钮。因为刚才已经改了开发的语言,所以预览看见的是编辑而不是edit。
.navigationBarItems(leading: Button(action: addUpdate){
Text("新增")
}, trailing: EditButton())
单击编辑按钮,会发现已经内置好了删除功能。实际上,这是 NavigationLink 的 onDelete 在起作用。注释了这个修饰,功能就没有了。这里能体会到的是这个编辑按钮能检测到 onDelete 或者 onMove 修饰并且相应增加了界面上需要的组件。好强大。
8. 实现拖动排序
在 onDelete 下面,给NavigationLink组件再增加一个修饰 onMove。
.onMove { (source: IndexSet, destination: Int) in
self.store.updates.move(fromOffsets: source, toOffset: destination)
}
预览一下,单击编辑按钮,可以看见每个列表项的行尾多一个拖动按钮,按住可以拖拽列表项到想要的位置。
我测试发现最后一个飘到屏幕外面的不大好用啊,新增若干个之后,还是屏幕最下方的项目拖拽有问题。删除功能貌似没事,这是为什么?也不是总无效……六脉神剑啊(时灵时不灵)……就是可点击的范围非常小,后面再研究吧。
9. 修改 HomeView
找到 HomeView 中头像边上的那个按钮组件,将 sheet 修饰器中调用的 ContentView() 修改为 UpdateList()。
本节小结
- Combine 框架
- 遵循 ObservableObject 的类通过 @Published 定义 publisher,使用这个类的对象要用 @ObservedObject 声明
- 通过遍历数组,在 NavigationLink 上展示所有的元素,再将其用 List 包裹起来。
- 对每一个元素都可以使用数组的方法修饰,实现删除和移动等功能
- 对包裹 NavigationLink 的 List 使用 NavigationBarItems,增加导航栏按钮。
- 使用内置的函数实现导航栏按钮功能(当然也可以自定义其他功能)
接下来
使用预览和测试