第四章 Combine的转换操作符(collect、map、flatMap)

2,724 阅读3分钟

本章介绍Combine中一个基本元素符类别:转换运算符(Transforming Operators)。 转换运算符将来自发布者的值处理为订阅者可用的格式。你会发现,Combine中的转换运算符与Swift标准库中的常见运算符(如map和flatMap)之间存在相似之处。

操作符也是发布者(Publishers)

在Combine 中,我们称对来自发布者的值执行操作的方法为“操作符”。 每个 Combine 运算符返回一个发布者。一般而言,发布者接收上游事件,对其进行操作,然后将操作后的事件向下游发送给消费者。 为了简化这个概念,在本章中,您将专注于使用运算符和处理它们的输出。

除非操作符的目的是处理上游错误,否则它只会在下游重新发布所述错误

Collect

可以发布单独的值或值的集合的Publisher。例如当您想要填充列表或网格视图时,可以使用这个Publisher

功能

collect操作符提供了一种方便的方法,可以将来自发布者的一系列单个值转换为一个数组 为了帮助理解这个操作符和你将在本书中学到的所有其他操作符,你将使用弹珠图。 大理石图有助于可视化操作员的工作方式。最上面一行是上游发布者。该框代表操作员。底线是订阅者,或者更具体地说,在运营商操纵来自上游发布者的值之后订阅者将收到什么。 底线也可能是另一个运算符,它接收来自上游发布者的输出,执行其操作,并将这些值发送到下游。

Marble diagrams

img1005.jpg

代码示例

// sink接收了5次数据
["A", "B", "C", "D", "E"].publisher
 .sink { print($0) }
 
// print
A
B
C
D
E

// sink接收1次数据
["A", "B", "C", "D", "E"].publisher
 .collect()
 .sink { print($0) }

// print
["A", "B", "C", "D", "E"]
  

// sink接收了3次(序列元素个数 / collect参数,有余数则加1)
["A", "B", "C", "D", "E"].publisher
 .collect(2)
 .sink { print($0) }
 
// print
["A", "B"]
["C", "D"]
["E"]

map

除了收集数据,您通常还想以某种方式转换这些数据。为此,Combine 提供了多种映射运算符 你首先要了解的是 map,它的工作方式与 Swift 的标准 map 一样,只是它对发布者发出的值进行操作。

功能

map对发布者的数据(Output)进行操作,可以转换数据内容。

对于Output是元祖的类型,可以用map(.x)的方式获取其中一个数据

Marble diagrams

img1015.jpg

代码示例

[1, 2, 3, 4, 5].publisher
  // 用map将数据转换为 原始值*2
 .map { $0 * 2 }
 .sink { print($0) }
 
// print
2
4
6
8
10

// Output为元祖的情况
let url = URL(string: "https://www.baidu.com")!
let pub = URLSession.shared.dataTaskPublisher(for: url)
  // 将原本的(data, reponse)元祖,转换为单个data数据
 .map(\.data)

flatMap

flatMap是Combine中功能最强大的Operator之一。

书中定义为:flatMap 运营商将多个上游发布者扁平化为一个下游发布者——或者更具体地说,扁平化这些发布者的排放。

我们其实可以简单将其理解为:flatMap作用是将上游的Publisher转换为新的Publisher 而且通常情况下,新的Publisher的Output数据类型可以与旧的Output数据类型不同,错误类型也可以不同。

在Combine 中flatMap 的一个常见用例是,当你想要将一个发布者发出的元素传递给一个本身返回一个发布者的方法,并最终订阅第二个发布者发出的元素。

功能

flatMap将上游的publisher转换为新的publisher。

Marble diagrams

img1042.jpg

代码示例

[1, 2, 3].publisher.flatMap({ int in
 		// 转换原publisher为新publisher
 		// 新publisher的Output为Range类型
 		return (0..<int).publisher
 	}).sink(receiveCompletion: { _ in }, receiveValue: { value in
 		print("value: \(value)")
 	})

输出为

value: 0
value: 0
value: 1
value: 0
value: 1
value: 2

在实际应用中,flatMap可用于我们的搜索流程,比如我们在搜索栏键入一段搜索关键字,点击“搜索”按钮,或是软键盘上点击Return,通过网络请求,搜索我们的关键字,返回搜索结果。

可以看如下代码:

ViewModel.swift

class ViewModel: ObservableObject {

    /// 搜索关键字Publisher,用于启动Combine流程
    var searchPublisher = PassthroughSubject<String, Never>()
    private var cancellable = Set<AnyCancellable>()

    init() {
        
        searchPublisher
            .print("_Combine_")
			
            // MARK: - 通过PassthroughSubject订阅关键字变化
            //         通过flatMap转换Publisher为Publisher<StockInfo, Error>
            //         通过search查询关键字
            .flatMap { searchContent in
                return self.search(searchContent)
            }
			
            .receive(on: RunLoop.main)
            .sink { [weak self] completion in
                switch completion {
                case .finished:
                    print("finished")
                case .failure(let error):
                    print("sink error: \(error)")
                }
            } receiveValue: { [weak self] searchResult in
                print("receiveValue: \(searchResult)")
                // 搜索后获得数据,可存储到ViewModel中的@Published变量,来刷新View视图
            }
            .store(in: &cancellable)
    }
    
    /// 按照关键字搜索 ,返回为Publisher
    /// - Parameters:
    ///   - text: 关键字
    /// - Returns: AnyPublisher<StockInfo, Error>
    public func search(_ text: String) -> AnyPublisher<StockInfo, Error> {
        
        let url = URL(string: "your request url" + text)
        guard let url = url else {
            // url失败返回一个错误Publisher
            return Fail(error: APIError.badURL).eraseToAnyPublisher()
        }
        
	// 网络请求Publisher
        return URLSession.shared.dataTaskPublisher(for: url)
            .retry(2)
            .map { $0.data }
            .decode(type: StockInfo.self, decoder: JSONDecoder())
            .replaceError(with: [])
            .setFailureType(to: Error.self)
            .eraseToAnyPublisher()
    }
	
	// 错误枚举定义
	enum APIError: Error, CustomStringConvertible {
   
        case badURL
        case badNetwork(error: Error)
        case badDecode
        case unknown

        var description: String {
            switch self {
            case .badURL:
                return "_URL Invalided_"
            case .badNetwork(let error):
                return "_network error: \(error.localizedDescription)_"
            case .badDecode:
                return "_decode error_"
            case .unknown:
                return "_unknown error_"
            }
        }
    }
}

在我们的View中,可以通过声明ViewModel变量,然后调用ViewModel中的searchPublisher.send来启动这个搜索流程。

@StateObject var vm = ViewModel()

....

// 在搜索时调用
vm.searchPublisher.send(搜索关键字)

如果不使用flatMap,我们可以试想下是如何实现这个搜索功能。

  1. 声明一个搜索函数,用于发起网络请求,需要传入的参数是搜索关键字
  2. 在用户开始搜索时,获取搜索框中的内容,然后调用搜索函数,传入搜索关键字做参数

使用flatMap后,流程变为

  1. 初始化中,创建Combine流程

搜索关键字异步变化 ––– 转换为网络请求 ––– 获取搜索结果

只有一步而已,这就是flatMap的强大之处