SimpleBoxes

ブロックを使ったアニメーション処理 (iPhone/iPad, Objective-C 2.0)

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 からでも利用できるようになるんじゃないでしょうか。

今回作成したコードは、アーカイブとして置いてあります。

コード SBAnimtor をダウンロード

スポンサーリンク

<< Safari on iPhone 対応に関するメモ :: Re: 初めてiPhoneアプリを作る人が使いたがる機能 >>