[Objective-C] フリップアニメーションでビューを切り替える


色々方法があるみたいなので、それをまとめておきます。
まずは一番シンプル？な方法。

##`UIView#transitionFromView:toView:duration:options:completion:`を使う

横にフリップして表示を切り替えるパターンの場合は、上記メソッドを実行するだけで簡単に制御できます。

```objc
UIView *view1 = [[UIView alloc] initWithFrame:self.view.bounds];
view1.backgroundColor = [UIColor redColor];

UIView *view2 = [[UIView alloc] initWithFrame:self.view.bounds];
view2.backgroundColor = [UIColor blueColor];

[self.view addSubview:view1];
[self.view addSubview:view2];

// 追加と同時に行うとアニメーションしないので、サンプルでは処理をちょっとだけ遅延させています
dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_MSEC));
dispatch_after(time, dispatch_get_main_queue(), ^{
	[UIView transitionFromView:view2 toView:view1 duration:5 options:UIViewAnimationOptionTransitionFlipFromLeft completion:^(BOOL finished) {
		NSLog(@"Animation end.");
	}]
});
```

###options

オプションに指定できるのは以下の項目が指定できるよう。

| オプション名 | 効果 |
|------------|-----------|
| UIViewAnimationOptionTransitionNone            | アニメーションなし |
| UIViewAnimationOptionTransitionFlipFromLeft    | フリップが左から右へ |
| UIViewAnimationOptionTransitionFlipFromRight   | フリップが右から左へ |
| UIViewAnimationOptionTransitionCurlUp          | 本のページがめくれるように上に |
| UIViewAnimationOptionTransitionCurlDown        | 本のページがめくれるように下に |
| UIViewAnimationOptionTransitionCrossDissolve   | ディゾルブ（クロスフェード） |
| UIViewAnimationOptionTransitionFlipFromTop     | フリップが上から下に |
| UIViewAnimationOptionTransitionFlipFromBottom  | フリップが下から上に |


##`CATranform3D`を使ってアニメーションを自作する

`CATransform3D`を使うことでフリップなどの3Dの演出を自作することが出来ます。
簡単なアニメーションだけであれば、サンプルを変更するだけで比較的それらしいアニメーションが作れると思います。

