LoginSignup
94
62

More than 1 year has passed since last update.

【Swift】CombineフレームワークのCombining Operators(Publisherを結合するOperator)の動きを確認する

Last updated at Posted at 2019-10-19

Combineフレームワークを使っている時に
いくつかのPublisherから出力される値を
まとめて使いたい場面はあると思います。

そのようなOperatorをCombining Operator(結合するOperator)と呼ばれ
いくつかの種類があり
それぞれ出力されるタイミングや値が異なります。

今回はそんなOperatorの動きを
見ていきたいと思います。

※ 下記に記載しているコードは
Playgroundで実行しています。

前に値を出力する(Prepend)

あるPublisherの前に値が出力されるようにPublisherを挿入します。

スクリーンショット 2019-10-19 11.42.53.png

具体的には3つのメソッドがあります。

prepend(Output...)

可変長引数を受け取ります。


var subscriptions = Set<AnyCancellable>()
let publisher = ["c", "d"].publisher
publisher
    .prepend("a", "b")
    .sink(receiveValue: { print($0) })
    .store(in: &subscriptions)

// 出力結果
a
b
c
d

prepend(S) where S: Sequence

Sequenceプロトコルに適合した型のインスタンスを受け取ります。


var subscriptions = Set<AnyCancellable>()
let publisher = ["e", "f", "g"].publisher
publisher
   .prepend(["c", "d"])
   .prepend(Set(["a", "b"]))
   .sink(receiveValue: { print($0) })
   .store(in: &subscriptions)

// 出力結果
a
b
c
d
e
f

※a, bはSetなので順不同です。

この例の結果を見てみますと
prependは
後に追加したものから先に出力される
ことがわかります。

prepend(P) where P: Publisher

Publisherプロトコルに適合した型のインスタンスを受け取ります。


var subscriptions = Set<AnyCancellable>()

let publisherA = ["c", "d"].publisher
let publisherB = ["a", "b"].publisher
publisherA
    .prepend(publisherB)
    .sink(receiveValue: { print($0) })
    .store(in: &subscriptions)

// 出力結果
a
b
c
d

ここで注意が必要な点としては
ドキュメントにも記載がありましたが
前のPublisherが終了しないと
次のPublisherの値が出力されないという点です。

例えば下記の場合ですと


var subscriptions = Set<AnyCancellable>()

let publisherA = ["c", "d"].publisher
let publisherB = PassthroughSubject<String, Never>()
publisherA
    .prepend(publisherB)
    .sink(receiveValue: { print($0) })
    .store(in: &subscriptions)
publisherB.send("a")
publisherB.send("b")

// 出力結果
a
b

となり
publisherAのc, dは出力されません。

出力するためにはpublisherBを終了させます。


var subscriptions = Set<AnyCancellable>()

let publisherA = ["c", "d"].publisher
let publisherB = PassthroughSubject<String, Never>()
publisherA
    .prepend(publisherB)
    .sink(receiveValue: { print($0) })
    .store(in: &subscriptions)
publisherB.send("a")
publisherB.send("b")
publisherB.send(completion: .finished)

// 出力結果
a
b
c
d

後に値を出力する(Append)

Prependとは反対に対象のPublisherの後に値を出力します。

スクリーンショット 2019-10-19 11.39.57.png

メソッドも同様に3つあります。

append(Output...)

可変長引数を受け取ります。


var subscriptions = Set<AnyCancellable>()

let publisherA = ["c", "d"].publisher
let publisherB = ["a", "b"].publisher
publisherA
    .append(publisherB)
    .sink(receiveValue: { print($0) })
    .store(in: &subscriptions)

// 出力結果
c
d
a
b

append(S) where S: Sequence

Sequenceプロトコルに適合した型のインスタンスを受け取ります。


var subscriptions = Set<AnyCancellable>()

let publisher = ["e", "f", "g"].publisher
publisher
    .append(["c", "d"])
    .append(Set(["a", "b"]))
    .sink(receiveValue: { print($0) })
    .store(in: &subscriptions)

