Core Animation 中級編

8/19担当、Qiita初投稿の[@inamiy](https://github.com/inamiy)です、こんにちは。

今回は、iOS/OSXアプリのUXの根幹である「Core Animation」について、
ハマりやすい点や、意外と知られていないtipsなどについて書きたいと思います。



## アニメーション完了時のコールバック

Delegateでif分岐しながら処理する方法もありますが、
`[CATransaction setCompletionBlock:]`を使う方が、より見通しの良いコードになります。

問題は、アニメーションが正常終了したかどうかの`finished`フラグがないという点ですが、
下記のようなコードを書くと、上手く判別できます。

```
[CATransaction begin];
[CATransaction setCompletionBlock:^{

    CAAnimation* animation = [layer animationForKey:@"myAnimation"];

    if (animation) {
        // -animationDidStop:finished: の finished=YES に相当

        [layer removeAnimationForKey:@"myAnimation"]   // 後始末
    }
    else {
        // -animationDidStop:finished: の finished=NO に相当
    }

}];

CABasicAnimation* animation = [CABasicAnimation animationWithKeyPath:@"position.x"];
animation.duration = 3;
animation.byValue = [NSNumber numberWithDouble:100.0];

// completion処理用に、アニメーションが終了しても登録を残しておく
animation.removedOnCompletion = NO; 
animation.fillMode = kCAFillModeForwards;

[layer addAnimation:animation forKey:@"myAnimation"];

[CATransaction commit];
```

もし、`CATransaction`で毎回wrapするのが気に入らない場合は、
animationのdelegateオブジェクトを保持してblocksを実現する方法もあります。
[CAAnimationBlocks](https://github.com/xissburg/CAAnimationBlocks)というライブラリを使えば、`CAAnimation`毎に、
`-setCompletion:`でblockを登録することができます。

* https://github.com/xissburg/CAAnimationBlocks



## アニメーションを一時中断する

[Technical Q&A QA1673: How to pause the animation of a layer tree](https://developer.apple.com/library/ios/#qa/qa2009/qa1673.html)が参考になります。
（下記例は CALayerのカテゴリで実装しています）

```
@implementation CALayer (TheWorld)

- (void)pauseAnimations 
{
    CFTimeInterval pausedTime = [self convertTime:CACurrentMediaTime() fromLayer:nil];
    self.speed = 0.0;    // 時よ止まれ
    self.timeOffset = pausedTime;
}

- (void)resumeAnimations
{
    CFTimeInterval pausedTime = [self timeOffset];
    self.speed = 1.0;    // そして時は動き出す
    self.timeOffset = 0.0;
    CFTimeInterval timeSincePause = [self convertTime:CACurrentMediaTime() fromLayer:nil] - pausedTime;
    self.beginTime = timeSincePause;
}
```

ここで、`CAAnimation`の`beginTime`は通常、
親の`CAAnimationGroup`の時間軸におけるオフセットとして各animationに対して設定しますが、
上記例のようにグループを使わない場合は、レイヤー自体が親になるので、
`CACurrentMediaTime()`を使ってレイヤー時間に変換して計算する必要があります。



## fillModeについて

普段、何気なく使っている（かもしれない）下記のおまじない

``` おまじない
// よく分からないけど、アニメーションが終了してもちゃんと残る
animation.removedOnCompletion = NO; 
animation.fillMode = kCAFillModeForwards;
```

ですが、これも`CAAnimationGroup`を使って考えると分かりやすいです。

例えば、

```
CABasicAnimation* animation = [CABasicAnimation animationForKeyPath:@"position.x"];
animation.beginTime = 3;
animation.duration = 5;
animation.byValue = [NSNumber numberWithDouble:100.0];

CAAnimationGroup* group = [CAAnimationGroup animation];
group.animations = [NSArray arrayWithObject:animation];
group.duration = 10;
[layer addAnimation:group forKey:@"myGroupAnimation"];
```

を実行した場合、t=3〜8秒の間はアニメーションが実行されて表示されますが、
0〜3秒と8〜10秒は特に指定がなく、あいまいな状態になっています。
この場合、`animation.fillMode`の設定によって、

* kCAFillModeRemoved ・・・表示しない（デフォルト）
* kCAFillModeForwards・・・8〜10秒が終状態として表示される
* kCAFillModeBackwards・・・0〜3秒が始状態として表示される
* kCAFillModeBoth・・・backwards+forwards
* kCAFillModeFrozen・・・（OSX 10.5でDeprecated）

となります。

なので、もしgroupを使わず、かつanimationの`beginTime`がデフォルトの0の
（前節のように、がんばって変換して代入でもしない）場合、
fillModeにforwards以外を設定する意味は特にないので、
基本的に上記おまじないを「使う」か「使わない」かの2択で考えて良いでしょう。
（使う場合は、あとでしっかり後始末しましょう）



## 暗黙的アニメーションが内部でやっていること

Appleのドキュメント[CALayer Class Reference](https://developer.apple.com/library/mac/#documentation/graphicsimaging/reference/CALayer_class/Introduction/Introduction.html)によると、
暗黙的アニメーション（implicit animation）を実行したとき、
内部では`CALayer`の`-actionForKey:`が呼ばれ、
下記の順番で`id <CAAction>`オブジェクト（実質`CAAnimation`）を探索します。


1. layer.delegateの`-actionForLayer:forKey:`
2. `layer.actions`
3. `layer.style`
4. `[CALayer defaultActionForKey:]`

(途中で`[NSNull null]`が来ると探索をストップして`nil`扱いになる)

もし、`id <CAAction>`オブジェクトが見つかった場合、`-runActionForKey:object:arguments:`が実行され、
これが

```objectivec:
// selfは(id <CAAction>)オブジェクト、objectはlayer
[object addAnimation:self forKey:actionKey];
```

相当のコードを実行します。

ここで、Core Animationの特殊な挙動として、
もし`CAPropertyAnimation` (`CAAnimation`のサブクラス)に
`keyPath`が設定されないまま`-addAnimation:forKey:`が実行された場合、
そこで使われた**animationKeyがそのままkeyPathに代入される**ので、例えば、

```
layer.opacity = 0.5
```

という暗黙的アニメーションを実行したら、
`layer.animationKeys`に`opacity`キーが登録されつつ、opacity値を変化させるアニメーションが実行されます。


## 暗黙的アニメーションをデフォルトでOFFにする

毎回、`[CATransaction setDisableActions:YES]`を書くのは面倒なので、
前節の手順のどこかで`[NSNull null]`を渡してあげると良いです。
その場で簡単にセットできるのは、2.の方法になります。

```おまじない2
layer.actions = [NSDictionary dictionaryWithObjectsAndKeys:
                 [NSNull null],@"onOrderIn",    // visibleになったときのフェードイン
                 [NSNull null],@"onOrderOut",   // invisibleになったときのフェードアウト
                 [NSNull null],@"sublayers",
                 [NSNull null],@"contents",
                 [NSNull null],@"position",
                 [NSNull null],@"bounds",
                 [NSNull null],@"transform",
                 nil];
```

なお、UIViewがwrapしているレイヤー(`view.layer`)が暗黙的アニメーションをしないのは、1.の方法によるもので、
`view.layer.delegate`(=`view`)が`-actionForLayer:forKey:`内で常に`NSNull`を返すためです。



## カスタムプロパティをanimatableにする

`CALayer`のサブクラスを作り、`+needsDisplayForKey:`、
`-actionForKey:`、`-drawInContext:`をオーバーライドして、
再描画を繰り返すと、animatableに見せることができます。

```
@implementation MyLayer

+ (BOOL)needsDisplayForKey:(NSString*)key
{
    if ([key isEqualToString:@"angle"]) {
        return YES;
    }

    return [super needsDisplayForKey:key];
}

- (id <CAAction>)actionForKey:(NSString*)key
{
    if ([key isEqualToString:@"angle"]) {

        CABasicAnimation* animation = [CABasicAnimation animation];
        animation.duration = 1;	// デフォルトを通常(0.25)より少し遅めにセット
        return animation;

    }

    return [super actionForKey:key];
}

- (void)drawInContext:(CGContextRef)context
{
	// angle値に応じて描画する
}

@end
```



## おわりに（ライブラリ紹介）

以前、Core Animationを使って、起動画面（スプラッシュ）を好きなようにトランジションさせながら
アプリを開始する簡単なクラスを書いたので、良かったら使ってみてください。

* https://github.com/inamiy/YISplashScreen

その他にも、アニメーションまわりの良ライブラリを、いくつか紹介します。

* https://github.com/warrenm/AHEasing
* https://github.com/dominikhofmann/PRTween
* https://github.com/nicklockwood/iCarousel
* https://github.com/mpospese/MPFoldTransition

下2つは特に有名ですね。ぜひ、お時間のあるときにソースを読んでみましょう。


では、良い月曜日を！