こんにちは。
主に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など並列処理の機能が充実しているので、複数の通信結果を待ってから以降を継続する場合などは、わかりやすく書けるなと感じました。
使い込むと、まだまだ色々な書き方ができそうですが、今回はここまでとします。
最後までお読みいただき、ありがとうございました。

よのでら
CSVIT事業部 よのでら
最近スープカレーにはまっています…。