【Swift UI/Combine】PassthroughSubjectとは?store(in:)メソッド
この記事からわかること
- SwiftのCombineフレームワーク
- PassthroughSubjectの特徴と使い方
- CurrentValueSubjectとの違い
- store(in:)メソッドの使い方
- &(アンパサンド)とは?
index
[open]
\ アプリをリリースしました /
友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-
posted withアプリーチ
SwiftのCombineフレームワークのPassthroughSubject
についてまとめていきます。まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
PassthroughSubjectとは?
final class PassthroughSubject<Output, Failure> where Failure : Error
PassthroughSubject
とは非同期処理やデータバインディングなどための機能を提供するCombineフレームワークのPublisherの1つです。Publisherはデータストリームを生成するためのオブジェクトであり、時間の経過と共に変化する一連の値の変化を配信(通知を送信)する特徴があります。
さらにPublisherの中のSubject(被写体)にカテゴライズされるのがPassthroughSubject
です。Combineフレームワークで定義されているSubject
とは「Publisherプロトコルを継承し、値を発行する役割」と、「他のPublisherが送信するデータストリームを受け取るSubscriber」の両方の役割を持つオブジェクトです。
同じSubjectの仲間にCurrentValueSubject
が定義されています。Combineフレームワークに関するPublisherやSubject、ストリームなどたくさんの単語が出てきましたが詳細は以下を参考にしてください。
おすすめ記事:【Swift UI】Combineフレームワークの使い方!PublisherとSubscriberの違い
PassthroughSubject:ここまでのまとめ
- CombineフレームワークのPublisherの1つ
- Publisherの中のSubject(被写体)にカテゴライズ
- 値の発行と購読、両方可能
特徴とCurrentValueSubjectとの違い
CurrentValueSubject
とPassthroughSubject
、2つの大きな違いは値を保持するかどうかです。
- CurrentValueSubject:最新値を常に保持し、出力時に最新値が出力
- PassthroughSubject:値を保持せず、受け取ったら値をそのまま出力
CurrentValueSubjectは常に最新値を保持してくれるのでログイン状態や設定値など参照する可能性のある場合に活用できそうです。
PassthroughSubjectは値自体を保持しないので常に最新の値に更新され続けるようなWeb API取得や一度きりの状態変化を取得したい場合などが良いのかもしれません。
PassthroughSubjectの役割
PassthroughSubject
クラスは値の変更を検知する箱のようなイメージです。箱の中に値が格納されたことを周りに知らせる(ブロードキャストする)役割(sendメソッド)や、その箱の変更を観測し変更があった場合に任意の処理を行わせる役割(sinkメソッド)が用意されています。
例えば以下はPassthroughSubject
クラスを使用した例です。
// PassthroughSubjectを定義
let subject = PassthroughSubject<Int, Never>()
// PassthroughSubjectを購読
let cancellable = subject.sink { value in
print("値は:", value)
}
// 値を発行
subject.send(1)
subject.send(2)
subject.send(3)
// 購読をキャンセル
cancellable.cancel()
// もう一度値を発行(購読がキャンセルされているため表示されない)
subject.send(4)
値は: 1
値は: 2
値は: 3
PassthroughSubject
は定義の通り、<Output, Failure>
の2つのデータ型を保持します。Output
にはString
やInt
、値が存在しないVoid
などを渡すことができ、Failure
には任意のエラー型を渡すことが可能です。
final class PassthroughSubject<Output, Failure> where Failure : Error
Swift UIで使用してみる
Swift UIの中で使用してみたいと思います。今回はWebPageState
クラスのプロパティにPassthroughSubject
クラスを適応させてみたいと思います。またアプリ内でシングルトンになるようにshared
プロパティを定義しておきます。
class WebPageState {
static let shared = WebPageState()
public let reload = PassthroughSubject<Void, Never>()
}
異なるView同士でイベント通知を検知し合えるのがメリットの1つなので2つのビューを定義してみました。本来ならそれぞれのViewModelを介することが多いかもしれませんがわかりやすくそのまま使用しています。TestWebView
側ではonAppear
のタイミングで購読を開始しています。この際に返却されるAnyCancellable
オブジェクトをViewのプロパティに格納しておきます。
struct TestWebView: View {
private let state = WebPageState.shared
@State var cancellable:AnyCancellable?
var body: some View {
VStack{
Text("WebView")
TestButtonView()
}.onAppear {
// Viewのプロパティとして受け取らないと検知できない
// let cancellable = state.reload.sink { ×
cancellable = state.reload.sink {
print("Webページのリロード処理を実行する")
}
}.onDisappear{
// 破棄されるタイミングで購読をキャンセルする
cancellable?.cancel()
}
}
}
struct TestButtonView: View {
private let state = WebPageState.shared
var body: some View {
VStack{
Button {
state.reload.send()
} label: {
Text("イベントを発行")
}
}
}
}
発行する側は特に変わったところはありません。このように異なるViewやクラスなどからイベントを検知してそれに伴う処理が実行できるようになります。
管理する対象を増やす
ではWebPageState
クラスにPassthroughSubject
を追加してみます。
class WebPageState {
static let shared = WebPageState()
public let reload = PassthroughSubject<Void, Never>()
public let back = PassthroughSubject<Void, Never>()
}
単純に以下のように実装するとcancellable
の中身が上書きされてreload
がイベントを検知できなくなってしまいます。
struct TestWebView: View {
private let state = WebPageState.shared
@State var cancellable:AnyCancellable?
var body: some View {
VStack{
Text("WebView")
TestButtonView()
}.onAppear {
cancellable = state.reload.sink {
print("Webページのリロード処理を実行する")
}
cancellable = state.back.sink {
print("Webページのバック処理を実行する")
}
}.onDisappear{
cancellable?.cancel()
}
}
}
store(in: &cancellables)
動作するようにするにはstore(in:)
メソッドを使用します。このメソッドはAnyCancellable
をSetの中に自動で格納してくれます。定義を見てみるとinout
が指定されているので「参照渡し」になります。呼び出す際には&
(アンパサンド)を変数の前に付与する必要があります。
final func store(in set: inout Set<AnyCancellable>)
実装コードは以下のようになります。キャンセルする方法もremoveAll
メソッドに変更しています。
struct TestWebView: View {
private let state = WebPageState.shared
@State var cancellables:Set<AnyCancellable>
var body: some View {
VStack{
Text("WebView")
TestButtonView()
}.onAppear {
state.reload.sink {
print("Webページのリロード処理を実行する")
}.store(in: &cancellables)
state.back.sink {
print("Webページのブラウザバック処理を実行する")
}.store(in: &cancellables)
}.onDisappear {
cancellables.removeAll()
}
}
}
ご覧いただきありがとうございました。