写真アプリのようにカスタマイズしたアニメーションで遷移したい！



## はじめに

iOS基本アプリ「写真」は写真を選択すると拡大しpresentできます。このようにしたい、もしくはこれを応用すればいろんなアニメーション作りたいときに参考にしてください。ネット上では様々なサンプルがたくさんありますが、自分なりに理解している部分をメモしました。

## 今回の説明

- CollectionViewからCellを選択する、選択イメージが拡大されながら遷移する。
- イメージ表示画面から閉じるボタンをタップし、CollectionViewへ縮小しながら閉じる。

## 処理するイメージはこんな感じ。

![スクリーンショット 2020-11-18 10.41.19.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/32323/72b529ed-ffe3-415f-1e93-52369d533d70.png)


## 登場人物（クラス、デリゲート）　＊図の説明

### HogeViewController
CollectionViewをもつViewController。以下を用意します。

- 選択したセルの変数
- 選択したイメージのスナップショット変数
- 遷移処理

### HogeViewController : TransitioningDelegate
遷移する際に必要なAnimatorインスタンス生成処理定義します。

- presentの時のAnimator生成ファクトリメソッド
- dismissの時のAnimator生成ファクトリメソッド

### Animator: NSObject, UIViewControllerAnimatedTransitioning
実際にアニメーション処理が定義されている。主にアニメーションするイメージを取得し、アニメーション処理を行う。
アニメーションをカスタマイズしたいならこのクラスを修正することになる。

- 遷移元、遷移先のViewControllerのインスタンスを持っている
- 遷移元のスナップショットをもっている

### FugaViewController

閉じる処理、特に何もすることはない。

## コードから説明

```swift:HogeViewController

func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        // 選択されているセル情報をインスタンス変数に保存
        selectedCell = collectionView.cellForItem(at: indexPath) as? CollectionViewCell
        // 選択されているイメージ（アニメーション対象）情報をインスタンス変数に保存
        selectedCellImageViewSnapshot = selectedCell?.locationImageView.snapshotView(afterScreenUpdates: false)
        
        // 遷移処理
        let fugaViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "FugaViewController") as! FugaViewController

        // 遷移アニメーションプロトコールを委任
        fugaViewController.transitioningDelegate = self

        fugaViewController.modalPresentationStyle = .fullScreen
        fugaViewController.data = data
        present(fugaViewController, animated: true)
    }
```

```swift:UIViewControllerTransitioningDelegate

// プロトコールメソッドの処理を定義
extension HogeViewController: UIViewControllerTransitioningDelegate {

    // presentをする時のAnimatorのインスタンスを生成
    func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        
　　　　　// 要確認： プロジェクトの構造により、ターゲットのViewControllerが違う場合があるため、デバックで確認しターゲットViewControllerを確認する必要がある。
       // もし、取得できなかったら修正が必要。（ターゲットが違う場合、エラーは発生しないが、アニメーションはしない。）
        guard let hogeViewController = presenting as? HogetViewController,
            let fugaViewController = presented as? FugaViewController,
            let selectedCellImageViewSnapshot = selectedCellImageViewSnapshot
            else { return nil }

        animator = Animator(type: .present, hogeViewController: hogeViewController, fugaViewController: fugaViewController, selectedCellImageViewSnapshot: selectedCellImageViewSnapshot)
        return animator
    }

    // dismissをする時のAnimatorのインスタンスを生成
    func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        // // 要確認： プロジェクトの構造により、ターゲットのViewControllerが違う場合があるため、デバックで確認しターゲットViewControllerを確認する必要がある。presentと同様な対応が必要。
        guard let fugaViewController = dismissed as? FugaViewController,
            let selectedCellImageViewSnapshot = selectedCellImageViewSnapshot
            else { return nil }

        animator = Animator(type: .dismiss,  hogeViewController: self, fugaViewController: fugaViewController, selectedCellImageViewSnapshot: selectedCellImageViewSnapshot)
        return animator
    }
}
```

