はじめに
Combine は Swift を使ってリアクティブプログラミングを行うためのフレームワークです。2019 年 6 月の WWDC で登場して、注目を浴びました。
本書では、Combine を iOS App 開発に活用する手法を述べます。特に、UIKit での開発に Combine を組み合わせて使う事例を考えます。
iOS App 開発では、UI 層とモデル層とのあいだでデータをやり取りする手段が意外と難しいところです。UIKit はそのための仕組みを、あまり明示的には提供していませんでした。ここに Combine を導入することで、UI とモデルとを関連づけることが容易になります。これによって、UIKit を使った開発でのアーキテクチャ設計を改善できます。
本書で述べる内容は、それほど難しいものではありません。しかし、Combine の書籍や記事は内容が高度なものが多く、敷居が高い場合が多いです。基本的なことをできるだけ丁寧に説明した本があったら良いのではないか、と考えて本書を執筆しました。
UIKit と SwiftUI
iOS App 開発では、UI フレームワークとして UIKit が使われてきました。一方、2019 年 6 月の WWDC で、新しい UI フレームワークとして SwiftUI が発表されました。現在の iOS App 開発では、UIKit と SwiftUI のどちらか(あるいは両方)を使って開発することになります。
将来的には、UIKit に代わって SwiftUI が主流になることも予想できます。しかし一方で、UIKit は現在も iOS App 開発の現場で多く使われています。本書では UIKit に焦点を当てることにします。
なお、SwiftUI は暗黙的に Combine を活用しています。そのため、将来的に UIKit から SwiftUI に移行するとしても、UIKit と Combine との組み合わせに慣れ親しんでいれば楽に移行できるでしょう。
本書の前提知識
本書を読んでいただくにあたっての前提を記載しておきます。
Swift の基本はある程度知っているものとします。また、UIKit を使った iOS App 開発の基本もある程度知っているものとします。
Combine については本書の最初の章で簡単にまとめてあります。もしも Combine に触れるのが初めてであまり自信がないということであれば、既刊の拙書「Combine をはじめよう」でより丁寧に基本を説明しています。ぜひ読んでみてください。
iOS の動作対象バージョン
2020 年 12 月現在、iOS の最新バージョンは 14 です。iOS 端末ユーザの多くは、最新バージョンまたはそのひとつ前のバージョンを利用しています。したがって、動作対象を iOS 13 以降とする前提はそれほど無理のないことだと考えます。
Combine は iOS 13 以降で動作します。iOS 12 以前では動作しません。このため本書では、開発する iOS App の動作対象を iOS 13 以降とします。
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.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 の基礎をざっとおさらいしました。