今回のサンプルは、[Githubにサンプル](https://github.com/edom18/FlipButtonSample/tree/master/FlipButtonSample)を上げているのでよかったら見てみてください。

とはいえ3Dについての知識があったほうが、なにがどんな処理をしているかが分かるので簡単にでも学んでおくといいと思います。
3Dに関しては以前、[自作の3Dエンジンを作ってみる際にまとめた記事](http://qiita.com/edo_m18/items/946df01d58b8cd3143d4)があるので、よかったら見てみてください。（専門家ではないのであまり分かりやすくはまとめられてませんが・・）

###3Dエフェクトを作る

3Dのアニメーションを作るには、`view.layer.sublayerTransform`にパースペクティブを設定する必要があります。
これを設定しないと3Dが有効にならず、2Dによる動きになってしまうので注意が必要です。

※ちなみにパースペクティブとは遠近感を表す用語です。数値を色々変更すると遠近感のかかり方が変わります。

####パースペクティブを設定する

```objc
// パースペクティブを設定
CATransform3D perspective = CATransform3DIdentity;
perspective.m34 = 1.0 / 500;
self.view.layer.sublayerTransform = perspective;
```

ここで行っていることは、`CATransform3D`型の変数に初期値を設定します。
（Identityは3Dマトリクスの単位行列で、数字で言えば`1`と同じ、演算しても結果が変わらない値です）
そして、`m34`要素にパースペクティブの値を設定します。

その値を`CALayer`のプロパティである`sublayerTransform`に設定しています。
これは、サブレイヤーのレンダリング時に適用されるtransformの値で、3D的には親の座標空間の設定、ということになります。

####背面の描画をオフに

3Dの世界では`カリング`という概念があります。
3Dはただでさえ重い処理なので、少しでも軽くする必要があります。
そこで、いわゆるポリゴンの裏は描画する必要がない部分です。背面カリングを有効にすることで、この背面をレンダリングしない、ということが可能になります。

これに相当する設定は以下のようにすることで、背面（つまり変形した結果が裏）の場合はレンダリングされなくなります。

```objc
view.layer.doubleSided = NO;
```

####アニメーションを実装する

以上の準備を踏まえて、実際のアニメーションのサンプルコードを載せておきます。

#####ビューの生成

```objc
@property (strong, nonatomic) UIView *frontView;
@property (strong, nonatomic) UIView *backView;
```

```objc
/**
 *  Viewのセットアップ
 */
- (void)setup
{
    CGRect viewFrame = CGRectMake(0, 100, self.view.bounds.size.width, 44);
    CGRect btnFrame  = CGRectMake(0, 0, viewFrame.size.width, viewFrame.size.height);
    
    // フリップするふたつのビューを生成
	self.frontView = [[UIView alloc] initWithFrame:viewFrame];
	self.frontView.backgroundColor   = [UIColor redColor];
    self.frontView.layer.doubleSided = NO; // 背面をレンダリングしない
    
	self.backView = [[UIView alloc] initWithFrame:viewFrame];
	self.backView.backgroundColor   = [UIColor blueColor];
    self.backView.layer.doubleSided = NO; // 背面をレンダリングしない
    self.backView.layer.transform   = CATransform3DMakeRotation(M_PI, 1.0, 0.0, 0); // X軸に対して180度回転
    
    UIButton *backBtn = [UIButton buttonWithType:UIButtonTypeCustom];
    backBtn.frame = btnFrame;
    [backBtn setTitle:@"フリップ" forState:UIControlStateNormal];
    [backBtn addTarget:self
                action:@selector(toggle:)
      forControlEvents:UIControlEventTouchUpInside];
    [self.backView addSubview:backBtn];
    
    UIButton *frontBtn = [UIButton buttonWithType:UIButtonTypeCustom];
    frontBtn.frame = btnFrame;
    [frontBtn setTitle:@"フリップ" forState:UIControlStateNormal];
    [frontBtn addTarget:self
                 action:@selector(toggle:)
       forControlEvents:UIControlEventTouchUpInside];
    [self.frontView addSubview:frontBtn];

    // ビューを追加
	[self.view addSubview:self.backView];
	[self.view addSubview:self.frontView];
    
    
	// パースペクティブを設定
	CATransform3D perspective = CATransform3DIdentity; //Identityは3Dマトリクスの初期値。数字で言えば`1`と同じ。演算しても結果が変わらない
	perspective.m34 = 1.0 / 1000;
	self.view.layer.sublayerTransform = perspective;
}
```

#####フリップのトグル

```objc
/**
 *  アニメーションのトグル
 */
- (void)toggle:(id)sender
{
    static BOOL toggleFlg = NO;

    [CATransaction begin];
    
	CATransform3D toFrontTransform = CATransform3DMakeRotation(M_PI * 2, 1.0, 0.0, 0.0);
	CATransform3D toBackTransform  = CATransform3DMakeRotation(M_PI,     1.0, 0.0, 0.0);
	
	// BasicAnimationを開始
	CABasicAnimation *toFrontAnim = [CABasicAnimation animationWithKeyPath:@"transform"];
	CABasicAnimation *toBackAnim  = [CABasicAnimation animationWithKeyPath:@"transform"];
    
	// アニメーション終了後の状態を維持
	toFrontAnim.removedOnCompletion = NO;
	toFrontAnim.fillMode            = kCAFillModeForwards;
	toBackAnim.removedOnCompletion  = NO;
	toBackAnim.fillMode             = kCAFillModeForwards;
    
    static const CFTimeInterval duration = 0.5;
    
	toFrontAnim.toValue   = [NSNumber valueWithCATransform3D:toFrontTransform];
	toFrontAnim.duration  = duration;
    
	toBackAnim.toValue    = [NSNumber valueWithCATransform3D:toBackTransform];
	toBackAnim.duration   = duration;
    
	// アニメーション終了時の処理
    [CATransaction setCompletionBlock:^{
        // アニメーション後の状態を設定
        if (toggleFlg) {
            self.frontView.layer.transform = CATransform3DMakeRotation(M_PI, 1.0, 0.0, 0);
            self.backView.layer.transform  = CATransform3DMakeRotation(0,    1.0, 0.0, 0);
            self.frontView.hidden = YES;
        }
        else {
            self.frontView.layer.transform = CATransform3DMakeRotation(0,    1.0, 0.0, 0);
            self.backView.layer.transform  = CATransform3DMakeRotation(M_PI, 1.0, 0.0, 0);
            self.backView.hidden = YES;
        }
    }];
    
    self.frontView.hidden = NO;
    self.backView.hidden  = NO;
    
    if ((toggleFlg = !toggleFlg)) {
        [self.frontView.layer addAnimation:toBackAnim  forKey:@"toBack"];
        [self.backView.layer  addAnimation:toFrontAnim forKey:@"toFront"];
    }
    else {
        [self.frontView.layer addAnimation:toFrontAnim forKey:@"toBack"];
        [self.backView.layer  addAnimation:toBackAnim  forKey:@"toFront"];
    }
    
    
    // アニメーションをコミット
	[CATransaction commit];
}
```

ざっくり説明すると、背面用と表面用のViewを作っておき、それをアニメーションで逆にしている、というものです。

#####注意点

* サンプルには載せていませんが、上記のままだと背面のボタンなどのアクションが行えません。
理由は、上記の演出はあくまで見た目上だけなのでレイヤーの重なり順などは変化していません。
なので、アニメーション終了後にレイヤーの重なり順を変えるか、表用Viewの`hidden`プロパティを`NO`にしてシステム上も非表示扱いにしておかないとなりません。

* それから、アニメーション後の設定をあえて書いていますが、アニメーション後の状態は見た目だけのようで、ボタンの位置は回転の適用前の位置のままでした。なので、アニメーションさせる以外に、実際の値も変更しておく必要があります。

* `setCompletion:`メソッドは、`addAnimation:forKey:`メソッドの前に設定しておかないと、即座に実行されてしまってうまく動きません。



##参考記事・リファレンス
* [CALayerで3Dグラフィックス？](http://d.hatena.ne.jp/terazzo/20111207/1323283155)
* [CATransaction Class Reference](https://developer.apple.com/library/Mac/DOCUMENTATION/GraphicsImaging/Reference/CATransaction_class/Introduction/Introduction.html)
* [CATransaction completion being called immediately](http://stackoverflow.com/questions/19975938/catransaction-completion-being-called-immediately)