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 からでも利用できるようになるんじゃないでしょうか。
今回作成したコードは、アーカイブとして置いてあります。