はじめに

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章 Combine の基礎

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 です。

subjectsend メソッドを呼ぶとイベントが送信されます。このコードでは、1 文字の値を送るイベントを 5 回に分けて送信しています。

Receiver クラスの中で、subjectsink メソッドを呼んでいます。sink メソッドは、イベントを受信したときの処理を指定します。このコードでは、print メソッドで値をコンソールに出力しています。

1.2 Publisher

Combine の用語で、イベントを送信するオブジェクトを「Publisher」と呼びます。また、Publisher がイベントを送信することを「publish」と呼びます。リスト1.1 で出てきた PassthroughSubject は Publisher です。PassthroughSubjectsend メソッドを呼ぶとイベントを 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.3publisher は、指定の 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.5publisher は、一定期間ごとにイベントを publish します。なお、実際に Timer を動作させるために connect メソッドを呼んでいます(代わりに autoconnect メソッドを使うこともできます)。

これ以外に、URLSessionSequence にも 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.15subjectInt 型の値を publish しています。subjectmap メソッドは、別の 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 の基礎をざっとおさらいしました。

試し読みはここまでです。
この続きは、製品版でお楽しみください。