```swift:Animator

enum PresentationType {

    case present
    case dismiss

    var isPresenting: Bool {
        return self == .present
    }
}
// 実際のアニメーション処理を定義する。
final class Animator: NSObject, UIViewControllerAnimatedTransitioning {

    // アニメーションインターバル
    static let duration: TimeInterval = 1.25

    // 遷移元、遷移先の両方のインスタンス変数、また遷移元のスナップショット変数
    private let type: PresentationType
    private let hogeViewController: HogeViewController
    private let hugaViewController: FugaViewController
    private var selectedCellImageViewSnapshot: UIView

    // 遷移元の座標情報
    private let cellImageViewRect: CGRect
    private let cellLabelRect: CGRect

    // インスタンス生成
    init?(type: PresentationType, hogeViewController: HogeViewController, fugaViewController: FugaViewController, selectedCellImageViewSnapshot: UIView) {
        self.type = type
        self.hogeViewController = hogeViewController
        self.fugaViewController = fugaViewController
        self.selectedCellImageViewSnapshot = selectedCellImageViewSnapshot

        guard let window = hogeViewController.view.window ?? fugaViewController.view.window,
            let selectedCell = hogeViewController.selectedCell
            else { return nil }

        // 遷移元の座標情報を取得
        self.cellImageViewRect = selectedCell.locationImageView.convert(selectedCell.locationImageView.bounds, to: window)

        // これも遷移元の座標情報
        self.cellLabelRect = selectedCell.locationLabel.convert(selectedCell.locationLabel.bounds, to: window)
    }

    // インターバル
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return Self.duration
    }

    // 遷移の際にアニメーションする内容を定義
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        
// アニメーションするビュー
        let containerView = transitionContext.containerView

        // 遷移先のビューを貼り付ける
        guard let toView = fugaViewController.view
            else {
                transitionContext.completeTransition(false)
                return
        }

        containerView.addSubview(toView)

        // 遷移元、遷移先のアニメーションするターゲットのスナップショットを取得する。
        guard
            let selectedCell = hogeViewController.selectedCell,
            let window = hogeViewController.view.window ?? fugaViewController.view.window,
            let cellImageSnapshot = selectedCell.locationImageView.snapshotView(afterScreenUpdates: true),
            let controllerImageSnapshot = fugaViewController.locationImageView.snapshotView(afterScreenUpdates: true),
            let cellLabelSnapshot = selectedCell.locationLabel.snapshotView(afterScreenUpdates: true),
            let closeButtonSnapshot = fugaViewController.closeButton.snapshotView(afterScreenUpdates: true)
            else {
                transitionContext.completeTransition(true)
                return
        }

        let isPresenting = type.isPresenting

        // アニメーション終了後処理
        let backgroundView: UIView
        let fadeView = UIView(frame: containerView.bounds)
        fadeView.backgroundColor =  fugaViewController.view.backgroundColor

        // presentアニメーション
        if isPresenting {
            selectedCellImageViewSnapshot = cellImageSnapshot

            backgroundView = UIView(frame: containerView.bounds)
            backgroundView.addSubview(fadeView)
            fadeView.alpha = 0
        } else {
           // dismissアニメーション
            backgroundView = hogeViewController.view.snapshotView(afterScreenUpdates: true) ?? fadeView
            backgroundView.addSubview(fadeView)
        }

        // アニメーション処理
        toView.alpha = 0
        [backgroundView, selectedCellImageViewSnapshot, controllerImageSnapshot, cellLabelSnapshot, closeButtonSnapshot].forEach { containerView.addSubview($0) }

        let controllerImageViewRect = fugaViewController.locationImageView.convert(fugaViewController.locationImageView.bounds, to: window)
        let controllerLabelRect = fugaViewController.locationLabel.convert(fugaViewController.locationLabel.bounds, to: window)
        let closeButtonRect = fugaViewController.closeButton.convert(fugaViewController.closeButton.bounds, to: window)

        [selectedCellImageViewSnapshot, controllerImageSnapshot].forEach {
            $0.frame = isPresenting ? cellImageViewRect : controllerImageViewRect
            $0.layer.cornerRadius = isPresenting ? 12 : 0
            $0.layer.masksToBounds = true
        }

        controllerImageSnapshot.alpha = isPresenting ? 0 : 1
        selectedCellImageViewSnapshot.alpha = isPresenting ? 1 : 0
        cellLabelSnapshot.frame = isPresenting ? cellLabelRect : controllerLabelRect
        closeButtonSnapshot.frame = closeButtonRect
        closeButtonSnapshot.alpha = isPresenting ? 0 : 1

        UIView.animateKeyframes(withDuration: Self.duration, delay: 0, options: .calculationModeCubic, animations: {

            UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 1) {
                self.selectedCellImageViewSnapshot.frame = isPresenting ? controllerImageViewRect : self.cellImageViewRect
                controllerImageSnapshot.frame = isPresenting ? controllerImageViewRect : self.cellImageViewRect
                fadeView.alpha = isPresenting ? 1 : 0
                cellLabelSnapshot.frame = isPresenting ? controllerLabelRect : self.cellLabelRect
                [controllerImageSnapshot, self.selectedCellImageViewSnapshot].forEach {
                    $0.layer.cornerRadius = isPresenting ? 0 : 12
                }
            }
            UIView.addKeyframe(withRelativeStartTime: 0, relativeDuration: 0.6) {
                self.selectedCellImageViewSnapshot.alpha = isPresenting ? 0 : 1
                controllerImageSnapshot.alpha = isPresenting ? 1 : 0
            }
            UIView.addKeyframe(withRelativeStartTime: isPresenting ? 0.7 : 0, relativeDuration: 0.3) {
                closeButtonSnapshot.alpha = isPresenting ? 1 : 0
            }
        }, completion: { _ in
            // アニメーション終了後、削除
            self.selectedCellImageViewSnapshot.removeFromSuperview()
            controllerImageSnapshot.removeFromSuperview()

            backgroundView.removeFromSuperview()
            cellLabelSnapshot.removeFromSuperview()
            closeButtonSnapshot.removeFromSuperview()
            toView.alpha = 1

            transitionContext.completeTransition(true)
        })
    }
}

```

## 参考URL

下記のURLのソースみながら練習したため、クラス名を変えた説明になっています。
（FirstViewControllerをHogeViewController、SecondViewControllerをFugaViewControllerにしています。）

英語が得意な方は下記のリンクをご参考ください。
https://github.com/tungfam/CustomTransitionTutorial