LoginSignup
12
11

More than 5 years have passed since last update.

PromiseKitのthen,catch,finally,whenについて

Last updated at Posted at 2015-04-22

参考のように僕の扱うプロジェクトでもコールバックの階層が深い部分が悩みの一つでした. これが一概に悪いとも言えないのですが, 非同期処理が追加されるにつれ階層が深くなるのはやはり良くありません.
そこで今回, 非同期処理をスッキリ書けるPromiseKitを導入しようかと思い, 導入前に挙動確認をしておきます.

参考
コールバック地獄からの脱却!複雑なiOSアニメーションをPromiseの決定版ライブラリPromiseKitですっきり実装する!!!

PromiseKitの導入方法は公式サイトを参照してください. ここでは検証コードだけを記述していきます.

ソースコード

ViewController.m
#import "ViewController.h"
#import <PromiseKit/PromiseKit.h>

@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];

    // ここに検証コードを書く //

}

// 指定秒後にfulfillされるpromiseを返す
- (PMKPromise *) promiseFulfillWithDelay:(double)delay name:(NSString *)name {
    return [PMKPromise new:^(PMKFulfiller fulfill, PMKRejecter reject) {
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@ running...", name);
            fulfill(name);
        });
    }];
}

// 指定秒後にrejectされるpromiseを返す
- (PMKPromise *) promiseRejectWithDelay:(double)delay name:(NSString *)name {
    return [PMKPromise new:^(PMKFulfiller fulfill, PMKRejecter reject) {
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            NSLog(@"%@ running...", name);
            NSError *e = [NSError errorWithDomain:name code:-1 userInfo:nil];
            reject(e);
        });
    }];
}

- (NSTimeInterval) currentTimeSec {
    return [[NSDate date] timeIntervalSince1970];
}
@end

基本

then

    PMKPromise *pA = [self promiseFulfillWithDelay:2.0 name:@"A"];

    pA.then(^(NSString *name) {
        NSLog(@"then %@", name);
    }).catch(^(NSError *e) {
        NSLog(@"catch %@", e.domain);
    }).finally(^{
        NSLog(@"finally");
    });

結果

A running...
then A
finally

fulfillされるとthenが実行されます.

catch

    PMKPromise *pB = [self promiseRejectWithDelay:1.0 name:@"B"];

    pB.then(^(NSString *name) {
        NSLog(@"then %@", name);
    }).catch(^(NSError *e) {
        NSLog(@"catch %@", e.domain);
    }).finally(^{
        NSLog(@"finally");
    });

結果

B running...
catch B
finally

rejectされるとcatchが実行されます.

finally

promiseはfulfillかrejectかのどちらか一方の状態にしかなりません. ただし, fulfillでもrejectでもfinallyは実行されます.

組み合わせる

例1

    [self promiseFulfillWithDelay:2.0 name:@"A"].then(^(NSString *name) {
        NSLog(@"then %@", name);
        return [self promiseFulfillWithDelay:1.0 name:@"B"];
    }).then(^(NSString *name) {
        NSLog(@"then %@", name);
        return [self promiseRejectWithDelay:1.0 name:@"C"];
    }).then(^(NSString *name) {
        NSLog(@"then %@", name);
    }).catch(^(NSError *e) {
        NSLog(@"catch %@", e.domain);
    }).finally(^{
        NSLog(@"finally");
    });

結果

A running...
then A
B running...
then B
C running...
catch C
finally

thenブロックの中でpromiseオブジェクトをreturnすると後続するthen/catchにつなげることができます.

例2

    [self promiseFulfillWithDelay:2.0 name:@"A"].then(^(NSString *name) {
        NSLog(@"then %@", name);
        return [self promiseRejectWithDelay:1.0 name:@"B"];
    }).then(^(NSString *name) {
        NSLog(@"then %@", name);
        return [self promiseFulfillWithDelay:1.0 name:@"C"];
    }).then(^(NSString *name) {
        NSLog(@"then %@", name);
    }).catch(^(NSError *e) {
        NSLog(@"catch %@", e.domain);
    }).finally(^{
        NSLog(@"finally");
    });

結果

A running...
then A
B running...
catch B
finally

メソッドチェーンの途中でrejectされると後続のthenをスキップしてcatchが呼ばれます.
上記例の場合, Cは実行されずにcatchでBのNSErrorを受け取り, 最後にfinallyが呼ばれます.

例3

    [self promiseFulfillWithDelay:2.0 name:@"A"].then(^(NSString *name) {
        NSLog(@"then %@", name);
        return [self promiseRejectWithDelay:1.0 name:@"B"];
    }).then(^(NSString *name) {
        NSLog(@"then %@", name);
        return [self promiseFulfillWithDelay:1.0 name:@"C"];
    }).catch(^(NSError *e) {
        NSLog(@"catch %@", e.domain);
        return [self promiseFulfillWithDelay:0.5 name:@"D"];
    }).then(^(NSString *name) {
        NSLog(@"then %@", name);
    }).catch(^(NSError *e) {
        NSLog(@"catch %@", e.domain);
    }).finally(^{
        NSLog(@"finally");
    });

