こんにちは。
主にiOS関連のアプリ開発を担当している、よのでらです。
以前に書かせて頂いた「Swift Hydra/Promiseで非同期処理を同期的に処理する」が、沢山の方にお読みいただけたようで嬉しい限りです。
この場をお借りして、御礼を申し上げます。
今回も引き続き、Hydra/Promiseの記事を書きたいと思います。
今回の概要
参考:Hydra
前回は、thenとcatchでのサンプルを書きましたが他にも便利な機能がありますので、今回はAll Featuresを網羅してみようと思います。
まずは前回の復習から
- Hydra/Promiseは同期や非同期の処理をわかりやすく制御するもの
- resolve(解決)、reject(拒否)、pending(保留中)の状態がある
- resolveは次の処理(then)を実行する
- rejectは次の処理を実行しない(catchへ)
- pendingは未処理
All Featuresを網羅してみる
私は、可能な限り動かして確認したい性分のため、実際に作成したサンプルコードと実行結果を記載します。
なお、あくまでも自分なりの解釈で記載します。
※クリックすると項目の説明にジャンプします
always
Promiseの結果が解決でも、拒否でも、常に処理を行います。
■サンプルコード
enum SampleError: Error { case sample } func sample() -> Promise<Void> { return Promise<Void>(in: .background, { resolve, reject, _ in // 乱数の結果が0の場合は成功、1の場合は失敗を想定 if Int.random(in: 0..<2) == 0 { resolve(()) } else { reject(SampleError.sample) } }) } func testAlways() { sample().then { _ in print("then") }.catch { error in print("catch: \(error)") }.always { // 結果が解決でも拒否でも実行される print("always") } }
■実行結果
解決
then always
拒否
catch: sample always
validate
Promiseが許可の際の返却値の検証を行い、その結果で以降を続けるかどうかを判定します。
■サンプルコード
func sample() -> Promise<Int> { return Promise<Int>(in: .background, { resolve, _, _ in // 0か1を乱数で返す resolve(Int.random(in: 0..<2)) }) } func testValidate() { sample().validate { result in print("validate: \(result)") guard result == 1 else { return false // 拒否 } return true // 解決 }.then { _ in print("then") }.catch { error in print("catch: \(error)") } }
■実行結果
validateの結果がfalse
validate: 1 catch: reject
validateの結果がtrue
validate: 0 then
timeout
設定した時間内にPromiseが解決するかしないかで、それ以降を続けるかどうかを判定します。
■サンプルコード
enum SampleError: Error { case timeout } func sample() -> Promise<Void> { return Promise<Void>(in: .background, { resolve, reject, _ in resolve(()) }).defer(6) // 解決に6秒かかったと仮定 } func testTimeout() { // タイムアウトを5秒に設定 sample().timeout(in: .background, timeout: 5, error: SampleError.timeout).then { _ in print("then") }.catch { error in print("catch: \(error)") // timeout } }
■実行結果
タイムアウトで拒否
catch: timeout
all
すべてのPromiseを並列で実行し、その結果により以降を続けるかどうかを判定します。
1つでも失敗した場合は拒否します。
■サンプルコード
enum SampleError: Error { case sample1, sample2 } func sample1() -> Promise<Int> { return Promise<Int>(in: .background, { resolve, reject, _ in let value = Int.random(in: 0..<2) if value > 0 { print("sample1") resolve(value) } else { reject(SampleError.sample1) } }) } func sample2() -> Promise<Int> { return Promise<Int>(in: .background, { resolve, reject, _ in let value = Int.random(in: 0..<2) if value > 0 { print("sample2") resolve(value) } else { reject(SampleError.sample2) } }) } func testAll() { all([sample1(), sample2()]).then { results in print("then") dump(results) // 全ての結果が配列で返却される }.catch { error in print("catch: \(error)") } }
■実行結果
全てが解決
sample1 sample2 then 2 elements - 1 - 1
sample1が拒否
catch: sample1
sample2が拒否
catch: sample2
any
すべてのPromiseを並列で実行し、一番早い結果により以降を続けるかどうかを判定します。
■サンプルコード
enum SampleError: Error { case sample1, sample2 } func sample1() -> Promise<Int> { return Promise<Int>(in: .background, { resolve, reject, _ in let value = Int.random(in: 0..<2) if value == 0 { print("sample1") resolve(value) } else { reject(SampleError.sample1) } }) } func sample2() -> Promise<Int> { return Promise<Int>(in: .background, { resolve, reject, _ in let value = Int.random(in: 0..<2) if value == 1 { print("sample2") resolve(value) // 一番早い解決 } else { reject(SampleError.sample2) } }) } func testAny() { any(sample1(), sample2()).then { result in print("then: \(result)") }.catch { error in print("catch: \(error)") } }
■実行結果
全てが解決
sample1 sample2 then: 0
sample1が拒否
catch: sample1
sample2が拒否
catch: sample2
pass
Promiseの型を変更せずに結果を確認することができ、そのまま次に渡すことも、それを拒否することもできます。
■サンプルコード
enum SampleError: Error { case pass } func sample() -> Promise<Int> { return Promise<Int>(in: .background, { resolve, _, _ in resolve(Int.random(in: 0..<100)) }) } func testPass() { sample().pass { result -> Promise<Int> in // データの確認 return Promise<Int>(in: .background, { resolve, reject, _ in if result % 2 == 0 { // 余りがなければ解決 resolve(result) } else { // 余りがあれば拒否 reject(SampleError.pass) } }) }.then { result in print("then: \(result)") }.catch { error in print("catch: \(error)") } }
■実行結果
解決(余りなし)
then: 0〜99を2で割って余りが0の数値
拒否(余りあり)
catch: pass
recover
一つ前の結果が拒否だった場合でも、解決に回復して処理を継続します。
■サンプルコード
enum SampleError: Error { case recover } func sample() -> Promise<Void> { return Promise<Void>(in: .background, { resolve, reject, _ in reject(SampleError.recover) // 拒否 }) } func testRecover() { sample().recover { result in print("recover: \(result)") // 拒否を解決にする return Promise(resolved: ()) }.then { print("then") // 解決になったので実行される }.catch { error in print("catch: \(error)") // 解決になったので実行されない } }
■実行結果
拒否〜解決
recover: recover then
map
配列に設定した値の引数を元にPromiseに変換し、それらを並列に実行するために使用します。
anyやallと組み合わせて使います。
■サンプルコード
enum SampleError: Error { case sample } func sample(value: Int) -> Promise<Int> { return Promise<Int>(in: .background, { resolve, reject, _ in let number = Int.random(in: 0..<10) print("value: \(value), number: \(number)") if number > 0 { resolve((value * number)) } else { reject(SampleError.sample) } }) } func testMap() { all([2, 4, 6].map(sample)).then { results in print("then: \(results)") }.catch { error in print("catch: \(error)") } }
■実行結果
全て解決の例
value: 2, number: 7 value: 4, number: 6 value: 6, number: 3 then: [14, 24, 18]
いずれかの値が拒否の例(以下は初めで拒否)
value: 2, number: 0 value: 4, number: 3 value: 6, number: 2 catch: sample
zip
異なるプロミスを並列で実行し、それぞれの結果をタプル型にまとめて返します。
■サンプルコード
enum SampleError: Error { case sample1, sample2 } func sample1() -> Promise<Bool> { return Promise<Bool>(in: .background, { resolve, reject, _ in let number = Int.random(in: 0..<5) print("sample1.number: \(number)") if number > 0 { resolve(true) } else { reject(SampleError.sample1) } }) } func sample2() -> Promise<String> { return Promise<String>(in: .background, { resolve, reject, _ in let number = Int.random(in: 0..<1) print("sample2.number: \(number)") if number > 0 { resolve(String(format: "%d", number)) } else { reject(SampleError.sample2) } }) } func testZip() { zip(sample1(), sample2()).then { (result1, result2) in print("then: \(result1), \(result2)") }.catch { error in print("catch: \(error)") } }
■実行結果
解決
sample1.number: 4 sample2.number: 4 then: true, 4
sample1またはsample2で拒否
sample1.number: 0 sample2.number: 0 catch: sample1
defer
Promiseを遅延実行します。サンプルは5秒後です。
■サンプルコード
enum SampleError: Error { case sample } func sample() -> Promise<Void> { print("sample") return Promise<Void>(in: .background, { resolve, reject, _ in if Int.random(in: 0..<2) > 0 { resolve(()) } else { reject(SampleError.sample) } }) } func testDefer() { print(Date()) // sampleを5秒遅延実行する sample().defer(5).then { print("then") print(Date()) }.catch { error in print("catch: \(error)") print(Date()) } }
■実行結果
解決
2019-12-17 06:35:40 +0000 sample then 2019-12-17 06:35:45 +0000
拒否
2019-12-17 06:36:12 +0000 sample catch: sample 2019-12-17 06:36:17 +0000
retry
設定した条件に応じて拒否されたPromiseをリトライします。
条件に達した場合は拒否します。
■サンプルコード
enum SampleError: Error { case sample } func sample() -> Promise<Void> { return Promise<Void> { resolve, reject, _ in if Int.random(in: 0..<2) > 0 { reject(SampleError.sample) } else { resolve(()) } } } func testRetry() { sample().retry(3) { (count, error) -> Bool in // 3回までリトライ(デクリメントされる) print("retry: \(count), error: \(error)") if count > 0 { return true } else { return false } }.then { print("then") }.catch { error in print("catch: \(error)") } }
■実行結果
1回目で解決
then
2回目で解決
retry: 2, error: sample // 2回目 then
3回目で解決
retry: 2, error: sample // 2回目 retry: 1, error: sample // 3回目 then
全てのリトライで拒否
retry: 2, error: sample retry: 1, error: sample catch: sample
cancel
Promise実行を外部からキャンセルすることができます。
以下のコードでは、sample()の遅延実行中にボタンをタップするとトークンが無効になり、sample()以降の処理をキャンセルします。
キャンセルの場合は解決でも拒否でもないので、thenもcatchも表示されません。
■サンプルコード
enum SampleError: Error { case sample } var token = InvalidationToken() func sample() -> Promise<Void> { print("sample") return Promise<Void>(in: .background, { (resolve, reject, operation) in DispatchQueue.global().asyncAfter(deadline: .now() + 5, execute: { if self.token.isCancelled { print("sample: cancel") operation.cancel() } else { print("sample: not cancel") if Int.random(in: 0..<5) > 0 { resolve(()) } else { reject(SampleError.sample) } } }) }) } func testCancel() { sample().cancelled { print("cancelled") }.then { print("then") }.catch { error in print("catch: \(error)") } } @IBAction func didTouchUpInside(_ sender: UIButton) { print("cancelButton") self.token.invalidate() }
■実行結果
キャンセルの場合
sample cancelButton sample: cancel cancelled
キャンセルなしで解決の場合
sample sample: not cancel then
キャンセルなしで拒否の場合
sample sample: not cancel catch: sample
最後に
こうして試してみるとallやanyなど並列処理の機能が充実しているので、複数の通信結果を待ってから以降を継続する場合などは、わかりやすく書けるなと感じました。
使い込むと、まだまだ色々な書き方ができそうですが、今回はここまでとします。
最後までお読みいただき、ありがとうございました。