第六章 Combine的组合操作符(switchToLatest)

1,102 阅读2分钟

switchToLatest

本节包含了Combine 中一些更复杂的组合运算符,比如switchToLatest
switchToLatest很复杂但非常有用。在Apple文档中,它的说明是

Republishes elements sent by the most recently received publisher.

翻译过来是

重新发布最近收到的发布者发送的元素。

此运算符与上游发布者合作,将数据流扁平化,使其看起来好像来自单个数据流。它在新发布者到达时切换内部发布者,但为下游订阅者保持外部发布者不变。
当这个运营商从上游发布者那里收到一个新的发布者时,它会取消之前的订阅。使用此功能可以防止早期发布者执行不必要的工作,例如创建网络请求发布者频繁更新用户界面发布者。

同时,最重要一点,只有发布发布者的发布者,才可以使用switchToLatest。也就是当Publisher的Output也是一个Publisher类型时。

我们先看下它的弹珠图

img1275.jpg

然后在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)

上述代码的注释部分分别为:

  1. 创建三个发布整数且错误类型为Never的 PassthroughSubject。
  2. 创建Output也是Publisher的 PassthroughSubject。例如,您可以通过它发送publisher1、publisher2 或publisher3。
  3. 在您的publishers上使用 switchToLatest。现在,每次通过publishers发送不同的发布者时,您都会切换到新的发布者并取消之前的订阅
  4. publishers发布第一个数据,数据是publisher1,然后publisher1发布数据,内容是1、2。
  5. publishers发送第二个数据,数据是publisher2,此时会取消对publisher1 的订阅。
    然后,publisher1发布数据3,但它被忽略(因为publisher1的订阅已经被取消了!)。
    随后publisher2发布数据4、5, 这两个数据是可以被接收的。
  6. publishers发布第三个数据,数据是publisher3,这时取消了publisher2 的订阅。和以前一样,您将 publisher2发布数据6 并被忽略,然后publisher3发送7、8 和9,这三个数据是可以被接收的。
  7. 最后,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,因为他帮我们取消了之前的订阅,只保留了最后一次。

Simulator Screen Shot - iPhone 12 - 2021-10-14 at 01.29.44.png