本書は Combine をこれから学んでみようという人のための本です。
Combine は Swift でリアクティブプログラミングを行うためのフレームワークです。Apple プラットフォーム(iOS、iPadOS、watchOS、tvOS、macOS)向けの App を開発するときに、様々な処理を宣言的に記述することが可能になります。
Combine が登場したのは 2019 年 6 月の WWDC でした。登場時点では、最新の OS でしか動作しなかったため、まだ実際の App 開発で採用するのは難しい状況でした。それから 1 年たって 2020 年 9 月現在、もうすぐ次のバージョンの OS がリリースされようとしています。これからの App 開発において、Combine が動作しない OS はサポートしないという選択が現実的に可能な状況になりました。
したがって、今は Combine をはじめるちょうど良い機会です。本書は Combine を使い始めるための基本的な事柄について解説します。
Combine については、Apple が WWDC のセッションで解説しているほか、解説書や解説記事が既に存在しています。それらを見て学ぶのが一番良い方法でしょう。
ただし、ここで落とし穴があります。
既存の解説の多くは、率直に言って、難しいです。
Combine の解説はたいていの場合、登場する用語や概念の説明から始まります。ここで、たくさんの用語や概念が一度に押し寄せてきます。この最初の導入部分でつまづきが発生しやすいです。結果として、Combine は敷居が高いものになってしまっている、と考えています。
基本的な事柄をもっと分かりやすく説明する解説書が存在するべきだ、という想いから、本書を執筆しました。本書では導入部分をできるだけ丁寧に説明するように心がけました。さらに説明の際に、具体的なコードを先に挙げるようにしました。
また、日本語で書かれた解説はまだそれほど多くありません。そこで、日本語で読める Combine の本を増やしたいという想いも、本書を執筆した動機のひとつです。
本書を読んでいただくにあたっての前提を記載しておきます。
Swift や Apple プラットフォーム向け App 開発の基本はある程度知っているものとします。また、開発環境として Mac と Xcode 11 以降が必要です。
Apple Developer Program への加入は、必須ではありません。ただ、加入していれば iPhone などの Apple デバイス上で動作を試すことが可能になります。
Swift で使えるリアクティブプログラミングのフレームワークは、Combine 以外にも RxSwift や ReactiveSwift などがあります。これらはサードパーティ製のフレームワークです。Combine が登場するよりも前から存在しており、広く使われてきました。一方、Combine は後発ですが標準フレームワークとして組み込まれているため、今後使われるようになっていくと考えられます。
もし既に RxSwift や ReactiveSwift を使ったことがあれば、Combine を理解する助けになるでしょう。ただし、用語や考え方が異なる点もありますので注意してください。
また、それらを使ったことがなくとも、本書を読んでいただいて Combine を理解していただけると考えています。
私たちプログラマが新しいフレームワークを学ぼうとするとき、何を最初にやるべきでしょうか。プログラマであれば、言葉であれこれ説明する前に、実際にコードを書いて動かしてみるのが理解が早いだろうと考えています。
そこで、Combine を Xcode Playground を使って試してみます。
ここで早速、Combine が標準フレームワークであることが役に立ちます。サードパーティ製のフレームワークだったなら、まず何らかの方法でフレームワークを使えるようにしなければならないところでした。Combine は標準フレームワークなので、Xcode さえあればすぐに Combine のコードを書いて動かしてみることができます。
なお、Xcode の Playground の代わりに iPad(または Mac)の Swift Playgrounds を使うことも可能です。本書では Xcode を使うことにします。
では、お手元に Mac と Xcode 11 以降を用意してください。
Xcode で Playground を新規作成します。中身は空で良いため Blank を指定して作成します。そして、次のコードを書いて実行します。
import Combine
let subject = PassthroughSubject<String, Never>()
subject
.sink { value in
print("Received value:", value)
}
subject.send("あ")
subject.send("い")
subject.send("う")
subject.send("え")
subject.send("お")
Playground 上でコードが無事に動作すれば、出力として次が表示されます。
では次に、このコードが何をしているかを見ていきましょう。
その前に、まだ Combine が何をするフレームワークなのかを説明していませんでした。Combine は、オブジェクトからオブジェクトにイベントを伝える仕組みを提供します。ここでいうイベントとは、GUI の操作やネットワーク通信など、App 内で発生した何らかの変化を伝えるものです。GUI や通信については後述することとして、ひとまずは、オブジェクトに何らかの値を渡すイベントを考えていきましょう。
リスト1.1 では、文字列の値を渡すイベントを扱います。イベントを伝える手段として Combine の PassthroughSubject
クラスを使っています。
3 行目で定義している subject
が PassthroughSubject
のインスタンスで、イベントを中継する役目を果たします。この定義の場合、String
型の値を送受信します。
6 行目の sink
メソッドは、subject
でイベントを受信した際に実行する処理を指定します。このコードでは、受信した String
型の値を print
を使って表示する処理を行っています。
10 行目の send
メソッドは、subject
で String
型の値を送信します。この値が受信処理に渡されて、6 行目で指定していた処理が実行されることになります。その結果が リスト1.2 です。
こうして、オブジェクトからオブジェクトに値を渡すイベントの処理が Combine で書けました。これが Combine を使ったコードのもっとも簡単な例のひとつです。
先ほどのコードでは、イベントで値を渡すだけでした。これを少し発展させて、イベントの完了を渡すようにしてみます。
import Combine
let subject = PassthroughSubject<String, Never>()
subject
.sink(receiveCompletion: { completion in
print("Received completion:", completion)
}, receiveValue: { value in
print("Received value:", value)
})
subject.send("あ")
subject.send("い")
subject.send("う")
subject.send("え")
subject.send("お")
subject.send(completion: .finished)
無事に動作すれば、出力として次が表示されます。
リスト1.3 の 6 行目の sink
メソッドでは、先ほどと異なり、ふたつのクロージャを指定しています。receiveValue
は先ほどと同様にイベントを受信した際に実行する処理です。receiveCompletion
が新しく増えたクロージャで、イベント完了を受信した際に実行する処理です。
17 行目の send
メソッドでは、値を送信する代わりにイベント完了を意味する .finished
を送信しています。この結果、5 行目で指定していたイベント完了を受信した際の処理が実行されることになります。
なお、イベント完了のあとでは、send
メソッドで値を送信しても、受信処理は行われません。例えば、リスト1.3 の 12 行目から 17 行目を次のように変更してみます。
subject.send("あ")
subject.send("い")
subject.send("う")
subject.send(completion: .finished)
subject.send("え")
subject.send("お")
実行してみると、イベント完了後の値の受信処理は行われないことが分かります。
イベントの完了は .finished
の他にもうひとつあります。エラー終了です。
import Combine
enum MyError: Error {
case failed
}
let subject = PassthroughSubject<String, MyError>()
subject
.sink(receiveCompletion: { completion in
print("Received completion:", completion)
}, receiveValue: { value in
print("Received value:", value)
})
subject.send("あ")
subject.send("い")
subject.send("う")
subject.send("え")
subject.send("お")
subject.send(completion: .failure(.failed))
今度は出力として次が表示されます(__lldb_expr_1
の部分は環境によって異なります)。
リスト1.7 の 7 行目で PassthroughSubject
インスタンスを生成する際、イベントの値の型を String
に、エラーの型を MyError
に指定しています。
21 行目の send
メソッドでは、値を送信する代わりにエラーを意味する .failure
を送信しています。エラーの場合も .finished
と同じ receiveCompletion
クロージャが実行されます。
ところで、リスト1.1 や リスト1.3 ではエラーの型を Never
にしていました。これは、エラーが発生しないことを意味する特別な指定です。リスト1.1 で使っていた 1 引数の sink
は、エラーの型が Never
の場合にのみ使用可能です。
Combine が扱うイベントについて整理しておきます。送受信されるイベントの内容は、以下の 3 種類があります。
.finished
).failure
)値の型は、送受信に使うクラスによって指定されます。ここまでの例では、String
型を指定していました。
エラーの型も同様に、送受信に使うクラスによって指定されます。エラーの型を Never
にすることで、エラーが起こらないイベント(値とイベント完了のみ)を送受信できます。
これ以降の章で Combine の用語や概念を説明していきますが、このイベントという概念が基本となります。まずはこれをおさえておいてください。