参考のように僕の扱うプロジェクトでもコールバックの階層が深い部分が悩みの一つでした. これが一概に悪いとも言えないのですが, 非同期処理が追加されるにつれ階層が深くなるのはやはり良くありません.
そこで今回, 非同期処理をスッキリ書けるPromiseKitを導入しようかと思い, 導入前に挙動確認をしておきます.
参考
コールバック地獄からの脱却!複雑なiOSアニメーションをPromiseの決定版ライブラリPromiseKitですっきり実装する!!!
PromiseKitの導入方法は公式サイトを参照してください. ここでは検証コードだけを記述していきます.
ソースコード
#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