1.1 Combine とは
Combine は、オブジェクトからオブジェクトにイベントを伝える仕組みを提供します。ここでいうイベントとは、GUI の操作やネットワーク通信など、App 内で発生した何らかの変化を伝えるものです。
Combine を使ったコードの例を挙げます。ここでは、文字列の値を渡すイベントを扱います。このコードは Xcode の Playground で動かすことができます。
リスト1.1 Swift コード
import Combine
let subject = PassthroughSubject<String, Never>()
final class Receiver {
private var subscriptions = Set<AnyCancellable>()
init() {
subject
.sink { value in
print("Received value:", value)
}
.store(in: &subscriptions)
}
}
let receiver = Receiver()
subject.send("あ")
subject.send("い")
subject.send("う")
subject.send("え")
subject.send("お")
リスト1.2 出力結果
Received value: あ
Received value: い
Received value: う
Received value: え
Received value: お
リスト1.1 では、イベントを送信するオブジェクトは subject
で、イベントを受信するオブジェクトは receiver
です。
subject
の send
メソッドを呼ぶとイベントが送信されます。このコードでは、1 文字の値を送るイベントを 5 回に分けて送信しています。
Receiver
クラスの中で、subject
の sink
メソッドを呼んでいます。sink
メソッドは、イベントを受信したときの処理を指定します。このコードでは、print
メソッドで値をコンソールに出力しています。
1.2 Publisher
Combine の用語で、イベントを送信するオブジェクトを「Publisher」と呼びます。また、Publisher がイベントを送信することを「publish」と呼びます。リスト1.1 で出てきた PassthroughSubject
は Publisher です。PassthroughSubject
は send
メソッドを呼ぶとイベントを publish します。
別の Publisher も見てみましょう。標準フレームワークで Publisher を生成するものがいくつかあります。そのうちのひとつ、NotificationCenter
クラスの Publisher を使ったコードを挙げます。
リスト1.3 Swift コード
import Combine
import Foundation
let myNotification = Notification.Name("MyNotification")
let publisher = NotificationCenter.default
.publisher(for: myNotification)
final class Receiver {
private var subscriptions = Set<AnyCancellable>()
init() {
publisher
.sink { value in
print("Received value:", value)
}
.store(in: &subscriptions)
}
}
let receiver = Receiver()
NotificationCenter.default
.post(Notification(name: myNotification))
リスト1.4 出力結果
Received value: name = MyNotification, object = nil, ...
リスト1.3 の publisher
は、指定の Notification が post されるとイベントを publish します。
もうひとつ、Timer
クラスの Publisher を使ったコードを挙げます。
リスト1.5 Swift コード
import Combine
import Foundation
let publisher = Timer.publish(every: 1, on: .main, in: .common)
final class Receiver {
private var subscriptions = Set<AnyCancellable>()
init() {
publisher
.sink { value in
print("Received value:", value)
}
.store(in: &subscriptions)
}
}
let receiver = Receiver()
publisher.connect()
リスト1.6 出力結果
Received value: 2020-12-24 09:00:00 +0000
Received value: 2020-12-24 09:00:01 +0000
Received value: 2020-12-24 09:00:02 +0000
Received value: 2020-12-24 09:00:03 +0000
リスト1.5 の publisher
は、一定期間ごとにイベントを publish します。なお、実際に Timer を動作させるために connect
メソッドを呼んでいます(代わりに autoconnect
メソッドを使うこともできます)。
これ以外に、URLSession
や Sequence
にも Publisher が標準で用意されています。また、Publisher を自分で作ることもできます。
1.3 Subscription
Combine の用語で、イベントの受信処理を指定することを「subscribe」と呼びます。また、subscribe したときの戻り値を「subscription」と呼びます。リスト1.1 では、sink
メソッドで subscribe しています。sink
の戻り値が subscription です。
リスト1.1 で subscription に対して store
メソッドを呼んでいます。仮に、このメソッドを呼んでいなかったらどうなるか、Xcode Playground を使って試してみましょう。
リスト1.7 Swift コード
import Combine
let subject = PassthroughSubject<String, Never>()
final class Receiver {
// <削除> private var subscriptions = Set<AnyCancellable>()
init() {
subject
.sink { value in
print("Received value:", value)
}
// <削除> .store(in: &subscriptions)
}
}
let receiver = Receiver()
subject.send("あ")
subject.send("い")
subject.send("う")
subject.send("え")
subject.send("お")
リスト1.8 出力結果
リスト1.7 は、Xcode Playground で実行しても何も出力しません。
sink
の戻り値の subscription に対して何もしないで破棄してしまうと、subscribe で指定した受信処理が破棄されます。store
メソッドを使うと、subscription を保持できます。これによって、受信処理が保持されます。store
メソッドを使っている リスト1.1 では受信処理が期待どおりに実行されます。
複数の subscribe を行う場合も、それぞれの subscription を store
メソッドで保持できます。
リスト1.9 Swift コード
import Combine
let subject1 = PassthroughSubject<String, Never>()
let subject2 = PassthroughSubject<String, Never>()
final class Receiver {
private var subscriptions = Set<AnyCancellable>()
init() {
subject1
.sink { value in
print("[1] Received value:", value)
}
.store(in: &subscriptions)
subject2
.sink { value in
print("[2] Received value:", value)
}
.store(in: &subscriptions)
}
}
let receiver = Receiver()
subject1.send("あ")
subject2.send("い")
subject1.send("う")
subject2.send("え")
subject1.send("お")
リスト1.10 出力結果
[1] Received value: あ
[2] Received value: い
[1] Received value: う
[2] Received value: え
[1] Received value: お
リスト1.9 のように、異なる subscription を同じ subscriptions
という Set
に格納して問題ありません。subscriptions
が破棄されたとき、そこに格納されている subscription が全て破棄されます。
1.4 イベント
ここまでに出てきた Publisher は、イベントとして何らかの値を送信していました。Combine で扱うイベントには、他にもイベント完了とエラーがあり、全部で以下の 3 種類です。
- 値
- イベント完了(
.finished
) - エラー終了(
.failure
)
値を送信する以外の 2 つをここで見ておきます。まず、イベント完了(.finished
)を扱うコードの例を挙げます。
リスト1.11 Swift コード
import Combine
let subject = PassthroughSubject<String, Never>()
final class Receiver {
private var subscriptions = Set<AnyCancellable>()
init() {
subject
.sink(receiveCompletion: { completion in
print("Received completion:", completion)
}, receiveValue: { value in
print("Received value:", value)
})
.store(in: &subscriptions)
}
}
let receiver = Receiver()
subject.send("あ")
subject.send("い")
subject.send("う")
subject.send(completion: .finished)
subject.send("え")
subject.send("お")
リスト1.12 出力結果
Received value: あ
Received value: い
Received value: う
Received completion: finished
イベント完了(.finished
)はイベント送受信の終了を意味します。リスト1.11 で .finished
よりも後に send
されたイベントは送受信されません。
また、sink
メソッドがこれまでのものと異なり、2 引数のものを使っています。receiveValue
引数はこれまで通りの値のイベントを処理するクロージャです。receiveCompletion
引数に .finished
を処理するクロージャを指定しています。
もうひとつ、エラー終了(.failure
)を扱うコードの例を挙げます。
リスト1.13 Swift コード
import Combine
enum MyError: Error {
case failed
}
let subject = PassthroughSubject<String, MyError>()
final class Receiver {
private var subscriptions = Set<AnyCancellable>()
init() {
subject
.sink(receiveCompletion: { completion in
print("Received completion:", completion)
}, receiveValue: { value in
print("Received value:", value)
})
.store(in: &subscriptions)
}
}
let receiver = Receiver()
subject.send("あ")
subject.send("い")
subject.send("う")
subject.send(completion: .failure(.failed))
subject.send("え")
subject.send("お")
リスト1.14 出力結果
Received value: あ
Received value: い
Received value: う
Received completion: failure(__lldb_expr_1.MyError.failed)
エラー終了(.failure
)もイベント送受信の終了となります。リスト1.13 で .failure
よりも後に send
されたイベントは送受信されません。
subject
のエラー型を MyError
にしているため、MyError
型のエラーを送受信しています。なお、エラー型を Never
にした場合は、エラー終了しないことを意味します。
1.5 Operator
Combine では、Publisher を subscribe する前に何らかの加工を行えます。別の言い方をすると、Publisher を加工して別の Publisher に変換できます。
Publisher を別の Publisher に変換するメソッドを、Combine の用語で「Operator」と呼びます。
Operator の例として、map
メソッドを使ったコードを挙げます。
リスト1.15 Swift コード
import Combine
import Foundation
let subject = PassthroughSubject<Int, Never>()
final class Receiver {
private var subscriptions = Set<AnyCancellable>()
private let formatter = NumberFormatter()
init() {
formatter.numberStyle = .spellOut
subject
.map { value in
self.formatter.string(
from: NSNumber(integerLiteral: value)) ?? ""
}
.sink { value in
print("Received value:", value)
}
.store(in: &subscriptions)
}
}
let receiver = Receiver()
subject.send(0)
subject.send(1)
subject.send(2)
subject.send(3)
subject.send(4)
リスト1.16 出力結果
Received value: zero
Received value: one
Received value: two
Received value: three
Received value: four
リスト1.15 の subject
は Int
型の値を publish しています。subject
の map
メソッドは、別の Publisher に変換して返します。ここでは、String
型の値を publish する Publisher に変換しています。
1.6 Combine のコンセプト
Combine では以下の 3 つの要素が重要です。
- Publisher
- Operator
- Subscriber
これらについては既にここまでに述べました。Subscriber という用語は明示的には出てきませんでしたが、Publisher に対して subscribe を行うオブジェクトを Subscriber と呼びます。
Combine は非同期イベントを扱うためのフレームワークです。
iOS App 開発ではさまざまな非同期イベントを処理する必要があります。Foundation や UIKit で非同期イベントを扱う仕組みは、Target-Action、Delegate、Notification Center、GCD、Callback などさまざまな方法が使われています。
Combine は、それらとはまた異なる、別の非同期イベント処理の実装方法を提供します。実のところ、Combine の記法ですべて統一できます。記法を統一することで、Publisher や Subscriber によるイベント処理や Operator による柔軟な操作が、すべての非同期イベントに対して行えるようになります。
1.7 この章のまとめ
この章では、Combine の基礎をざっとおさらいしました。