はじめに
こんにちは。iOSアプリ開発グループの神山です。
最近Combineについて触れる機会があり、絶賛勉強中です。
今回はCombineの中で、流れてきたPublisherに処理を施してPublisherを再生成することのできるOperatorについてまとめてみました。
Operatorとは
Operatorは流れてきたイベントを加工して、新たなイベントを流すことができるPublisherの機能の一つです。
Appleのドキュメントにはこのような記載があります。
Use operators to assemble a chain of republishers, optionally ending with a subscriber, that processes elements produced by upstream publishers. Each operator creates and configures an instance of a Publisher or Subscriber, and subscribes it to the publisher that you call the method on.
つまり以下の図のような流れでPublisherをSubscribeすることができます。
Operator使用例
Operatorは非常に多くあるため、ここでは一部をご紹介いたします。
マッピング系
map: あるデータ型を別のデータ型に変換する
[1, 5, 25].publisher .map { String($0) } .sink { value in print(value) } // 出力結果: "1", "5", "25" |
tryMap: エラーをthrowすることのできる map
enum ConvertError: Error { case integerError } ["1", "2", "hoge"].publisher .tryMap { value throws -> Int in if let integer = Int(value) { return integer } else { throw ConvertError.integerError } } .sink { completion in switch completion { case let .failure(error): print(error) case .finished: print("finished") } } receiveValue: { value in print(value) } // 出力結果: 1, 2, integerError |
flatMap: あるPublisherを別のPublisherに変換する
enum ConvertError: Error { case integerError } ["1", "hoge", "2"].publisher .flatMap { value in return Just(value) .tryMap { value throws -> Int in if let integer = Int(value) { return integer } else { throw ConvertError.integerError } } .catch { _ in Just(0) } } .sink { completion in switch completion { case let .failure(error): print(error) case .finished: print("finished") } } receiveValue: { value in print(value) } // 出力結果: 1, 0, 2, finished |
scan: 最後に返された値と共に流れてきた値を処理する
["a", "b", "c"].publisher .scan("") { accumulator, current in accumulator + current } .sink { value in print(value) } // 出力結果 "a", "ab", "abc" |
tryScan: エラーをthrowすることのできる scan
enum StringError: Error { case ngWord } ["a", "b", "error"].publisher .tryScan("") { accumulator, current in if current == "error" { throw StringError.ngWord } return accumulator + current } .sink { completion in switch completion { case let .failure(error): print(error) case .finished: print("finished") } } receiveValue: { value in print(value) } // 出力結果: "a", "ab", ngword |
フィルタリング系
compactMap: nilを排除して値を返却する
["a", nil, "b"].publisher .compactMap { $0 } .sink { value in print(value) } // 出力結果: "a", "b" |
filter: 条件に合わない値を排除して値を返却する
[5, 10, 15].publisher .filter { $0 != 10 } .sink { value in print(value) } // 出力結果: 5, 15 |
removeDuplicates: 以前に送信された値を記憶し、現在の値と一致しない値のみを返却する
[1, 1, 2, 1, 5, 5, 10].publisher .removeDuplicates() .sink { value in print(value) } // 出力結果: 1, 2, 1, 5, 10 |
まとめる系
reduce: 全ての要素をまとめて値を返却する
["a", "b", "c"].publisher .reduce("") { accumulator, current in accumulator + current } .sink { value in print(value) } // 出力結果: "abc" |
collect: 流れてきた要素を配列にまとめて値を返却する
["a", "b", "c", "d", "e", "f"].publisher .collect(2) .sink { value in print(value) } // 出力結果: ["a", "b"], ["c", "d"], ["e", "f"] |
数値系
max: 流れてきた値の最大値を返却する
[1, 5, 10, 25].publisher .max() .sink { value in print(value) } // 出力結果: 25 |
min: 流れてきた値の最小値を返却する
[1, 5, 10, 25].publisher .min() .sink { value in print(value) } // 出力結果: 1 |
count: 流れてきた値の個数を返却する
[1, 5, 10, 25].publisher .count() .sink { value in print(value) } // 出力結果: 4 |
条件合致系
allSatisfy: 全ての要素に対して条件処理を行い、全一致しているかどうかの単一のBool値を返却する
["a", "b", "c", "de"].publisher .allSatisfy { $0.count == 1 } .sink { value in print(value) } // 出力結果: false |
contains: 全ての要素に対して条件処理を行い、部分一致しているかどうかの単一のBool値を返却する
["a", "b", "c", "e", "ef"].publisher .contains { $0.count == 1 } .sink { value in print(value) } // 出力結果: true |
組み合わせる系
combineLatest: 1つのPublisherに対して複数のPublisherを接続して、メインの現在の値と共に接続した値を返却する
let publisher = [1, 3, 5, 7].publisher let publisher2 = [2, 4, 6, 8].publisher publisher .combineLatest(publisher2) .sink { value in print(value) } // 出力結果: (7, 2), (7, 4), (7, 6), (7, 8) |
merge: 1つのPublisherに対して複数のPublisherを接続して、メインで流された値と接続した値を返却する
let publisher = [1, 3, 5, 7].publisher let publisher2 = [2, 4, 6, 8].publisher publisher .merge(with: publisher2) .sink { value in print(value) } // 出力結果: 1, 3, 5, 7, 2, 4, 6, 8 |
zip: 2つのPublisherを結合し、値をタプルで返却する
let publisher = [1, 3, 5, 7].publisher let publisher2 = [2, 4, 6].publisher publisher .zip(publisher2) .sink { value in print(value) } // 出力結果: (1, 2), (3, 4), (5, 6) |
エラーハンドリング系
catch: エラーを受け取った際に新たなPublisherを作成する
enum ConvertError: Error { case integerError } ["1", "hoge", "2"].publisher .tryMap { value throws -> Int in if let integer = Int(value) { return integer } else { throw ConvertError.integerError } } .catch { _ in Just(0) } .sink { completion in switch completion { case let .failure(error): print(error) case .finished: print("finished") } } receiveValue: { value in print(value) } // 出力結果: 1, 0, finished |
mapError: エラーを新たなエラーに変換する
enum ConvertError: Error { case integerError case mapError } ["1", "hoge", "2"].publisher .tryMap { value throws -> Int in if let integer = Int(value) { return integer } else { throw ConvertError.integerError } } .mapError { _ in ConvertError.mapError } .sink { completion in switch completion { case let .failure(error): print(error) case .finished: print("finished") } } receiveValue: { value in print(value) } // 出力結果: 1, mapError |
さいごに
今回ご紹介したOperatorはほんの一部で、他にもさまざまな機能を有したものがあります。
全てを把握するのはとても大変なので、実際に開発していく中で使用方法について理解を深めていこうと思います。
最後まで記事を読んで頂き、ありがとうございました。
参考文献
Apple document (Publisher Operators)
この記事を書いた人
- iOSエンジニアです。
最近書いた記事
- 2023.05.09iOSの証明書関連をイラストで理解する
- 2022.12.19【Swift】Widgetの作り方 〜iOS 16対応版〜
- 2022.09.26【Swift】 Combine Publisher Operatorsまとめ
- 2022.09.26【Swift】 Combineを使用するメリットについて考えてみる