// 出力結果
e
f
g
c
d
b
a

※a, bはSetなので順不同です。

この例の結果を見てみますと
appendは
前に追加したものから先に出力される
ことがわかります。

append(P) where P: Publisher

Publisherプロトコルに適合した型のインスタンスを受け取ります。


var subscriptions = Set<AnyCancellable>()

let publisherA = ["c", "d"].publisher
let publisherB = ["a", "b"].publisher
publisherA
    .append(publisherB)
    .sink(receiveValue: { print($0) })
    .store(in: &subscriptions)

// 出力結果
c
d
a
b

prependと同様に
前のPublisherが終了しないと
次のPublisherの値が出力されないという点です。

例えば下記の場合ですと


var subscriptions = Set<AnyCancellable>()

let publisherA = ["c", "d"].publisher
let publisherB = PassthroughSubject<String, Never>()
let publisherC = PassthroughSubject<String, Never>()
publisherA
    .append(publisherB)
    .append(publisherC)
    .sink(receiveValue: { print($0) })
    .store(in: &subscriptions)
publisherB.send("a")
publisherB.send("b")
publisherC.send("e")
publisherC.send("f")

// 出力結果
c
d
a
b

となり
publisherCのe, fは出力されません。

出力するためにはpublisherBを終了させます。


var subscriptions = Set<AnyCancellable>()

let publisherA = ["c", "d"].publisher
let publisherB = PassthroughSubject<String, Never>()
let publisherC = PassthroughSubject<String, Never>()
publisherA
    .append(publisherB)
    .append(publisherC)
    .sink(receiveValue: { print($0) })
    .store(in: &subscriptions)
publisherB.send("a")
publisherB.send("b")
publisherB.send(completion: .finished)
publisherC.send("e")
publisherC.send("f")

// 出力結果
c
d
a
b
e
f

merge(with:)

Publisherの後に出力されるPublisherを繋げます。
最大8つ繋げることができます。

スクリーンショット 2019-10-19 11.50.39.png


var subscriptions = Set<AnyCancellable>()

let publisherA = PassthroughSubject<String, Never>()
let publisherB = PassthroughSubject<String, Never>()

publisherA
    .merge(with: publisherB)
    .sink(receiveCompletion: { _ in print("finish") },
          receiveValue: { print($0) })
    .store(in: &subscriptions)

publisherA.send("a")
publisherA.send("b")
publisherB.send("c")
publisherA.send("d")
publisherB.send("e")

publisherA.send(completion: .finished)
publisherB.send(completion: .finished)

// 出力結果
a
b
c
d
e
finish

switchToLatestと同様に
結合した全てのpublisherが終了しないと
対象のpublisherは終了しません。


...

publisherA.send(completion: .finished) 
// publisherB.send(completion: .finished)

// 出力結果
a
b
c
d
e
finishは出力されない

switchToLatest

複数のPublisherを
一つのPublisherとして出力するように結合します。

この際に新しいPublisherを出力すると
前のPublisherのSubscriptionはキャンセルされます。

スクリーンショット 2019-10-19 12.12.43.png


var subscriptions = Set<AnyCancellable>()

let publisherA = PassthroughSubject<String, Never>()
let publisherB = PassthroughSubject<String, Never>()
let publisherC = PassthroughSubject<String, Never>()

let publishers = PassthroughSubject<PassthroughSubject<String,
    Never>, Never>()

publishers.switchToLatest()
    .sink(receiveCompletion: { _ in print("finish") },
          receiveValue: { print($0)})
    .store(in: &subscriptions)

publishers.send(publisherA)
publisherA.send("a")
publisherA.send("b")

// publisherBへ切り替え
publishers.send(publisherB)
publisherA.send("c") // 出力されない
publisherB.send("d")
publisherB.send("e")

// publisherCへ切り替え
publishers.send(publisherC)
publisherB.send("f") // 出力されない
publisherC.send("g")
publisherC.send("h")
publisherC.send("i")

publisherC.send(completion: .finished)
publishers.send(completion: .finished)

