Edited at

iOS/Android対応 Boltsを使って非同期処理を統一的に書く


Bolts Framework とは

昨年Facebookに買収されて話題になったParseチームが開発しているiOS/Androidフレームワーク。

Bolts自体はParseとは独立しているため、ParseのBaaSを使っていない人にも役立ちます。

Parseはとても品質の良いサービスですので、Parseチームが作っているということでBoltsを安心して使えると思います。

https://github.com/BoltsFramework/Bolts-iOS

Boltsはローレベルライブラリのコレクションだと書かれていますが、今のところは非同期処理の統一インターフェースとなるタスクのみ用意されています。今後いろいろ増えていくのかもしれません。

タスクを使うと何ができるかというと、jQuery.deferredみたいなことです。

ネストしまくりなコールバック地獄をわかりやすく書けたり、エラー処理が統一的に書けたり、直列or並列の連続処理の仕組みを提供してくれたりします。


導入方法

CocoaPodsでサクッと導入。

pod "Bolts"


Bolts with AFNetworking

とりあえずBolts使うとどんな感じに書けるの?というのがまず最初に知りたい情報だと思います。

Githubの公式ページでは最初のサンプルがParseフレームワークと組み合わせた例になっています。

使ったことがない人にはいまいちわかりずらいので、代わりにAFNetworkingと組み合わせた例を書いてみました。


BFTask

Boltsではタスクという単位で処理を扱います。タスクはPromiseという用語でよく使われているオブジェクトで、BFTaskというクラスになっています。

コールバックベースで書かれたコードをBoltsを使って書き直す場合、引数にコールバック処理のブロックを取るメソッドを、BFTaskを返すように変更します。


コールバック版

// 非同期処理の呼び出し側

[self doSomething:^(id result) {
...
}];

// 非同期処理を実行するメソッド
-(void) doSomethingAsync:(void(^)(id))callback {
[[APIClient sharedClient] getSomething:^(id result) {
if (callback) {
callback(result);
}
}];
}



Bolts版

// 非同期処理の呼び出し側

[[self doSomething] continueWithSuccessBlock:^id(BFTask* task) {
...
}]

// 非同期処理を実行するメソッド
-(BFTask*) doSomethingAsync {
return [[APIClient sharedClient] getSomething];
}



BFTaskCompletionSource

上記例ではAPIClient.doSomethingの定義もBoltsを使って書き換える必要があります。

返す時点ではBFTaskの状態が確定していない場合は、BFTaskCompletionSourceクラスを使います。


コールバック版

@implementation APIClient

-(void) getSomething:(void(^)(id))callback {
[self getPath:@"getSomething" parameters:@{} success:^(AFHTTPRequestOperation *operation, id res) {
if (callback) {
callback(res);
}
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
if (callback) {
callback(error);
}
}];
}



Bolts版

-(BFTask*) getSomething {

BFTaskCompletionSource *tcs = [BFTaskCompletionSource taskCompletionSource];

[self getPath:@"getSomething" parameters:@{} success:^(AFHTTPRequestOperation *operation, id res) {
tcs.result = res;
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
tcs.error = error;
}];
return tcs.task;
}



ダミーレスポンス

BFTaskを生成する簡単な方法としてtaskWithResult/taskWithErrorが用意されています。

以下の例は、APIのサーバサイドが未実装な場合に、クライアントサイドでダミーレスポンスを生成しています。


Bolts版ダミーAPI

-(BFTask*) getSomething {

return [BFTask taskWithResult:@{@"name": @"dummy"}];
}


コールバックの入れ子をなくす

API呼び出しの入れ子の例として、メッセージを送信して、成功したらメッセージ一覧を更新する、という流れです。

2段くらいだと実はコールバック版の方がわかりやすい気もします。


コールバック版

[[APIClient sharedClient] postMessage:msg callback:^(id res, NSError *error) {

if (error == nil) {
[[APIClient sharedClient] getMessages:nil callback:^(id res, NSError *error) {
if (error == nil) {
// refresh messages
...
}
}];
} else {
// show error
...
}
}];


Bolts版

[[[[APIClient sharedClient] postMessage:msg] continueWithBlock:^id(BFTask *task) {

if (task.error == nil) {
return [[APIClient sharedClient] getMessages:nil];
} else {
// show error
...
return nil;
}
}] continueWithSuccessBlock:^id(BFTask *task) {
// refresh messages
...
}];


複数タスクを直列実行

単一オブジェクトの削除APIをオブジェクト数分繰り返したい場合を例に書きました。

continueWithBlockで処理を繋げていくことで直列実行が実現できます。

BFTask *task = [BFTask taskWithResult:nil];

for (Message *msg in messages) {
task = [task continueWithBlock:^id(BFTask *task) {
return [[APIClient sharedClient] deleteMessage:msg.id];
}];
}
[task continueWithBlock:^id(BFTask *task) {
// refresh messages
...
return nil;
}];


複数タスクを並列実行

上記の例を並列実行する場合はtaskForCompletionOfAllTasksを使います。

並列実行の場合は、実行順は保証されません。

NSMutableArray *tasks = [[NSMutableArray alloc] init];

for (Message *msg in messages) {
BFTask *task = [[APIClient sharedClient] deleteMessage:msg.id];
[tasks addObject:task];
}
[[BFTask taskForCompletionOfAllTasks:tasks] continueWithBlock:^id(BFTask *task) {
// refresh messages
...
return nil;
}];


実行スレッドの制御

UIコンポーネントを操作する場合はメインスレッド上で実行する必要がある場合があります。

このような場合はBFExecutorを指定してタスクを実行します。


メインスレッド上で実行

[task continueWithExecutor:[BFExecutor mainThreadExecutor] withBlock:^id(BFTask *task) {

// UIコンポーネント操作
...
}];


使ってみた感想

非同期処理のブロックのインターフェースが統一的に書けるという点が一番のメリットだと感じました。

今まではコールバックを成功時と失敗時の両方用意するのか、成功時だけで失敗時は無視するのか、成功時も失敗時も同一のブロックで処理させるのか、という点をその都度考える必要がありました。

その点Boltsを使えばとりあえずBFTaskを返しておけば、あとは呼び出し側でだけ選べばいいので楽です。


おまけ

jQuery.deferredと比較してみました。

Bolts
jQuery.Deferred

BFTask.continueWithBlock:
deferred.always()

BFTask.continueWithSuccessBlock:
deferred.done()

BFTask.continueWithFailureBlock:
deferred.fail()

BFTask.result
done(args)

BFTask.error
fail(args)

BFTask.setResult:
deferred.resolveWith()

BFTask.setError:
deferred.rejectWith()

BFTask.taskForCompletionOfAllTasks:
jQuery.when()

BFTaskCompletionSource::taskCompletionSource
jQuery.Deferred()

BFTaskCompletionSource.task
deferred.promise()

下記の記事を参考にさせていただきました。

結局jQuery.Deferredの何が嬉しいのか分からない、という人向けの小話