【Swift UI】Combineフレームワークの使い方!PublisherとSubscriberの違い
この記事からわかること
- SwiftのCombineフレームワークの使い方
- PublisherとSubscriberの違い
- Subject(被写体)とは?
- sendメソッドの使い方
- Observerパターン
- sinkメソッドの使い方
- RxSwiftとの違いと使い分け
index
[open]
\ アプリをリリースしました /
友達や家族の誕生日をメモ!通知も届く-みんなの誕生日-
posted withアプリーチ
Combineフレームワークとは?
SwiftのCombineフレームワーク
とは非同期処理やストリーム処理、データのバインディング、イベントハンドリングなどのリアクティブプログラミングに倣った機能を提供しているApple純正のフレームワークです。非同期処理やイベントドリブンな処理を実現できるため、ユーザーインタラクション(ユーザーの操作に対してシステムが反応を返すこと)やネットワーク通信などの処理に適しています。
このフレームワークはSwift5.0から導入され、Xcode11以降からはデフォルトで組み込まれるようになりました。そのため手動で導入する必要がなくimport文を記述するだけですぐに利用可能になります。
import Combine
CombineフレームワークはGoFのデザインパターンの1つ「Observerパターン」に基づいた機能を提供します。Observerパターンとはオブジェクトの状態変化を観測し変化したタイミングで別オブジェクトの状態を更新する設計を施すパターンです。Combineフレームワークでは「Publisher」が変化を監視し、「Subscriber」に対して通知する仕組みが用意されています。
Combineフレームワークのプロトコル名を見る限り正確には「Publish-Subscribeパターン」なのかも知れません。
コールバックとの違い
データベースへのCRUD処理など時間がかかり非同期で実行される処理の結果を取得したい場合にコールバックは活用されます。しかしコールバックはネストが深くなりがちでコールバックが幾重にも重なってしまうコールバック地獄を引き起こす可能性があります。Combineではそれをスッキリとさせるために利用されることが多いです。両者のコードの違いを見てみます。
CallBackの実装
func getCallBack(completion: @escaping (String) -> Void ) {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
completion("成功したよ")
}
}
受け取り側
getCallBack { str in
print(str)
}
Combineの実装
func getPublisher() -> Future<String, Never> {
return Future() { promise in
DispatchQueue.main.asyncAfter(deadline: .now() + 2) {
promise(.success("成功したよ"))
}
}
}
受け取り側
cancellable = getPublisher().sink { str in
print(str)
}
Publisher(発行者)とは?
protocol Publisher<Output, Failure>
Combineフレームワークにおける「Publisher(パブリッシャー:発行者)」はいわゆるデータストリームを生成するためのオブジェクトです。ストリームとはデータの入出力の流れを保持する抽象的なオブジェクトであり時間の経過と共に変化する一連の値の変化を配信(通知を送信)する特徴があります。
その流れは「データの変更から完了またはエラーまで」を一連の順序(シーケンス)として管理しておりマーブルダイアグラムとして表現されることが多いです。
Combineフレームワークでは複数のストリームを組み合わせたり、ストリームを加工したりすることも可能になっています。
Subscriber(購読者)とは?
protocol Subscriber<Input, Failure> : CustomCombineIdentifierConvertible
「Subscriber(サブスクライバー:購読者)」とはPublisherが送信するデータストリームを受け取るオブジェクトです。Publisherから送信されたデータに対して「データを加工して別の形式に変換する」「データをフィルタリングして特定の条件に一致するものだけを処理する」「複数のストリームを結合して新しいストリームを作成する」と言ったさまざまな処理を行うことが可能になります。またデータ変更が通知された時だけでなくエラーや完了を通知したときにも処理を実行することができます。
そしてCombineフレームワークでは「Publisher
」と「Subscriber
」がプロトコルとして定義されています。
PublisherとSubscriberの使用例
PublisherとSubscriberを使用したシンプルな例を作成してみました。Just
オペレーターは値を一回だけ発行するPublisherです。続いてSubscribers.Sink
メソッドで値を受け取った時や完了した時の処理を定義しています。最後に「値が一回だけ生成されるストリームを持つPublisher」に対して「String型を受け取り出力する処理を持つSubscriber」を設定することで変化を検知して処理が実行されています。
import Combine
// Publisherインスタンスを生成
let publisher = Just("Hello, World!")
// Subscriberインスタンスを生成
let subscriber = Subscribers.Sink<String, Never>(
receiveCompletion: { _ in },
receiveValue: { value in
print(value)
}
)
// PublisherインスタンスにSubscriberインスタンスを登録
publisher.subscribe(subscriber)
// Hello, World!
この方法ではPublisherとSubscriberが明確に分かれていたので分かりやすかったですが、Subjectを使用すると少しややこしくなります。
Subject(被写体)とは?
そもそもObserverパターンは対象となる2つのオブジェクトとして「観測される側(Subject)」と「観測する側(Observer)」に分けて考えられます。CombineフレームワークでもObserverパターンの説明として挙げられる「観測される側(Subject)」と同名のSubject
プロトコルが定義されています。
protocol Subject<Output, Failure> : AnyObject, Publisher
Combineフレームワークで定義されているSubject
とは「Publisherプロトコルを継承し、値を発行する役割(sendメソッド)」と、「他のPublisherが送信するデータストリームを受け取るSubscriber」の両方の役割を持つオブジェクトです。
Combineフレームワークで実際に使用されるPublisherは後述するSubject
やNotificationCenterクラスのpublisher
メソッドなどが継承しています。NotificationCenterクラスでは簡単に「アプリがアクティブになった」などのイベントを検知することが可能です。
sendメソッドの役割
Subject
プロトコルに定義されているsend
メソッドはSubjectが生成したデータを購読しているSubscriberに配信するために使用されるメソッドです。以下のような4種類が定義されています。
func send(Self.Output) // サブスクライバーに値を送信
func send() // サブスクライバーにvoid値を送信
func send(subscription: Subscription) // サブスクライバーにサブスクリプションを送信
func send(completion: Subscribers.Completion<Self.Failure>) // サブスクライバーにコールバック関数を送信
sendメソッドは、Subjectが完了状態になっている場合や、エラーが発生している場合には呼び出すことができません。
Subjectの種類
Subjectには2つの種類があり、役割が異なります。実際の使用方法は後述していきます。
- CurrentValueSubject:最新値を保持し、出力時に最新値が出力
- PassthroughSubject:値を保持せず、受け取ったら値をそのまま出力
2つの使い分けの使用例を考えてみます。CurrentValueSubjectは常に最新値を保持してくれるのでログイン状態や設定値など参照する可能性のある場合に活用できそうです。
PassthroughSubjectは値自体を保持しないので常に最新の値に更新され続けるようなWeb API取得などが良いのかもしれません。
CurrentValueSubjectの使い方
公式リファレンス:CurrentValueSubjectクラス
final class CurrentValueSubject<Output, Failure> where Failure : Error
CurrentValueSubject
クラスは値が更新されるたびに要素を公開するシンプルなSubjectです。このCurrentValueSubjectオブジェクトは常に最新の値をvalueプロパティに保持しています。インスタンス化する際にCurrentValueSubjectの後に対象となる値のデータ型と初期値を渡します。
// 初期値を「0」でインスタンス化
let subject = CurrentValueSubject<Int, Never>(0)
エラーが発生する可能性がない場合はNever
を渡します。イニシャライザや主なプロパティ、メソッドは以下の通りです。
init(Output) // 指定された初期値で現在の値のサブジェクトを作成します。
var value: Output // このサブジェクトによってラップされた値で、変更されるたびに新しい要素として公開されます。
func send(Output) // サブスクライバーへの要素の配信
単純に値を参照する
まずは単純にsubject.value
で値を参照してみます。ここではsend
メソッドが値を更新するためのメソッドだと思うと理解しやすいかもしれません。更新された後にはvalue
で取得する値も変化していることを確認できます。
import Combine
let subject = CurrentValueSubject<Int, Never>(0)
print(subject.value) // 0
subject.send(1)
print(subject.value) // 1
subject.send(2)
print(subject.value) // 2
Subjectを購読する
公式リファレンス:Publisher.sink(receiveValue:)メソッド
続いてこのSubjectを購読(サブスクライブ)してみます。今回のsubjectを購読するためのサブスクライバーはPublisher
プロトコルの持つsink(receiveValue:)
メソッドを使用して定義します。引数のコールバック関数は値を受け取ったときに実行されます。ここからはPassthroughSubjectでも基本的には同じになります。
import Combine
let subject = CurrentValueSubject<Int, Never>(0)
let cancellable = subject
.sink { value in
print(value)
}
subject.send(1)
subject.send(2)
// 0
// 1
// 2
購読を停止させる
sink
メソッドの返り値であるAnyCancellable
オブジェクトからcancel
メソッドを呼び出すことで購読状態のSubscriberに対して、Publisherからの値を受信することを停止させることができます。
cancellable.cancel()
subject.send(3) // 何も出力されない
値の変化(ストリーム)を完了させる
ストリームで観測していた値の変化を完了させてみます。先程の購読するためのsink
メソッドの引数を少し変更し、完了時と値の受け取り時に実行する処理を追加してみました。エラーは発生しない(Never)のでエラー処理はありません。
import Combine
let subject = CurrentValueSubject<Int, Never>(0)
let cancellable = subject.sink(
receiveCompletion: { completion in
switch completion {
case .finished:
print("Completed")
}
},
receiveValue: { value in
print("Received value: \(value)")
}
)
// Received value: 0
subject.send(1) // Received value: 1
subject.send(2) // Received value: 2
subject.send(3) // Received value: 3
subject.send(completion: .finished) // Completed
subject.send(4) // 何も表示されない
データバインディング:assignメソッド
Publisherから流れてくるデータをオブジェクトに紐づける(データバインディングする)にはassign
メソッドを使用します。
このメソッドを使うことで変化する値と指定したオブジェクトのプロパティをリンクさせることが可能になります。
class Sample {
var count:Int = 0
}
let obj = Sample()
subject.assign(to: \.count,on: obj)
print(obj.count)
subject.send(1) // Received value: 1
print(obj.count) // 1
subject.send(2) // Received value: 2
print(obj.count) // 2
subject.send(3) // Received value: 3
print(obj.count) // 3
subject.send(completion: .finished) // Complete
sinkメソッドの役割
公式リファレンス:sink(receivecompletion:receivevalue:)
何度か登場していたsink
メソッドは受け取った通知に対しての処理を実装するためのメソッドです。引数にはクロージャー単位で処理を定義できるようになっており、引数違いで以下の2つが定義されています。
func sink(receiveValue: @escaping ((Self.Output) -> Void)) -> AnyCancellable
1つ目は受け取った値に対してのみ任意の処理を実行できるクロージャーを持たせることができ、2つ目は完了またはエラー時にも処理を実行できるクロージャーを持たせることができます。
func sink(
receiveCompletion: @escaping ((Subscribers.Completion<Self.Failure>) -> Void),
receiveValue: @escaping ((Self.Output) -> Void)
) -> AnyCancellable
Swift UIとCombineフレームワーク
Swift UIを使用しているとクラスのプロパティの変化をリアルタイムにビューに反映させるために @Published
を使用したことがあると思います。このプロパティラッパこそがCombineフレームワークから提供されています。
class Article1:ObservableObject{
@Published var title:String = ""
var body:String = ""
}
RxSwiftとの違いと使い分け
似たようなObserverパターンを構築するために「RxSwift」と呼ばれるライブラリが有名です。RxSwiftはApple純正のライブラリではなく、MicrosoftがリリースしているReactiveXに含まれるSwift版のライブラリです。
CombineフレームワークはSwift独自のシンタックスや基本構文(関数など)を利用することが可能です。一方、RxSwiftはReactiveXの標準ライブラリを基にしているため、Swift言語とは異なるオペレーターやメソッドも持っているため、独自の文法やAPIを理解する必要があります。
Combineフレームワーク
- Apple純正
- Swift言語の基本構文を流用
- 手動でインポートする必要なし
RxSwiftライブラリ
- MicrosoftのReactiveX
- 独自の構文もあり
- 手動でインポートする必要あり
RxSwiftは他の言語のバージョンと共通の文法を持っているため、異なる言語バージョン間の移植性を考慮する必要がある場合に有用です。
使い分け
- Combine:iOSやmacOSなどApple限定アプリの開発時
- RxSwift:他言語を使用してのアプリ開発も想定している時
実装例
Combineフレームワークを使って練習がてら「Swift UIでボタンタップでイベントを発火して真偽値を入れ替える」コードを書いてみました。これは正直意味のないコードですが何かの参考になるかもしれないので載せておきます。
import Combine
import SwiftUI
class Test: ObservableObject {
static let shared = Test()
@Published var subject = CurrentValueSubject<Bool, Never>(false)
private var cancellables = Set<AnyCancellable>()
init() {
subject
.sink { [weak self] _ in
self?.objectWillChange.send()
}
.store(in: &cancellables)
}
func sendTrue() {
subject.send(true)
}
func sendFalse() {
subject.send(false)
}
}
struct TestComBineView: View {
@ObservedObject var test = Test.shared
var body: some View {
VStack {
Text(test.subject.value ? "YES" : "NO")
ChildView()
}
}
}
struct ChildView: View {
@ObservedObject var test = Test.shared
var body: some View {
VStack {
Button("Toggle") {
if test.subject.value {
test.sendFalse()
} else {
test.sendTrue()
}
}
}
}
}
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。