結果

A running...
then A
B running...
catch B
D running...
then D
finally

上記例ではBがrejectされcatchが呼ばれているますが, そのブロックの中でpromiseオブジェクトをreturnしています. そして, 後続するthen/catchへとつながっています.

つまりthenでもcatchでもブロック中でpromiseオブジェクトをreturnすることで処理を続けていくことができます.

例4

    [self promiseFulfillWithDelay:2.0 name:@"A"].then(^(NSString *name) {
        NSLog(@"then %@", name);
        return @{ @"hoge": @"foo" };
    }).then(^(id obj) {
        NSLog(@"then %@", obj);
        // no return
    }).then(^(id obj) {
        NSLog(@"then %@", obj);
    });

結果

A running...
then A
then { hoge = foo; }
then (null)

実はpromiseオブジェクト以外のオブジェクトでもreturnできます. その場合はthenが呼ばれ引数にそのオブジェクトを渡します.
更にreturnをしなくてもthenがつながっていれば, そのthenが呼ばれます. その場合, 引数の値はnilになります.

when

whenは複数の非同期処理を並行処理しすべてfulfillされた時, thenが呼ばれます.

    PMKPromise *pA = [self promiseFulfillWithDelay:3.0 name:@"A"];
    PMKPromise *pB = [self promiseFulfillWithDelay:1.0 name:@"B"];
    PMKPromise *pC = [self promiseFulfillWithDelay:2.0 name:@"C"];
    NSTimeInterval startT = [self currentTimeSec];

    [PMKPromise when:@[ pA, pB, pC ]].then(^(NSArray *results) {
        NSTimeInterval t = [self currentTimeSec] - startT;
        NSLog(@"then %f %@", t, results);
    }).catch(^{
        NSLog(@"catch");
    }).finally(^{
        NSLog(@"finally");
    });

結果

B running...
C running...
A running...
then 3.298405 ( A, B, C )
finally

thenは最後の非同期処理がfulfillされた時点で呼ばれるので上記例ではだいたい3秒後になります.
結果はNSArrayで渡されますが, 要素の順番は非同期処理が完了した順ではなく, whenに指定した配列の順番にならいます.
また, NSArrayではなくNSDictionaryで指定することもできます.

    PMKPromise *pA = [self promiseFulfillWithDelay:3.0 name:@"A"];
    PMKPromise *pB = [self promiseFulfillWithDelay:1.0 name:@"B"];
    PMKPromise *pC = [self promiseFulfillWithDelay:2.0 name:@"C"];
    NSTimeInterval startT = [self currentTimeSec];

    [PMKPromise when:@{ @"a": pA, @"b": pB, @"c": pC }].then(^(NSDictionary *results) {
        NSTimeInterval t = [self currentTimeSec] - startT;
        NSLog(@"then %f %@", t, results);
    }).catch(^{
        NSLog(@"catch");
    }).finally(^{
        NSLog(@"finally");
    });

結果

B running...
C running...
A running...
then 3.001315 { a = A; b = B; c = C; }
finally

whenはひとつでもrejectされた場合, 即catchが呼ばれます.

    PMKPromise *pA = [self promiseFulfillWithDelay:3.0 name:@"A"];
    PMKPromise *pB = [self promiseFulfillWithDelay:1.0 name:@"B"];
    PMKPromise *pC = [self promiseRejectWithDelay:2.0 name:@"C"];

    NSTimeInterval startT = [self currentTimeSec];
    [PMKPromise when:@[ pA, pB, pC ]].then(^(NSArray *results) {
        NSLog(@"then %@", results);
    }).catch(^{
        NSTimeInterval t = [self currentTimeSec] - startT;
        NSLog(@"catch %f", t);
    }).finally(^{
        NSLog(@"finally");
    });

結果

B running...
C running...
catch 2.199885
finally
A running...

上記例ではCがrejectされたので即catchが呼ばれています.
注意したいのはfinallyの後にAが走っていることです. rejectされた場合でも非同期処理自体をキャンセルはしてくれないようです.

なお, allというメソッドもありますが, allはwhenのエイリアスなようです. 挙動はwhenと同じでした.

finally -> then?

ちなみにfinallyの後にthenをつけることもできました. ただし, finallyブロックではreturnすることができません(ビルドエラーになります).
こんな使い方しませんが...

    [self promiseFulfillWithDelay:1.0 name:@"A"].then(^(NSString *name) {
        NSLog(@"then %@", name);
    }).finally(^{
        NSLog(@"finally 1");
    }).then(^(NSString *name) {
        NSLog(@"then %@", name);
    }).finally(^{
        NSLog(@"finally 2");
    });

結果

A running...
then A
finally 1
then (null)
finally 2
12
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
12
11