ブロックを使ったアニメーション処理 (iPhone/iPad, Objective-C 2.0)
- 2010.09.02 Thursday
- dev
iOS4 から UIView クラスにある
+ (void)animateWithDuration:animations:
というクラスメソッドを使ってアニメーション処理を行うことができます。
従来の
+ (void)beginAnimations:context: + (void)commitAnimations
などの一連のアニメーション関連のクラスメソッドも引き続きサポートされていますが、discouraged (非推奨) になっています。
animateWithDuration:animations:
によるアニメーション処理の特徴的なところは、ブロックと呼ばれるクロージャ機構を利用しているところです。
従来のように終了処理セレクタを用意することなく、アニメーション処理を記述できるので、一般的にコードはより簡潔になります。
例えば、以下のようなアニメーション処理をするコードがあるとします。
- (void)doAnimation { self.alpha = 0.1f; self.transform = CGAffineTransformMakeScale(0.1f, 0.1f); [UIView beginAnimations:nil context:nil]; [UIView setAnimationDelegate:self]; [UIView setAnimationDidStopSelector:@selector(animationFinished:finished:context:)]; [UIView setAnimationDuration:0.3f]; self.alpha = 1.0f; self.transform = CGAffineTransformMakeScale(1.2f, 1.2f); [UIView commitAnimations]; } - (void)animationFinished:(NSString *)animationID finished:(BOOL)finished context:(void *)context { self.transform = CGAffineTransformIdentity; }
終了処理用のセレクタ animationFinished:finished:context:
が必要になっています。また、beginAnimations:context:
から始まって 5 つのクラスメソッドを呼んでいるのも分かると思います。
一方、新しいブロックを利用したアニメーション処理では、
- (void)doAnimation { self.alpha = 0.1f; self.transform = CGAffineTransformMakeScale(0.1f, 0.1f); [UIView animateWithDuration:0.3f animations:^{ self.alpha = 1.0f; self.transform = CGAffineTransformMakeScale(1.2f, 1.2f); } completion:^(BOOL finished){ self.transform = CGAffineTransformIdentity; }]; }
のようになります。
行数的にはそれほど大きな変化はありませんが、利用しているメソッドはひとつだけ。アニメーションとして処理される部分も animations:
という引数にまとまっているので、分かりやすくなっています。また、アニメーション終了処理用セレクタも必要ありません。
ブロックを利用したアニメーション処理は、簡潔ですし、なにより分かりやすくなっています。しかしながら、iPhoneOS 3.2 では利用できません。そのため、iPad 向けには今のところ旧来の方式を記述するしかないようです。
でも、一度ブロックを利用した記述に慣れてしまうと、もはや旧来の方式で記述するのは苦痛。同じ動作をプラットフォーム毎に別の記述するのも面倒です。
そこで同じ記述でどちらも動作するコードを考えてみます。方針としては、ブロックを利用したアニメーション処理を iPhoneOS 3.2 でも利用できるようにするのが目標です。
アニメーション処理を行うクラス SBAnimator
を作ります。
#import <UIKit/UIKit.h> @interface SBAnimator : NSObject + (void)animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations; + (void)animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion; + (void)animateWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay options:(NSUInteger)options animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion; @end
最初の二つのメソッド animateWithDuration:animations:
と animateWithDuration:animations:completion:
は引数を省略したショートカットバージョンです。
+ (void)animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations { [SBAnimator animateWithDuration:duration delay:0.f options:UIViewAnimationOptionTransitionNone | UIViewAnimationOptionCurveEaseInOut animations:animations completion:nil]; } + (void)animateWithDuration:(NSTimeInterval)duration animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion { [SBAnimator animateWithDuration:duration delay:0.f options:UIViewAnimationOptionTransitionNone | UIViewAnimationOptionCurveEaseInOut animations:animations completion:completion]; }
実質的には最後のものだけを実装します。
+ (void)animateWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay options:(NSUInteger)options animations:(void (^)(void))animations completion:(void (^)(BOOL finished))completion { [UIView beginAnimations:nil context:UIGraphicsGetCurrentContext()]; if (completion) { [[[SBAnimationHandler alloc] initWithCompletionAction:completion] autorelease]; } [UIView setAnimationCurve:(options >> 16) & 0x7]; [UIView setAnimationRepeatAutoreverses:options & UIViewAnimationOptionAutoreverse]; [UIView setAnimationDelay:delay]; [UIView setAnimationDuration:duration]; animations(); [UIView commitAnimations]; }
ご覧の通り、内部では旧来の方法を利用しています。今のところ、旧来の方式は iOS4 でも利用できるので、これでどちらの環境でも動作します。
基本的には引数として渡された completion
ブロックを遅延動作することさえできればいいわけで、それを実現するために SBAnimationHandler
というプライベートなクラスを使います。
@interface SBAnimationHandler : NSObject { @private void (^_completionAction)(BOOL finished); } - (id)initWithCompletionAction:(void (^)(BOOL finished))completion; @end @implementation SBAnimationHandler - (id)initWithCompletionAction:(void (^)(BOOL finished))completion { if (self = [super init]) { _completionAction = Block_copy(completion); [UIView setAnimationDelegate:[self retain]]; [UIView setAnimationDidStopSelector:@selector(animationFinished:finished:context:)]; } return self; } - (void)animationFinished:(NSString *)animationID finished:(BOOL)finished context:(void *)context { _completionAction(finished); [self release]; } - (void)dealloc { Block_release(_completionAction); [super dealloc]; } @end
SBAnimationHandler
側でアニメーション終了処理用のセレクタを定義しているので、呼び出し側には追加のセレクタは必要ありません。
終了処理セレクタでは、単純に保持していたブロックを実行しているだけ。若干特殊な部分があるとすれば、アニメーション終了処理を実行したら、SBAnimationHandler
のインスタンスは必要ないので、[self release];
をコールして、自己破棄しているところぐらいでしょうか。
ブロックはスコープを抜けると破棄されていまうので、_completionAction = Block_copy(completion);
で保持するようにしています。
リファレンスカウンタの絡みで [UIView setAnimationDelegate:[self retain]];
と若干ハック的なことをしていますが、あまり突っ込まないでください。
これによって iPhoneOS 3.2 だろうと iOS4 だろうと、どちらも
- (void)doAnimation { self.alpha = 0.1f; self.transform = CGAffineTransformMakeScale(0.1f, 0.1f); [SBAnimator animateWithDuration:0.3f animations:^{ self.alpha = 1.0f; self.transform = CGAffineTransformMakeScale(1.2f, 1.2f); } completion:^(BOOL finished){ self.transform = CGAffineTransformIdentity; }]; }
のように記述できます。
とりあえず、iPad シミュレータと iPhone シミュレータでのみ動作確認をしています。私の環境では確認していませんが、PLBlocks を利用すれば iPhoneOS 2.2 からでも利用できるようになるんじゃないでしょうか。
今回作成したコードは、アーカイブとして置いてあります。
スポンサーリンク