どうしてCombineを学びたいと思ったのか?
一番最初に作ったアプリで、MVVMアーキテクチャを使用し、1View1ViewMdoelへ書き換えたいという場面がありました。ここで問題になったのが、二つのViewModelで同じ値を保持させたいということです(値の変化も検知し、Viewへ反映させる)。その際に、assgin(to:)
というメソッドを使った書き方を教えていただき、やりたいことが達成できました。その経験から『Combineの一部の機能だけでこんなできちゃうの?めちゃくちゃ便利やん?』と思ったのが、勉強に至った経緯です。現在チーム開発も行っており、将来的にも役に立つかなと思っています。
流れ
- Combineとは?
- 今回のキーワード
- 値を流し、受け取り、ルールを定義してみる!
- サブスクライバーの登録を無効にする
- 片方のサブスクライバーの登録を無効化する
- 複数のサブスクライバーの登録を一気解除する
- まとめ
Combineとは?
公式の一部を引用させてもらいました。
イベント処理演算子を組み合わせて、非同期イベントの処理をカスタマイズする。Combine フレームワークは、時間の経過とともに値を処理するための宣言型 Swift APIを提供します。これらの値は、さまざまな種類の非同期イベントを表すことができます。Combine は、パブリッシャーが時間の経過とともに変化する可能性のある値を公開し、サブスクライバーがパブリッシャーからそれらの値を受け取ることを宣言します。
今回ポイントとなるところが、 『サブスクライバー(Subscriber)』 と 『パブリッシャー(Publisher)』 です。
ざっくり説明すると、値を流すのが、パブリッシャーで、流された値を受け取るのがサブスクライバーです。僕は下の図のようなイメージを持っています。電波塔は誰が聞くかかわからない放送を流します(パブリッシャーの役割)。その放送を聴きたい時はラジオのチューナーを合わせて聴く(サブスクライバーの役割)と言った感じです。
サブスクライバーについて補足です。パブリッシャーに対して、登録の処理を行うオブジェクトを指します。例えば、sink()
で処理の登録を行なったプロパティを指すと言った感じです。
値を流すものをストリーム
や管
と言ったりもするそうです。僕は管がイメージしやすかったので、今回は管を用いて説明させていただきます。
今回のキーワード
『PassthroughSubject<Output,Failure>()
』
『sink()
』
『send()
』
『cancel()
』
『AnyCancellable型
』
値を流し、受け取り、ルールを定義してみる!
パブリッシャー・・・PassthroughSubject<Int,Never>()
サブスクライバー・・・sink()
で登録を行なったsubjectA
import Combine //🟥Combineフレームワークをインポートする。
let subject = PassthroughSubject<Int,Never>()//🟦値を流すための 管を作る
let subjectA = subject.sink { num in //🟦ルールを定義する。
print("PassthroughSubjectgaが受け取った値は:\(num)")
}
subject.send(1) //🟦管に値を流し込む
subject.send(2)
subject.send(3)
『PassthroughSubject<Int,Never>()
』は管のような役割を持っています。今回の場合は、Int型を受け取り、Int型を出力します。第二引数部分はエラーが起こらないNever型になっています。これに対して、『.send()
』を使って管に値を送ります。その送られてきた値をどのように処理するのかを『sink()
』のクロージャ内で定義しています。ここから、いろいろ変化していきます!!!
上の例で出てきたPassthroughSubjectはSubjectプロトコルに適合しているのですが、SubjectプロトコルもPublisherプロトコルを併せ持っているので、Pubisherと同様の働きをすることが可能です。そしてもう一つ。PassthroughSubjectと似た
サブスクライバーの登録を無効にする
sink()のクロージャの中で行っていた処理の登録を無効にさせます。
let subject = PassthroughSubject<Int,Never>()
let subjectA = subject.sink { number in
print("subjectA:",number)
}
subject.send(1)// A 1
subjectA.cancel()
subject.send(2)// 出力されない
subject.send(3)// 出力されない
ここで登場するのが『cancel
』メソッドです。このcancelメソッドが、どうして値の流れを止めることができるのか説明します。まず、『sink』メソッドの返り値をドキュメントで確認すると、『AnyCancellable型
』になっています。このAnyCancellable型はcancelメソッドを持っており、これを実行すると、値の流れを止めることができます。
今回の場合はsubjectAがcancelメソッドをsend(2)の直前で実行したため、subjectAに対して、send(2),send(3)を送ったとしても、sinkメソッドの処理が走らなかったというわけです。
片方のサブスクライバーの登録を無効化する
値の流れを二つの箇所で受け取りたいとし、尚且つ、subjectAに対しては途中で処理を無効化させたいときのサンプルコードです。
let subject = PassthroughSubject<Int,Never>()
let subjectA = subject.sink { number in
print("subjectA:",number)
}
let subjectB = subject.sink { number in
print("subjectB:",number)
}
subject.send(1)// B 1,A 1
subjectA.cancel()
subject.send(2)//B 2
subject.send(3)//B 3
subjectBでは全ての値を受け取り、subjectAではsend(1)以降のルールを行いたくないと言った場合です。sendで1が送られた後に、subjectAに登録の解除をさせます。
複数のサブスクライバーの登録を一気解除する
Set型のプロパティに追加
一つ一つキャンセルしてもいいですが、それが50個、100個と多くなっていくとさすがに骨が折れますよね。
なので、配列にAnyCancellable型の要素を追加していき、一気にcancelを実行すると楽にできそうです。しかし、ここで配列には追加していく要素は、キャンセルするために追加していくものなので、重複する意味はありません。(クリーニングのチケットを2枚もらっても、戻ってくる服は一枚しかない。みたいなイメージです。2枚もらう意味はあまりありませんよね。)この重複を避けるために使われるのがSet型です。重複した要素を自動で弾いてくれます。
*Set型に追加したい場合はinsertで要素を追加していきます。
forEachで取り出しキャンセル
setプロパティに追加された要素をforEachを使用して順次取り出し、キャンセルしていきます。
var set = Set<AnyCancellable>()
let subject = PassthroughSubject<Int,Never>()
let subjectA = subject.sink { number in
print("A",number)
}
let subjectB = subject.sink { number in
print("B",number)
}
let subjectC = subject.sink { number in
print("C",number)
}
set.insert(subjectA)
set.insert(subjectB)
set.insert(subjectC)
set.count //🟦3つのサブスクライバーが入っている状態
subject.send(10)
subject.send(20)
subject.send(30)
set.forEach{
$0.cancel() //🟦一気に登録を解除する
}
subject.send(40) //出力されない
実行結果-------*順番は固定されていません。
C 10
A 10
B 10
C 20
A 20
B 20
C 30
A 30
B 30
まとめ
- 値を流す(パブリッシャー)
- 登録された値の処理が書かれたオブジェクト(サブスクライバー)
- 登録の解除(cancel)
上の3つについて説明させてもらいました。次回は@Published
やオペレーターについてアウトプットしていきたいと思います!
間違っている箇所があれば、教えていただけると大変嬉しいですのでよろしくお願いします!