PromiseKitの使い方 その1
Nov 17, 2017 · iosswift3
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
の基本的な動作としては、
- 1つの非同期処理は必ず成功か失敗のどちらかの結果を返す
- 成功時は次の処理へ進む。その際、任意のデータを渡せる
- 失敗時はエラー処理へ飛んで終了する(次へ進まない)
となる
非同期処理を実装
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?) -> Void
)で返す - クロージャには2つの引数(成功時のデータと失敗時のエラー)が必要
- 成功時は
Data
に結果を入れて返す(エラーはnil
に)
→ 成功時のデータ型は他の型でもOK - 失敗時は
Error
にエラーを入れて返す(データ部分はnil
に)
→ 失敗時のデータはError
を継承した型のみ
である。なお、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 {
...
という感じで結果を受け取れる
エラーが発生した場合は、以下の様にcatch
でError
を受け取ることができる
}.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>
の部分)を省略せずに指定するとビルドが通る様になる
開発環境
- Xcode 8.3.3
- iOS 10.3
- PromiseKit 4.4