It’s now or never

IT系の技術ブログです。気になったこと、勉強したことを備忘録的にまとめて行きます。

【iOS】【Swift】Combineの基本的な使い方メモ

概要

iOS13から Combine というフレームワークが追加されました。
RxSwiftやRxJavaなどのRx系のライブラリを使わずとも純正のフレームワークでReactive Programingができるようになるというものです。

Rx系は、そこまで経験がないので、まずは基本的な使い方からサンプルを書いてみました。

環境

  • iOS: 13.5
  • Swift: 5.2.4

基本的な使い方(サンプルコード)

func first() -> AnyPublisher<String, Error> {
    // NOTE: 成功時にString型の値を返す、失敗時は、Errorを返す Futuerの作成
    return Future<String, Error> { promise in
        promise(.success("1"))
    }
    // NOTE: AnyPublisher型への変換
    .eraseToAnyPublisher()
}

func second(_ value: String) -> AnyPublisher<String, Error> {
    return Future<String, Error> { promise in
        promise(.success(value + " 2"))
    }
    .eraseToAnyPublisher()
}

func third(_ value: String) -> AnyPublisher<String, Error> {
    return Future<String, Error> { promise in
        promise(.success(value + " 3"))
    }
    .eraseToAnyPublisher()
}

var cancellable: [AnyCancellable] = []
first()
    .flatMap( { firstResult in second(firstResult) })
    .flatMap( { secondResult in third(secondResult) })
    // NOTE: イベントの購読
    // receiveCompletion -> イベントの結果(成功 or Error)を受け取る
    .sink(receiveCompletion: { completion in
        switch completion {
        case .finished:
            print("success")
        case let .failure(error):
            print("failure:", error)
        }
        // receiveValue -> イベントが返す値を受け取る
    }, receiveValue: { result in
        print(result)
    })
    .store(in: &cancellable)

この処理を実行すると、次のような出力になります。

1 2 3
success

first()が発行した値("1")をsecond()が受け取り("2")と結合、それをさらthird()に渡し("3")を結合するといったように連続的にイベント処理が行われます。

まず、 Combineには3つの役割が存在します。

  • Publisher: イベントの発行者
  • Subscriber: イベントを購読する
  • Operator: イベントの値を加工する

これらは、Rx系ライブラリでも名前は異なりますが、同様の機能が存在するので覚えておいた方が良さそうです。

イベントの発行

今回は、Publisher の作成には Future を使っています。

Future は、1回の結果(1つの値 or エラー)を返すための Publisher です。

Combineの色々なサンプルを見るとPublisherやSubscriberを1から実装することは少なく、Comibneが用意していくれるユーザビリティな機能で生成することが多そうです。
Futureの他にも Just, Deferredなど用途に合わせたPublisherを作成するクラスが純正で用意されいます。

func first() -> AnyPublisher<String, Error> {
    // NOTE: 成功時にString型の値を返す、失敗時は、Errorを返す Futuerの作成
    return Future<String, Error> { promise in
        promise(.success("1"))
    }
    // NOTE: AnyPublisher型への変換
    .eraseToAnyPublisher()
}

まず、Futuerのインスタンスを作成します。

Futureには、ジェネリクスで『成功した時の型と失敗した時の型』を指定し、実行したい処理をクロージャに渡して初期化します。
クロージャには、結果を返すコールバック(promise)が渡されるため、 .success または .failure で指定した型の値を返します。

eraseToAnyPublisher は、Publisher プロトコルに準拠している型を AnyPublisher型に変換してくれるメソッドで、生成されるPublisherをこのようにAnyPublisher型に変換しておくことで、他のPublisherと組み合わせて使うときに便利に使えるようです。

イベントの購読

first()
    .flatMap( { firstResult in second(firstResult) })
    .flatMap( { secondResult in third(secondResult) })
    // NOTE: イベントの購読
    // receiveCompletion -> イベントの結果(成功 or Error)を受け取る
    .sink(receiveCompletion: { completion in
        switch completion {
        case .finished:
            print("success")
        case let .failure(error):
            print("failure:", error)
        }
    // receiveValue -> イベントが返す値を受け取る
    }, receiveValue: { result in
        print(result)
    })

