【Swift/Combine】combineLatestで複数条件を監視する方法!mergeやzipとの違い

【Swift/Combine】combineLatestで複数条件を監視する方法!mergeやzipとの違い

この記事からわかること

  • SwiftCombineフレームワーク使い方
  • combineLatestメソッドとは?
  • 複数publisher1つにまとめて監視する方法
  • mergezipとの違い使い分け

index

[open]

\ アプリをリリースしました /

みんなの誕生日

友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-

posted withアプリーチ

公式リファレンス:Combine Framework

環境

combineLatestメソッドの使い方

公式リファレンス:combineLatest(_:)

func combineLatest<P>(_ other: P) -> Publishers.CombineLatest<Self, P> where P : Publisher, Self.Failure == P.Failure

combineLatest複数のpublisherをまとめて1つのpublisherにして管理することができるメソッドです。例としてバリデーションロジックを実装する時など「全てがtrueの場合のみ処理を実装」する場合で考えてみます。

combineLatestメソッドは1つのpublisherから呼び出し、引数に一緒に観測させたいpublisherを渡します。まとめた後のオペレーターからはタプルで流れてくるそれぞれの値を参照することが可能ですが、mapなどを使用して1つの値を流すように変換すると見通しが良くなります。

let subjectA: PassthroughSubject<Bool, Never> = .init()
let subjectB: PassthroughSubject<Bool, Never> = .init()
let subjectC: PassthroughSubject<Bool, Never> = .init()

let cancellable = subjectA
    .combineLatest(subjectB, subjectC)
    .map { valueA, valueB, valueC in
        return valueA && valueB && valueC
    }
    .sink { value in
        if value {
            print("All subjects are true.")
        }
    }

subjectA.send(true)
subjectB.send(false)
subjectC.send(true)

combineLatestメソッドでまとめると全てのpublisherから値が流れてこないと処理が走りません。以下のようにsubjectCだけsendメソッドが送られていない状態の場合はその後のオペレーターなどは動作しなくなります。

subjectA.send(true)
subjectB.send(false)

全ての値が一度でも流れていれば、それ以降はいずれかの値が流れてくるたびに動作します。

subjectA.send(true)
subjectB.send(false)
subjectC.send(true)
sleep(3)
subjectB.send(true) // このタイミングで All subjects are true. が出力される

publisherのデータ型はバラバラでもOK

publisherが保持するデータ型が異なっていても正常に動作します。

let subjectA: PassthroughSubject<Bool, Never> = .init()
let subjectB: PassthroughSubject<Int, Never> = .init()
let subjectC: PassthroughSubject<Bool, Never> = .init()

let cancellable = subjectA
    .combineLatest(subjectB, subjectC)
    .sink { valueA, valueB, valueC in
        print("\(valueA)/\(valueB)/\(valueC)")
    }

subjectA.send(true)
subjectB.send(0)
subjectC.send(true)

まとめられる最大個数

public func combineLatest<P, Q, R>(_ publisher1: P, _ publisher2: Q, _ publisher3: R) -> Publishers.CombineLatest4<Self, P, Q, R> where P : Publisher, Q : Publisher, R : Publisher, Self.Failure == P.Failure, P.Failure == Q.Failure, Q.Failure == R.Failure

combineLatest引数の個数違いで複数定義されていますが、用意されているのは最大3個までのようです。

let subjectA: PassthroughSubject<Bool, Never> = .init()
let subjectB: PassthroughSubject<Int, Never> = .init()
let subjectC: PassthroughSubject<Bool, Never> = .init()
let subjectD: PassthroughSubject<Int, Never> = .init()

let cancellable = subjectA
    .combineLatest(subjectB, subjectC, subjectD)
    .sink { valueA, valueB, valueC, valueD in
        print("\(valueA)/\(valueB)/\(valueC)/\(valueD)")
    }

subjectA.send(true)
subjectB.send(0)
subjectC.send(true)
subjectD.send(1)

3個以上をまとめたい場合はcombineLatestを再度呼び出せばsinkの引数ではタプルになってしまいますが、結合させることができます。

let subjectA: PassthroughSubject<Bool, Never> = .init()
let subjectB: PassthroughSubject<Bool, Never> = .init()
let subjectC: PassthroughSubject<Bool, Never> = .init()
let subjectD: PassthroughSubject<Bool, Never> = .init()
let subjectE: PassthroughSubject<Bool, Never> = .init()
let subjectF: PassthroughSubject<Bool, Never> = .init()
let subjectG: PassthroughSubject<Bool, Never> = .init()

let cancellable = subjectA
    .combineLatest(subjectB, subjectC, subjectD)
    .combineLatest(subjectE, subjectF, subjectG)
    .sink { valueAtoD, valueE, valueF, valueG in
        print("\(valueAtoD)/\(valueE)/\(valueF)/\(valueG)")
        // (true, true, true, true)/true/true/true
    }

subjectA.send(true)
subjectB.send(true)
subjectC.send(true)
subjectD.send(true)
subjectE.send(true)
subjectF.send(true)
subjectG.send(true)

エラー型が異なる場合は連結できない

連結したいpublisherのエラー型が異なる場合はコンパイルエラーが発生します。

class MyError: Error { }
class TestError: Error { }

let subjectA: PassthroughSubject<Bool, MyError> = .init()
let subjectB: AnyPublisher<Int, Never> = Just(0).eraseToAnyPublisher()
let subjectC: AnyPublisher<Bool, TestError> = Just(false).setFailureType(to: TestError.self).eraseToAnyPublisher()

// Instance method 'combineLatest' requires the types 'MyError' and 'Never' be equivalent
let cancellable = subjectA
    .combineLatest(subjectB2, subjectC2)
    .sink { error in
        print(error)
    } receiveValue: { valueA, valueB, valueC in
        print("\(valueA)/\(valueB)/\(valueC)")
    }

そのためsetFailureTypemapErrorなどを使用して適切にエラーの型を合わせてあげる必要があります。

let subjectB2 = subjectB.setFailureType(to: MyError.self)
let subjectC2 = subjectC.mapError { _ in MyError() }

let cancellable = subjectA
    .combineLatest(subjectB2, subjectC2)
    .sink { error in
        print(error)
    } receiveValue: { valueA, valueB, valueC in
        print("\(valueA)/\(valueB)/\(valueC)")
    }

エラー型の統一は後述するmergezipflatMapなどでも同じです。

並列実行

時間のかかるpublisherだと把握しやすいですがcombineLatestで連結させると並列処理でpublisherを実行することが可能です。

print("START")
func slowPublisher(_ delay: Double) -> AnyPublisher<String, Never> {
    return Future { promise in
        DispatchQueue.global().asyncAfter(deadline: .now() + delay) {
            print("delay", delay)
            promise(.success("Result from Publisher \(delay)"))
        }
    }.eraseToAnyPublisher()
}

let cancellable = slowPublisher(3)
    .combineLatest(slowPublisher(5))
    .sink { result1, result2 in
        print("Both publishers have completed!")
    }

そのためslowPublisher(3)slowPublisher(5)は同時にカウントが始まるので出力は以下のようになります。

0秒:START
3秒:delay 3.0
5秒:delay 5.0
5秒:Both publishers have completed!

mergeやzipとの違いと使い分け

似たようなpublisherをまとめるメソッドにmergezipがあります。それぞれの違いは以下の通りです。

mergeメソッド

公式リファレンス:mergeメソッド

func merge<P>(with other: P) -> Publishers.Merge<Self, P> where P : Publisher, Self.Failure == P.Failure, Self.Output == P.Output

mergeメソッドは複数のpublisherからのイベントを単一のpublisherに順次結合するメソッドです。実行してみるとわかりますが、複数のpublisherをmergeしても流れてくる値は1つのみです。つまり両方のイベントが発火するタイミングで同じpublisherから値が観測することができるので全てのpublisherの値が揃うのを待つわけではありません。

let publisher1 = PassthroughSubject<Int, Never>()
let publisher2 = PassthroughSubject<Int, Never>()

let cancellable = publisher1
    .merge(with: publisher2)
    .sink { value in
        print(value)
    }

publisher1.send(1)  // 出力: 1
print("-----")
publisher2.send(2)  // 出力: 2

そのため出力は以下のようになります。

出力: 1
-----
出力: 2

Output型は統一する

mergeメソッドはOutputが1つにまとめられてしまうのでOutput型を統一する必要があります。異なるOutput型でmergeしようとするとNo exact matches in call to instance method 'merge'というコンパイルエラーが発生するのでmapなどを使用してOutput型を変換する必要があります。

let publisher1 = PassthroughSubject<Int, Never>()
let publisher2 = PassthroughSubject<String, Never>()

let publisher3 = publisher2.map { _ in 0 }

let cancellable = publisher1
    .merge(with: publisher3)
    .sink { value in
        print(value)
    }

zipメソッド

公式リファレンス:zipメソッド

func zip<P>(_ other: P) -> Publishers.Zip<Self, P> where P : Publisher, Self.Failure == P.Failure

zipメソッドは複数のpublisherに対応する位置の値をペアにして出力します。値が流れるのはzipでまとめている値が全て揃った時です。

let publisher1 = PassthroughSubject<Int, Never>()
let publisher2 = PassthroughSubject<String, Never>()
let publisher3 = PassthroughSubject<Bool, Never>()

let cancellable = publisher1
    .zip(publisher2, publisher3)
    .sink { value in
        print(value)
    }

publisher1.send(1)
publisher2.send("A")
publisher3.send(false)  // 出力: (1, "A", false)

値が一度流れると再度全ての値が揃うまで動作することはありません。例えば以下のようなイベントの流れ方をした場合は「11行目の3つの値が揃ったタイミング」で値が流れます。

publisher1.send(1)
publisher2.send("A")
publisher3.send(false)  // 出力: (1, "A", false)

sleep(2)

publisher1.send(2)

sleep(2)
publisher2.send("B")
publisher3.send(true)  // 出力: (2, "B", true)

まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。

ご覧いただきありがとうございました。

searchbox

スポンサー

ProFile

ame

趣味:読書,プログラミング学習,サイト制作,ブログ

IT嫌いを克服するためにITパスを取得しようと勉強してからサイト制作が趣味に変わりました笑
今はCMSを使わずこのサイトを完全自作でサイト運営中〜

New Article

index