目次

目次

【Swift】 Combine Publisher Operatorsまとめ

アバター画像
神山義仁
アバター画像
神山義仁
最終更新日2022/09/26 投稿日2022/09/26

はじめに

こんにちは。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.png

Operator使用例

Operatorは非常に多くあるため、ここでは一部をご紹介いたします。

マッピング系

map: あるデータ型を別のデータ型に変換する

[1, 5, 25].publisher
    .map { String($0) }
    .sink { value in
        print(value)
    }

// 出力結果: "1", "5", "25"
Map.png

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
TryMap.png

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
FlatMap.png

scan: 最後に返された値と共に流れてきた値を処理する

["a", "b", "c"].publisher
    .scan("") { accumulator, current in
        accumulator + current
    }
    .sink { value in
        print(value)
    }

// 出力結果 "a", "ab", "abc"
Scan.png

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
TryScan.png

フィルタリング系

compactMap: nilを排除して値を返却する

["a", nil, "b"].publisher
    .compactMap { $0 }
    .sink { value in
        print(value)
    }

// 出力結果: "a", "b"
CompactMap.png

filter: 条件に合わない値を排除して値を返却する

[5, 10, 15].publisher
    .filter { $0 != 10 }
    .sink { value in
        print(value)
    }

// 出力結果: 5, 15
Filter.png

removeDuplicates: 以前に送信された値を記憶し、現在の値と一致しない値のみを返却する

[1, 1, 2, 1, 5, 5, 10].publisher
    .removeDuplicates()
    .sink { value in
        print(value)
    }

// 出力結果: 1, 2, 1, 5, 10
RemoveDuplicate.png

まとめる系

reduce: 全ての要素をまとめて値を返却する

["a", "b", "c"].publisher
    .reduce("") { accumulator, current in
        accumulator + current
    }
    .sink { value in
        print(value)
    }

// 出力結果: "abc"
Reduce.png

collect: 流れてきた要素を配列にまとめて値を返却する

["a", "b", "c", "d", "e", "f"].publisher
    .collect(2)
    .sink { value in
        print(value)
    }

// 出力結果: ["a", "b"], ["c", "d"], ["e", "f"]
Collect.png

数値系

max: 流れてきた値の最大値を返却する

[1, 5, 10, 25].publisher
    .max()
    .sink { value in
        print(value)
    }

// 出力結果: 25
Max.png

min: 流れてきた値の最小値を返却する

[1, 5, 10, 25].publisher
    .min()
    .sink { value in
        print(value)
    }

// 出力結果: 1
Min.png

count: 流れてきた値の個数を返却する

[1, 5, 10, 25].publisher
    .count()
    .sink { value in
        print(value)
    }

// 出力結果: 4
Count.png

条件合致系

allSatisfy: 全ての要素に対して条件処理を行い、全一致しているかどうかの単一のBool値を返却する

["a", "b", "c", "de"].publisher
    .allSatisfy { $0.count == 1 }
    .sink { value in
        print(value)
    }

// 出力結果: false
AllSatisfy.png

contains: 全ての要素に対して条件処理を行い、部分一致しているかどうかの単一のBool値を返却する

["a", "b", "c", "e", "ef"].publisher
    .contains { $0.count == 1 }
    .sink { value in
        print(value)
    }

// 出力結果: true
Contains.png

組み合わせる系

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)
CombineLatest.png

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
Merge.png

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)
Zip.png

エラーハンドリング系

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
Catch.png

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
MapError.png

さいごに

今回ご紹介したOperatorはほんの一部で、他にもさまざまな機能を有したものがあります。

全てを把握するのはとても大変なので、実際に開発していく中で使用方法について理解を深めていこうと思います。

最後まで記事を読んで頂き、ありがとうございました。

参考文献

Apple document (Publisher Operators)

Combine in Practice

アバター画像

神山義仁

iOSエンジニアです。

目次