🌾

[Swift] [Combine] 1つの Publisher を共有したいときは share() を使おう!

2022/02/20に公開

伝えたいこと

  • 1つの Publisher を share() せずに共有した場合は subscribe した数だけ重複して処理が走ってしまうので、share() をすることによって、それを防ぐことができる

[この記事の続編↓]

前提条件

以下のような AnyPublisher を用意します。

import Combine

var cancellables: Set<AnyCancellable> = []
let numPublisher: PassthroughSubject<Int, Never> = .init()

letPublisher: AnyPublisher<Int, Never>
let 子1Publisher: AnyPublisher<Int, Never>
let 子2Publisher: AnyPublisher<Int, Never>
let 孫1_1Publisher: AnyPublisher<Int, Never>
let 孫1_2Publisher: AnyPublisher<Int, Never>

以下のような関係で Publisher を共有していきます。

- numPublisher
  -- 子1
      - 孫1_1 ←
      - 孫1_2 ←
    - 子2 ←

そして、 のついている Publisher を subscribe して numPublishersend() したときの挙動を確認します。

share() を使わない場合

Publisher = numPublisher
    .map {
        print("「親」を通過しました")
        return $0
    }
    .eraseToAnyPublisher()

子1Publisher =Publisher
    .map {
        print("「子1」を通過しました")
        return $0
    }
    .eraseToAnyPublisher()

子2Publisher =Publisher
    .map {
        print("「子2」を通過しました")
        return $0
    }
    .eraseToAnyPublisher()

孫1_1Publisher = 子1Publisher
    .map {
        print("「孫1_1」を通過しました")
        return $0
    }
    .eraseToAnyPublisher()

孫1_2Publisher = 子1Publisher
    .map {
        print("「孫1_2」を通過しました")
        return $0
    }
    .eraseToAnyPublisher()

// 以下、subscribe の処理
子2Publisher
    .sink { print("子2Publisher receive: \($0)")}
    .store(in: &cancellables)

孫1_1Publisher
    .sink { print("孫1_1Publisher receive: \($0)")}
    .store(in: &cancellables)

孫1_2Publisher
    .sink { print("孫1_2Publisher receive: \($0)")}
    .store(in: &cancellables)

この状態で numPublishersend() してみます。

numPublisher.send(1)

// (出力)
// 「親」を通過しました
// 「子1」を通過しました
// 「孫1_1」を通過しました
// 孫1_1Publisher receive: 1
// 「親」を通過しました ← 余計
// 「子1」を通過しました ← 余計
// 「孫1_2」を通過しました
// 孫1_2Publisher receive: 1
// 「親」を通過しました ← 余計
// 「子2」を通過しました
// 子2Publisher receive: 1

以下のように subscribe した数だけ処理が重複して走っていることがわかります。

  • 親:3回 ← 余計に 2 回通過
  • 子1:2回 ← 余計に 1 回通過
  • 子2:1回
  • 孫1_1:1回
  • 孫1_2:1回

今回のように print() で値を出力していれば、このことに気がつくのですが、そうでもしない限り、なかなか気づきづらい罠になります。

share() を使う場合

共有される 「親Publisher」 と 「子1Publisher」 を share() した変数を新たに作成して、それを subscribe していきます。

Publisher = numPublisher
    .map {
        print("「親」を通過しました")
        return $0
    }
    .eraseToAnyPublisher()

// share() した Publisher を作成
let shared親Publisher =Publisher.share()

子1Publisher = shared親Publisher
    .map {
        print("「子1」を通過しました")
        return $0
    }
    .eraseToAnyPublisher()

子2Publisher = shared親Publisher
    .map {
        print("「子2」を通過しました")
        return $0
    }
    .eraseToAnyPublisher()

// share() した Publisher を作成
let shared子1Publisher = 子1Publisher.share()

孫1_1Publisher = shared子1Publisher
    .map {
        print("「孫1_1」を通過しました")
        return $0
    }
    .eraseToAnyPublisher()

孫1_2Publisher = shared子1Publisher
    .map {
        print("「孫1_2」を通過しました")
        return $0
    }
    .eraseToAnyPublisher()

この状態で numPublishersend() します。

numPublisher.send(1)

// (出力)
// 「親」を通過しました
// 「子1」を通過しました
// 「孫1_2」を通過しました
// 孫1_2Publisher receive: 1
// 「孫1_1」を通過しました
// 孫1_1Publisher receive: 1
// 「子2」を通過しました
// 子2Publisher receive: 1

出力がスッキリしましたね。

以下のように share() をつけると出力の回数が全て 1 回になったことがわかります。

  • 親:1回
  • 子1:1回
  • 子2:1回
  • 孫1_1:1回
  • 孫1_2:1回

これでもいける

share() した Publisher の変数をわざわざ用意するのが面倒な時は、eraseToAnyPublisher() の前に share() を追加するだけで同様の挙動になります。

そのときは share() されていることがわかるような変数名にすることをお勧めします。

shared親Publisher = numPublisher
    .map {
        print("「親」を通過しました")
        return $0
    }
    .share() // 追加
    .eraseToAnyPublisher()

shared子1Publisher =Publisher
    .map {
        print("「子1」を通過しました")
        return $0
    }
    .share() // 追加
    .eraseToAnyPublisher()

結論

  • 1つの Publisher を share() せずに共有した場合は subscribe した数だけ重複して処理が走ってしまうので、share() をすることによって、それを防ぐことができる

[この記事の続編として、share() を使う際の注意点を紹介しております。]

以上になります。

参考

GitHubで編集を提案

Discussion