// 出力結果
a
b
d
e
g
h
i
finish

上記のように

publisherBへ切り替えた後のpublisherA

publisherCへ切り替えた後のpublisherB
の値の出力はされません。

また注意点として
現在のpublisherが終了しないと
publishersの終了は出力されません。


...

// publisherC.send(completion: .finished)
publishers.send(completion: .finished)

// 出力結果
a
b
d
e
g
h
i
finishは出力されない

どういう時に使うか?

イメージしづらいかもしれませんが
例えば
ボタンを押すと外部からデータを取得するなどを行う時に
何度もボタンを押されても
通信を重複して行わないようにする場合などが
使用例として考えられます。

combineLatest

これも複数のPublisherを一つにまとめて値を出力しますが
下記の3つの特徴があります。

  • 出力される値はそれぞれのPublisherから出力される値のTuple
  • 結合している全てのPublisherから新しい値が一つ出力されないと値は出力されない
  • 結合しているいずれかのPublisherから新しい値が出力されると値が出力される

combineLatest.png


var subscriptions = Set<AnyCancellable>()

let publisherA = PassthroughSubject<Int, Never>()
let publisherB = PassthroughSubject<String, Never>()

publisherA
    .combineLatest(publisherB)
    .sink(receiveCompletion: { _ in print("finish") },
          receiveValue: { print("A: \($0), B: \($1)") }) 
    .store(in: &subscriptions)

publisherA.send(1)
publisherA.send(2)
publisherB.send("a")
publisherB.send("b")
publisherA.send(3)
publisherB.send("c")

publisherA.send(completion: .finished)
publisherB.send(completion: .finished)

// 出力結果
A: 2, B: a
A: 2, B: b
A: 3, B: b
A: 3, B: c
finish

どういう時に使うか?

考えられる例としては
冒頭で紹介したような
複数の入力フォームがあって
全てに入力しないと登録ボタンが押せないように
ボタンの状態を制御するときなど
各フォームの値の変更を自動で検知してくれるので
便利です。

zip

combineLatestに似ていますが
下記部分が異なります。

  • 結合している全てのPublisherから新しい値が出力されると値が出力される

スクリーンショット 2019-10-19 12.19.30.png


var subscriptions = Set<AnyCancellable>()

let publisherA = PassthroughSubject<Int, Never>()
let publisherB = PassthroughSubject<String, Never>()

publisherA
    .zip(publisherB)
    .sink(receiveCompletion: { _ in print("finish") },
          receiveValue: { print("A: \($0), B: \($1)") }) 
    .store(in: &subscriptions)

publisherA.send(1) // publisherA①
publisherA.send(2) // publisherA②
publisherB.send("a") // publisherB①
publisherB.send("b") // publisherB②
publisherA.send(3) // publisherA③
publisherB.send("c") // publisherB③

publisherA.send(completion: .finished)
publisherB.send(completion: .finished)

// 出力結果
A: 1, B: a
A: 2, B: b
A: 3, B: c
finish

ちょっとややこしいのですが
publisherA①の後に
publisherA②を出力していますが
まだ何も出力されません。

その次のpublisherB①が出力された後に初めて
A: 1, B: a
という値が出力されます。

そして
publisherB②が出力されると
A: 2, B: b
が出力されます。

publisherの出力値に番号がついていて
同じ番号のものが全て揃った時に
値が出力されるようなイメージだとわかりやすいかもしれません。

どういう時に使うか?

複数の非同期で行うAPIリクエストの
全ての結果を受け取った後に
処理を行いたい場合などに活用できます。

まとめ

Combineの中でも
Publisherを結合するOperatorについて見てきました。

使うと便利なものがたくさんありますが
使い方を間違えると
微妙に思った通りの動きをしてくれないなど
混乱してしまう可能性があります。

他にも便利ですが
似た様なOperatorはありますので
また頭の整理のためにも
見ていきたいと思います😃

もし間違いなどございましたら
ご指摘頂けますとうれしいです🙇🏻‍♂️

94
62
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
94
62