はじめに
こんにちは。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はほんの一部で、他にもさまざまな機能を有したものがあります。
全てを把握するのはとても大変なので、実際に開発していく中で使用方法について理解を深めていこうと思います。
最後まで記事を読んで頂き、ありがとうございました。
参考文献
神山義仁
iOSエンジニアです。