PromiseKitの使い方 その1

Swiftのクロージャはコールバック処理がObjectiveCと比べるとかなり書きやすくなっているが、それでも非同期処理が重なるとコールバック地獄に陥りやすい

それから脱出するのに役立つのがPromiseで、SwiftではPromiseKitというフレームワークで実装されている。その使い方のメモ

ちなみにPromise自体を使うにはPromiseKitでなくても良いし、個人的にはSwiftTaskが良さげだったが、選定時点ではSwift3への対応がまだされていなかったのでPromiseKitを選んだ

また、現在の最新はVer.5だが公式に記載のあるとおりドキュメントが未整備なのでVer.4を使用

メリット・デメリット

メリット

非同期での逐次処理が見やすくなる

例えば、非同期処理の完了後に次の非同期処理を行なっていく場合、クロージャだと入れ子がどんどん深くなっていってしまったり、エラーを一元的に管理するのが難しい。デリゲートだとステータス管理が必要になったり、処理の流れが分散して分かりにくくなってしまう

Promiseであれば、非同期の逐次処理を直列的に書くことができ、次にどの処理に行くのか?どういう結果を受け取るのか?エラー時の処理はどうなるのかといったことが判りやすくなる

状態遷移の管理が不要

特にデリゲートの場合だと、ステータス管理を自分で行わないといけないが、Promiseではフレームワーク側で管理してもらえる。ステータス管理はバグが潜みやすく、何か改修が入った時も影響を受けやすいが、フレームワーク側で管理されているとその心配をしなくて良い

リトライやタイムアウトといった処理では、若干自前で実装しないといけないが、それでも一から実装するよりははるかに楽

いずれにせよ、Promiseを使った場合は、処理の流れの制御をあまり気にすることなく、各処理の内容に集中できるのでとても良い

デメリット

覚えないといけない

これをデメリットにあげるのは微妙だが、Promiseという概念とクロージャの使い方を覚える必要がある

ただ、Promise自体はJSなどWeb系ではよく知られた考え方だし、クロージャもSwiftでは欠かせないので、さほど問題にはならないはず(古いObjC時代のコーディングに慣れていて知識がアップデートされていない人がチームにいたりすると・・・)

デリゲートと相性が悪い場合がある

利用するフレームワークにもよる部分もあるが、例えば、ダウンロードの進捗を毎回通知する様な場合は向いていない(書き方がややこしくなってしまう)

書き方がややこしい場合がある

ちょっと複雑な処理をしたい場合など、書き方が難しい場合がある(日本語情報も少なかったりする)。ただ、逆にいうと、そういった場合はPromise自体の概念にあっていないか、処理の切り分けに失敗している可能性があるので、一概にデメリットとは言えない

ただ、Swiftでは(クロージャ部分は特に)様々な省略記法が使える上、PromiseKitも色々な書き方ができる様になっており、同じ処理でも色々な書き方が存在して混乱することがあるので、これは書き方を統一するなどした方が良い。 実際、今回、メモに残したコードもあくまで一例で他の書き方が可能だったりする

使い方

まず、Promiseの基本的な動作としては、

となる

非同期処理を実装

PromiseKitでは、非同期処理部分をPromise<T>(※TはGenerics。任意の型のデータを入れれる)を返すメソッドとして定義する必要がある

一番シンプルなのは、非同期処理をまず以下の様なメソッドとして定義し、それをwrapする方法である。 例えば、単純にAPIを叩くだけの場合は以下の通り

func hoge(completion: (Data?, Error?) -> Void) {
    URLSession.shared.dataTask(with: url) { (data, response, error) in
        guard error != nil {
            // 失敗
            completion(nil, error)
            return
        }

        // 成功
        completion(data, nil)
    }
}

ポイントは、

である。なお、DataとError両方ともnilを返すとエラーになる

メソッドを定義できれば、以下の様にしてPromiseを返すメソッドに変換する

PromiseKit.wrap(hoge)

この場合、Promiseの型はPromise<Data>となる。 これは、元のメソッドの成功時に返すデータがDataであるのでそうなる

非同期処理を繋げる

Promiseを返す非同期処理のメソッドができれば、次はそれを繋げてフローを作っていく

例えば、最初にhoge1を実行し、成功すれば次のhoge2を実行するといった場合は、以下の実装となる

firstry {
    // ここに1つ目の非同期処理
    PromiseKit.wrap(hoge1)
}.then {
    // ここに2つ目の非同期処理
    PromiseKit.wrap(self.hoge2)
}.then {
    // ここで2つ目の非同期処理が成功した場合の処理
}.catch {
    // エラー時の処理
}

最初の処理は、firstryを使い、続くクロージャの中に初回の非同期処理を書く。 この時クロージャはPromiseを返す必要がある

2つ目以降の処理はthenを使う。thenは前の処理の結果が成功であればクロージャ内の処理を実行する。 このクロージャも最後の処理以外はPromiseを返す必要がある。 フローの最後に当たるthenの中では、前の非同期処理が成功した時の処理だけを書く(Promiseを返す処理を書いても、それの成功時の処理が無いのでエラーとなる)。 また、2つ目以降はselfがいる様になるので注意

非同期処理でエラーを返した場合は、catchの中の処理が実行され、残りの処理は実行されない。 例えば、上図でhoge1がエラーだった場合は、catchへ飛んでhoge2は実行されない

処理結果の受け取り

上のサンプルコードでは省略されているが、各非同期の処理結果はクロージャの引数として受け取れる。 この場合の型は、非同期処理を行なったメソッドが成功時に返すデータの型と同じである

例えば、hoge1がDataを返す場合は、

firstry {
    PromiseKit.wrap(hoge1)
}.then { result in
    // result: Data型の結果データ
    PromiseKit.wrap(self.hoge2)
}.then {
...

という感じで結果を受け取れる

エラーが発生した場合は、以下の様にcatchErrorを受け取ることができる

}.catch { error in
    print(error.localizedDescription)
}

書き方の省略について

Swiftのクロージャは省略して書ける書き方がかなり多い。 上のサンプルも色々と省略をして書いているが、省略せずに書くと、

firstry { () -> Promise<Data> in
    return PromiseKit.wrap(hoge1)
}.then { (result: Data) -> Promise<Data> in
    return PromiseKit.wrap(self.hoge2)
}.then { (result: Data) -> Void in
    // ここで2つ目の非同期処理が成功した場合の処理
}.catch { (error: Error) -> Void in
    // エラー時の処理
}

といった感じになる。 さすがに上記の様に全く省略しないのは冗長になるのでやめた方が良いが、ある程度、型情報は残しておいた方がわかりやすいし、補完も効きやすいのでオススメ

また、Promiseを返す部分でたまに正しくPromiseを返しているのにエラーとなる場合がある。 この場合は、クロージャの返り値の型(->の後ろのPromise<T>の部分)を省略せずに指定するとビルドが通る様になる

開発環境

関連記事一覧