発行側はできたので、購読側を作ります。
Subscriberも1から作るのではなく、sink() というメソッドを呼び出すことでイベントを購読することができます。
(flatMap については後述します。)

sinkメソッドは、2つのクロージャで構成されています。
receiveCompletion はイベントの結果(成功or失敗)を受け取るためのクロージャで、receiveValue は成功時のイベントで返ってきた値を受け取るためのクロージャです。
失敗した場合は、receiveValueは呼び出されないようです。

イベントの加工

first()
    .flatMap( { firstResult in second(firstResult) })
    .flatMap( { secondResult in third(secondResult) })

最後にOperatorについてです。
Operatorを使うとイベントで流れる結果の値を様々な形で加工することができます。
Operatorも多くの種類があるため、用途によって使い分けることが必要です。

今回は、flatMap というOperatorを使用しています。

flatMap を使うと前のイベントから受け取った値を加工して新たな Publisher として返すことができます。
これを使うと、「複数のイベントを組み合わせて一つのイベントとして扱う」といったことができます。

今回は、firts() と同じように、second(), third() というPublisherを返すメソッドをを用意し、前のイベントの文字列を結合して結果を返す連続的なイベント処理を実装しました。

イベントの購読をキャンセル

var cancellable: [AnyCancellable] = []
first()
  ・・・
    .store(in: &cancellable)

イベントの購読を中止するには、sink() メソッドが返す AnyCancellable を受け取り、cancel()メソッドを呼び出します。
今回は、store() メソッドを使い、AnyCancellbleのインスタンスをコレクションに保持しています。

こうしておくと、インスタンス初期化時にまとめて購読して、インスタンス破棄時にまとめてキャンセルするような使い方ができます。

■ キャンセルする場合

for c in cancellable {
    c.cancel()
}

注意点

幾つか実装していて気をつける点があったので記載します。

cancellableは、 イベント購読終了まで生存していなくてはならない

sink()が返すAnyCancellableは、イベントの購読を完了するまでオブジェクトとして生存しておく必要があります。

class SomeClass {
    func subscribe() {
        var cancellable: AnyCancellable
        cancellable = createPublisher()
            .sink(receiveCompletion: { completion in
                ・・・
            }, receiveValue: { result in
                ・・・
            })
    }
}

例えばあるクラスのインスタンス中で、イベントの購読処理をした場合、上記の書き方ではメソッド終了時に購読もキャンセルされイベントを受け取ることができません。

class SomeClass {
    var cancellable: AnyCancellable
    func subscribe() {
        cancellable = createPublisher()
            .sink(receiveCompletion: { completion in
                ・・・
            }, receiveValue: { result in
                ・・・
            })
    }
}

このように、オブジェクトの生存スコープに保持しておく必要があります。

Futureの初期化時のクロージャは、 初期化のタイミングで実行される

Futureの初期化時に渡すイベントのクロージャは、購読前に実行されます。

let publisher = Future<String, Error> { promise in
    print("call future")
    promise(.success("success"))
}
DispatchQueue.main.asyncAfter(deadline: .now() + 10) {
    publisher.sink(receiveCompletion: {c in}, receiveValue: {v in})
}

例えば、このように購読までに時間が空いたとしても、Future生成時に中のクロージャは実行されています。

これは意図した仕様のようで、もし購読開始までイベント処理の実行を遅らせたい場合は次のように書くようです。

Deferred {
    Future<String, Error> { promise in
        print("call future")
        promise(.success("success"))
    }
}

Deferred を使うと引数で受け取ったPublisherの実行を購読まで待機してくれます。

まとめ

基本的な実装について試してみましたが、Combineには他にも多くの機能があるため、使いこなすためにはまだ色々と調べる必要があるなと感じました。

おそらくComibineの使い方や考えかたは、Rx系のライブラリと似ていると思うので、 今までRxを使用したライブラリを使っている人はそれほど難しくはないのかもしれません。

それでもiOSの純正で使用できるということは大きなメリットではあると思うので、この機会にReactive Programingに入門するのも良いのではないでしょうか。

参考リンク