switchToLatest
本节包含了Combine 中一些更复杂的组合运算符,比如switchToLatest
switchToLatest很复杂但非常有用。在Apple文档中,它的说明是
Republishes elements sent by the most recently received publisher.
翻译过来是
重新发布最近收到的发布者发送的元素。
此运算符与上游发布者合作,将数据流扁平化,使其看起来好像来自单个数据流。它在新发布者到达时切换内部发布者,但为下游订阅者保持外部发布者不变。
当这个运营商从上游发布者那里收到一个新的发布者时,它会取消之前的订阅。使用此功能可以防止早期发布者执行不必要的工作,例如创建网络请求发布者频繁更新用户界面发布者。
同时,最重要一点,只有发布发布者的发布者,才可以使用switchToLatest。也就是当Publisher的Output也是一个Publisher类型时。
我们先看下它的弹珠图
然后在playground加入如下代码,针对这个弹珠图进行重现:
// 1
let publisher1 = PassthroughSubject<Int, Never>()
let publisher2 = PassthroughSubject<Int, Never>()
let publisher3 = PassthroughSubject<Int, Never>()
// 2
let publishers = PassthroughSubject<PassthroughSubject<Int, Never>, Never>()
// 3
publishers
.switchToLatest()
.sink(
receiveCompletion: { _ in print("Completed!") },
receiveValue: { print($0) }
)
.store(in: &subscriptions)
// 4
publishers.send(publisher1)
publisher1.send(1)
publisher1.send(2)
// 5
publishers.send(publisher2)
publisher1.send(3)
publisher2.send(4)
publisher2.send(5)
// 6
publishers.send(publisher3)
publisher2.send(6)
publisher3.send(7)
publisher3.send(8)
publisher3.send(9)
// 7
publisher3.send(completion: .finished)
publishers.send(completion: .finished)
上述代码的注释部分分别为:
- 创建三个发布整数且错误类型为Never的 PassthroughSubject。
- 创建Output也是Publisher的 PassthroughSubject。例如,您可以通过它发送publisher1、publisher2 或publisher3。
- 在您的
publishers上使用switchToLatest。现在,每次通过publishers发送不同的发布者时,您都会切换到新的发布者并取消之前的订阅。 publishers发布第一个数据,数据是publisher1,然后publisher1发布数据,内容是1、2。publishers发送第二个数据,数据是publisher2,此时会取消对publisher1 的订阅。
然后,publisher1发布数据3,但它被忽略(因为publisher1的订阅已经被取消了!)。
随后publisher2发布数据4、5, 这两个数据是可以被接收的。publishers发布第三个数据,数据是publisher3,这时取消了publisher2 的订阅。和以前一样,您将 publisher2发布数据6 并被忽略,然后publisher3发送7、8 和9,这三个数据是可以被接收的。- 最后,publisher3发布一个完成事件,
publishers也发布一个完成事件。这将完成所有有效订阅
运行playground,得到如下结果:
1
2
4
5
7
8
9
Completed!
如果您不确定为什么这在实际应用中有用,请考虑以下场景:您的用户点击触发网络请求的按钮。紧接着,用户再次点击按钮,触发第二个网络请求。但是如何去掉挂起的请求,只使用最新的请求呢? switchToLatest 这时就会展示他的作用。
原书中的Demo我运行不能得到希望的结果,所以我重新用SwiftUI的MVVM模式重新写了一个Demo
我们试试这个例子:
struct SwitchToLeastView: View {
@StateObject var vm = ViewModel()
var body: some View {
VStack {
Button(action: {
vm.getUser()
}, label: {
Text("获取数据")
.padding()
.frame(maxWidth: .infinity)
.background(Color.blue)
.foregroundColor(.white)
.clipShape(RoundedRectangle(cornerRadius: 12))
.padding()
})
Text("(vm.user.id)")
Text(vm.user.name)
Text(vm.user.email)
}
}
}
首先我们新建一个SwiftUI文件,并绘制View中的一些内容,比如在一个VStack中,显示一个按钮,并显示一些文本框(Text),在MVVM中,我们还需要一个Model和一个ViewModel,所以我们需要再增加一些代码:
extension SwitchToLeastView {
// Model
struct User: Decodable {
var id: Int
var name: String
var email: String
init(id: Int = 0, name: String = "", email: String = "") {
self.id = id
self.name = name
self.email = email
}
}
// ViewModel
class ViewModel: ObservableObject {
// View刷新需要的数据
@Published var user = User()
// 用户点击按钮时,触发Combine流程的Publisher
var pub = PassthroughSubject<Void, Never>()
var cancellable = Set<AnyCancellable>()
init() {
pub
.map { _ in
// 将publisher的原数据,转换为Publisher
Service.shared.fetchUser()
}
// 取消上一次订阅,切换为最新一次的订阅,防止多次同样的网络请求
.switchToLatest()
.receive(on: RunLoop.main)
.sink { [weak self] user in
self?.user = user
}
.store(in: &cancellable)
}
// View的Button点击时调用
public func getUser() {
pub.send()
}
}
// 网络数据处理类
class Service{
// 设置为单例模式
static let shared = Service()
// 获取网络数据
func fetchUser() -> AnyPublisher<User, Never> {
let url = URL(string: "https://jsonplaceholder.typicode.com/users/1")!
return URLSession.shared.dataTaskPublisher(for: url)
.handleEvents(
receiveOutput: { _ in
print("request for user")
})
.map { $0.data }
.decode(type: User.self, decoder: JSONDecoder())
.replaceError(with: User())
.eraseToAnyPublisher()
}
}
}
关键部分我加上了注释,但我还是要整体的说下这个Demo的原理。
整个demo很简单,只有一个页面,页面上有一个按钮。当我们点击按钮时,会发送网络请求,请求返回的数据,显示在按钮下方。
当我们点击按钮时,我们通过ViewModel的pub,来手动发布数据,来触发Combine的流程(ViewModel的init()部分)。
这样做的原因,是因为switchToLatest的使用条件必须是发布者的发布数据必须也是publisher才可以,所以我们在pub上用map来将其原Output的数据转换为publisher,这个publisher是我们获取网络数据请求的publisher。
运行我们的Demo,并连续点击“获取数据”按钮,如果不使用switchToLatest的方式,则会每次点击按钮,都会打印出request for user,但当我们使用switchToLatest后,会发现只打印一次request for user,因为他帮我们取消了之前的订阅,只保留了最后一次。