Core Animationを使って、あるアニメーションが終わったら次、それが終わったらまた次……といったように逐次的にアニメーションをしたくなった。
Core Animationは非常に優秀なアニメーションフレームワークで、非同期アニメーションを大した労力もなく実行することができる。一方、アニメーションと同期して処理を実行するのは結構面倒くさい。CAAnimationの委譲メソッドを使うか、CATransactionの+setCompletionBlock:メソッドを使うしかない(はず)。逐次的にアニメーションをしていく上で、コールバックを用いて書くとまとまりが悪くなってあまりよろしくない。
という訳で、あるアニメーションが終わるまでスレッドをブロックすることにする。実装の方法は他にもあるかもしれないが、今回はCFRunLoopを使ってみることにした。
大まかな戦略としてはこうである。
- アニメーションを開始
- CFRunLoopを起動して待機
- アニメーションの終了通知を受け取り、CFRunLoopを解除
これを1セットの処理として、アニメーションごとに行うことで、逐次的にアニメーションを実行する。
- (void)doAnimationsSequentially {
CFRunLoopRef rl = CFRunLoopGetCurrent();
CFDateRef distantFuture = (__bridge CFDateRef)[NSDate distantFuture]; // ARC環境下
CFRunLoopTimerRef timer = CFRunLoopTimerCreate(NULL, CFDateGetAbsoluteTime(distantFuture), 0, 0, 0, NULL, NULL);
CFRunLoopAddTimer(rl, timer, kCFRunLoopDefaultMode);
// アニメーション1
[CATransaction begin];
[CATransaction setCompletionBlock:^(void){
CFRunLoopStop(rl); // (3) RunLoop解除
}];
// (1) アニメーション開始
[CATransaction commit];
CFRunLoopRun(); // (2) 終了まで待機
// アニメーション2:アニメーション1が終わってから開始する
[CATransaction begin];
[CATransaction setCompletionBlock:^(void){
CFRunLoopStop(rl);
}];
// 何か
[CATransaction commit];
CFRunLoopRun();
... // 以下同様……
// 後片付け
CFRunLoopTimerInvalidate(timer);
CFRelease(timer);
}
割とさっぱりしていると思う。
イベントループ中で実行するとアプリケーションがアニメーションの終了まで止まってしまう。
CATransactionが明示的にネストされていると、一番外側のCATransactionがcommitするまでアニメーションが開始されないため、フリーズしてしまうので注意。
ちょっと解説。
CFRunLoopは、入力ソースなどのイベントを監視するためのクラス。CFRunLoopRun()を実行すると監視ループに入り、現在のスレッドでの処理が止まる。CFRunLoopに登録されている入力ソースが全て処理を終えるか、CFRunLoopStop()を実行することで監視ループを抜けることが出来る(一回ずつ入力ソースを処理したり、タイムアウトを設定することも出来る)。ここで注意しなければいけないのは、入力ソースが登録されていない場合、CFRunLoopRun()はループに入ることなく終了するということ。
CFRunLoopはスレッドごとに一つ存在し、CFRunLoopGetCurrent()で現在のスレッドのCFRunLoopを取得できる。スレッド作成時点では入力ソースは一つも登録されていないので、サブスレッドで実行することを考えて、空の(コールバックを持たない)タイマーを作って登録している。このようにCFRunLoopTimerCreate()を使っていいかどうかはリファレンスに書いていなかった。一応動作はしたが、気になるならコールバックに適当な関数を突っ込んでおいてもいいかも。
CFRunLoopTimerCreate()の第二引数(fireDate)にdistantFutureを設定して発火しないタイマーを作る。これによってCFRunLoopStop()が呼ばれるまで監視ループを勝手に抜けない様にする。