SwiftUI 和 Combine 是在 WWDC 2019 上宣布的两个新框架。这两个框架在 WWDC 2019 上受到了很多关注,这些技术在会议上的数量就是证明。
SwiftUI 被引入为
一种革命性的新方法,可以更快地构建更好的应用程序。
组合被描述为
随着时间的推移处理值的统一声明框架
在初始版本和现在(2020 年 5 月,Swift 5.2)之间,发生了一些变化。任何刚接触 SwiftUI 和 Combine 的人,如果看过 WWDC 视频,可能会对这两个框架如何协同工作有一些疑问。
Combine 定义了两个接口:Publisher 和 Subscriber。发布者向订阅者发送事件。请参见下面的序列图。
如果您在 SwiftUI 中启动应用程序,然后添加 combine,则不会提及 Publisher 或 Subscriber,这是使用 Combine 所需的两个主要参与者。考虑下面这个非常简单的示例应用程序。
import SwiftUI
import Combine
import SwiftUI
final class ActorViewModel: ObservableObject {
var name : String
private var imageUrl : URL?
//@Published
private (set) var image : Image = Image(systemName: "photo") {
willSet {
DispatchQueue.main.async {
self.objectWillChange.send()
}
}
}
init(name: String, imageUrl: URL?) {
self.name = name
self.imageUrl = imageUrl
self.fetchImage()
}
private func fetchImage() {
guard nil != self.imageUrl,
String() != self.imageUrl!.absoluteString else { return }
let task = URLSession.shared.dataTask(with: self.imageUrl!) { (data, response, error) in
guard nil == error , nil != response, nil != data,
let uiImage = UIImage(data: data!) else { return }
self.image = Image(uiImage: uiImage)
}
task.resume()
}
}
struct ContentView: View {
@ObservedObject var actor : ActorViewModel
var body: some View {
HStack {
actor.image
.resizable()
.aspectRatio(contentMode: ContentMode.fit)
.frame(width: 60, height: 60)
Text(actor.name)
}
}
}
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
let actor = ActorViewModel(name: "Mark Hammill",
imageUrl: URL(string: "https://m.media-amazon.com/images/M/MV5BOGY2MjI5MDQtOThmMC00ZGIwLWFmYjgtYWU4MzcxOGEwMGVkXkEyXkFqcGdeQXVyMzM4MjM0Nzg@._V1_.jpg"))
return ContentView(actor: actor)
}
}
通过画布预览的应用程序如下所示:
该应用使用列表视图来显示演员的姓名和图像。只有两个类需要考虑:
- ContentView -- SwiftUI 视图子类
- ActorViewModel -- ContentView 的数据源(称为 ViewModel,因为它在 MVVM 中扮演 VM 的角色)
根据下面的类图,视图具有对参与者对象的引用。
虽然这个例子使用了Combine,但它并不是很明显。没有提及发布者或订阅者。怎么回事?
答案:查看类层次结构可以填补缺失的空白。下面的类图解释了全图(点击图片查看更详细)。
查阅 Apple 的文档提供了这些类型的定义:
-
ObservedObject:订阅可观察对象并在可观察对象发生更改时使视图无效的属性包装器类型。
-
ObservableObject:一种具有发布者的对象,在对象更改之前发出。默认情况下,ObservableObject 会合成一个 objectWillChange 发布者,该发布者会在其任何 @Published 属性更改之前发出更改后的值。
-
objectWillChange:在对象更改之前发出的发布者。
-
PassthroughSubject:向下游订阅者广播元素的主题。作为 Subject 的具体实现,PassthroughSubject 提供了一种方便的方式来将现有的命令式代码适应于 Combine 模型。
首先,考虑@ObservedObject 的含义。这是一个属性包装器。属性包装器减少了代码重复,并在声明隐藏属性存储和定义方式的属性时允许使用简洁的语法。在这种情况下,“被观察对象”是观察另一个对象的属性。
换句话说,该属性是一个订阅者(来自组合框架)。 Actor 是(通过使用属性包装器)是一个订阅者,它订阅一个 Publisher,但是在这种情况下,Publisher 是什么?
“可观察对象”本身并不是发布者,而是拥有发布者。 ActorViewModel 符合ObservableObject 协议。通过这样做,它通过扩展(框架在 ObservableObject 协议上提供)提供了一个名为 objectWillChange 的发布者属性。这个objectWillChange 属性属于PassthroughSubject 类型,它是Publisher 协议的具体类型。 passthrough 主题有一个名为send 的属性,它是一种发布者方法,用于向任何订阅者发送数据。所以名为“objectWillChange”的属性是Publisher。
概括地说,Subscriber 是 ContentView 类中名为 actor 的属性,Publisher 是 ActorViewModel 类中名为 objectWillChange 的属性。订阅者 订阅 到 Publisher 的需求如何? “@ObservedObject”属性包装器本身就是订阅者,因此它必须订阅发布者。但是视图如何发现发送给订阅者的更改?这是由我们从未见过的 SwiftUI 框架处理的。
要点:我们无需担心将视图订阅到 Publisher。另一方面,我们确实需要确保发布者在某些事情即将发生变化时通知订阅者。当从远程服务器获取图像,并将数据转换为图像对象时,我们调用objectWillChange.send() 通知视图。一旦订阅者从发布者那里收到关于某些内容即将发生变化的通知,它就会使视图无效(这会导致视图自身重绘)。
总结
SwiftUI 使用 ObservedObject PropertyWrapper 的方式并没有从表面上泄露出组合甚至存在于等式中的事实。但是通过检查 ObservedObject 和 ObservableObject,可以发现底层的 Combine 框架以及设计模式:
订阅者 --> 订阅发布者 --> 然后发布者发布更改 --> 订阅者收到的更改
参考文献: