Swift・iOS

Swiftを中心に学んだことを記録に残すブログです。技術に関係ない記事もたまに書いています。

【Combine】"AnyCancellable"と"Set<AnyCancellable>"の使い分け

 

はじめに

表題のテーマについて疑問に思ったので調べてみました。

※「あくまで現状の理解では」という前置きがついた内容ですのでご注意ください・・・。

 

開発環境

 

本題

様々なCombineのサンプルコードを読む中で、以下のようにAnyCancellable型のプロパティをinitで初期化するパターンと、Setに"store(in:)"を使ってAnyCancellableのインスタンスを格納するパターンがあることに気がつきました。動作としてはどちらも同じ。なぜ2パターンあるのか、どのように使い分ければいいのか疑問に思いました。

  • AnyCancellable型のプロパティをinitで初期化するパターン
private var cancellable: AnyCancellable?

init() {
    cancellable = $text
        .filter( { $0.unicodeScalars.allSatisfy( { CharacterSet.alphanumerics.contains($0) } ) } )
        .debounce(for: .milliseconds(500), scheduler: RunLoop.main)
        .receive(on: RunLoop.main)
        .assign(to: \.filteredText, on: self)
}

 

  • Setのプロパティに"store(in:)"を使ってAnyCancellableのインスタンスを格納するパターン
private var cancellables: Set<AnyCancellable> = []

init() {
    $text
        .filter( { $0.unicodeScalars.allSatisfy( { CharacterSet.alphanumerics.contains($0) } ) } )
        .debounce(for: .milliseconds(500), scheduler: RunLoop.main)
        .receive(on: RunLoop.main)
        .assign(to: \.filteredText, on: self)
        .store(in: &cancellables)
}

 

Combineで調べても中々該当の情報にたどりつけなかったのですが、RxSwiftのdisposeとDisposeBagの違いを調べることで解決できました。現状の理解としては、Setでは複数のAnyCancellableを一括で管理できるメリットがあるというイメージです。

参考:RxSwiftを理解する[初級編] - Qiita

 

例えば、3つのtext(textA、textB、textC)の値に対してCombineの処理を定義する場合を考えてみます。AnyCancellable型のプロパティをinitで初期化するパターンだと、以下のように3つAnyCancellable型のプロパティを定義することになります。

private var cancellableA: AnyCancellable?
private var cancellableB: AnyCancellable?
private var cancellableC: AnyCancellable?

init() {
    cancellableA = $textA
        .sink(receiveValue: { _ in
            // do something
        })
    
    cancellableB = $textB
        .sink(receiveValue: { _ in
            // do something
        })
    
    cancellableC = $textC
        .sink(receiveValue: { _ in
            // do something
        })
}

 

上記の実装をSetを使って修正してみます。こちらの方が実装がすっきりしますし、例えばプロパティtextDを追加して、Combineの処理を定義したい場合でも対応しやすいように感じます。canselメソッドを呼ぶタイミングがそれぞれ違うなどの場合を除けば、基本的にはこちらのパターンで実装してしまってよいと思いました。

private var cancellables: Set<AnyCancellable> = []

init() {
    $textA
        .sink(receiveValue: { _ in
            // do something
        })
        .store(in: &cancellables)
    
    $textB
        .sink(receiveValue: { _ in
            // do something
        })
        .store(in: &cancellables)
    
    $textC
        .sink(receiveValue: { _ in
            // do something
        })
        .store(in: &cancellables)
}

 

おわりに

RxSwiftの経験がある方からすると当たり前の知識だと思いますが、Combineからリアクティブプログラミングを学習し始めた身としては理解に時間がかかりました笑

(「本当に理解できているのか・・・?いや、できていないに違いない・・・!」という心境ですが笑)

 

CombineよりRxSwiftの方がドキュメントの数は多いですし、今回のようにCombineで探したい情報にたどり着けない場合は、似た概念やメソッドがRxSwiftにないか探してみるのもありだなと思いました。RxSwift未経験だと、似た概念やメソッドを探すのはなかなか気合いが必要という問題はあるのですが笑

 

参考