UICollectionViewをジェスチャーで拡大縮小したい(iOS7〜)


iOS7から、UICollectionViewのレイアウトを「アニメーションしながら」変更するために、以下の２種類のAPI群が追加されています。

- タップなどでアニメーションする場合
 - 途中のコントロールが効かない代わりに簡単に使える
 - ``setCollectionViewLayout:animated:``(iOS6から)
 - ``setCollectionViewLayout:animated:completion: ``(iOS7から)
- ジェスチャーなどでアニメーションする場合
 - 途中のコントロールが可能で、「つまんで拡大縮小」といった事が出来る
 - ``startInteractiveTransitionToCollectionViewLayout:completion:`` (iOS7から)
 - ``finishInteractiveTransition``(iOS7から)
 - ``cancelInteractiveTransition``(iOS7から)

今回、ピンチジェスチャーでUICollectionViewを拡大縮小したかったので、``startInteractiveTransitionToCollectionViewLayout:completion:``なAPIを使って実装しました。

サンプルコード
----
[Github](https://github.com/taktamur/PAMTransitionSample)

![video](https://raw.github.com/taktamur/PAMTransitionSample/master/video.gif)


処理の流れ
----
UICollectionViewのinteractiveTransitionとジェスチャーを組み合わせ、以下のような流れで処理が進みます。

- ジェスチャーの開始
 - 次に表示するUICollectionViewLayoutオブジェクトを生成
 - ``startInteractiveTransitionToCollectionViewLayout:completion:``でTransitionを開始
- ジェスチャーの状態が変更
 - PinchGestureの場合scaleの値がとれるので、これを元に0〜1の進捗(Progress)を計算し、UICollectionViewTransitionLayoutの``transitionProgress``にセットする
  - このprogressに応じて、元のレイアウトと新しいレイアウトの間の状態が自動で計算され、表示される
- ジェスチャーが終了
 - progressの値に応じて、``finishInteractiveTransition``もしくは``cancelInteractiveTransition``を呼び出す
 - ジェスチャーを一時停止する(B)
- (隙間時間が発生)(A)
- アニメーションの完了
 - start時にしていたcompletionブロックが呼び出される
 - ジェスチャーを再開する(B)

ここで注意するのは、ジェスチャーの終了からアニメーションの完了までに隙間時間(A)がある事です。progressが0.6等の半端な場合に``finishInteractiveTransition``や``cancelInteractiveTransition``を呼び出すと、progress1.0になるまでアニメーションして（体感1秒程度かかるときがある）、その後にcompletionブロックが呼び出されます。
この隙間時間に次のtransitionを開始しようとするとクラッシュします。
次のTransitionが始まったら、隙間時間をすっ飛ばして次に行きたい所ですが、自分にはそのやり方がわからなかったので、ジェスチャーを一時停止する事にしました(B)。

ソースコードの抜粋
---
ジェスチャー部分だけ抜粋します。 色々なコードを逃がしているので、全体像は[Github](https://github.com/taktamur/PAMTransitionSample/blob/master/PAMTransition/PAMCollectionViewController.m)を見てください。

```objective-c
#define kPAMProgressThreshold 0.5
-(void)pinchAction:(UIPinchGestureRecognizer *)gesture
{
    switch(gesture.state){
        case UIGestureRecognizerStateBegan:
        {
            NSLog(@"begin scale=%f",gesture.scale);
            // 拡大なのか縮小なのかを記録しておく。
            // これをしておかないと、拡大で開始して途中で縮小したりするとおかしな事になる。
            self.zoomingStatus = gesture.pam_zoomStatus;

            // Transitionする先のLayoutを用意
            NSUInteger nextItemCount = [self nextHoraizontalItemCount];
            UICollectionViewLayout *nextLayout=[UICollectionViewFlowLayout layoutWithHorizontalItemCount:nextItemCount];

            // Transitionの開始
            [self.collectionView startInteractiveTransitionToCollectionViewLayout:nextLayout
                                                                       completion:^(BOOL completed, BOOL finish) {
                                                                           NSLog( @"completion");
                                                                           // Transitionの完了時にジェスチャーを止めるので、ここで再開
                                                                           [self enableGesture];
                                                                       }];
        }
            break;
        case UIGestureRecognizerStateChanged:
            // Transitionの進捗(progress)を0.0〜1.0で更新
            self.transitionLayout.transitionProgress = [gesture pam_transitionProgressWithZoomStatus:self.zoomingStatus];
            NSLog( @"transitionProgress=%f",self.transitionLayout.transitionProgress);
            break;
        case UIGestureRecognizerStateEnded:
            // Transitionを完了
            if( self.transitionLayout.transitionProgress > kPAMProgressThreshold ){
                [self.collectionView finishInteractiveTransition];
                self.currentHoraizontalItemCount = [self nextHoraizontalItemCount];
            }else{
                [self.collectionView cancelInteractiveTransition];
            }
            // 最後のアニメーションが終わるまでジェスチャーを停止
            [self disableGesture];
            break;
        default:
            break;
    }
}

```


他のアニメーション
---
今回は１つのUICollectionViewControllerの中でレイアウトを変更しましたが、UIViewController間でのアニメーションも可能なようです。[WWDC 2014のビデオ](https://developer.apple.com/wwdc/videos/)(要ログイン)の中の「Custom Transitions Using View Controllers」にいろいろ紹介されています。なかなかややこしそうですが、やれる事が増えそうです。