Bolts Framework とは
昨年Facebookに買収されて話題になったParseチームが開発しているiOS/Androidフレームワーク。
Bolts自体はParseとは独立しているため、ParseのBaaSを使っていない人にも役立ちます。
Parseはとても品質の良いサービスですので、Parseチームが作っているということでBoltsを安心して使えると思います。
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);
}
}];
}
// 非同期処理の呼び出し側
[[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);
}
}];
}
-(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のサーバサイドが未実装な場合に、クライアントサイドでダミーレスポンスを生成しています。
-(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
...
}
}];
[[[[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の何が嬉しいのか分からない、という人向けの小話