画面遷移のカスタマイズ


はじめに

画面遷移のカスタマイズの流れを簡単にまとめておきます。
今回は以下のような画面遷移を作ります。

画面遷移をカスタマイズする際に必要なもの

  • UIViewControllerAnimatedTransitioning
     遷移時のアニメーションを用意する
  • UIViewControllerTransitioningDelegate
     用意したアニメーションをViewControllerの遷移時に適用させる

通常の画面遷移

カスタマイズ前の画面遷移はこんな感じです。(通常のpresentでの遷移です)

ViewController.swift
let detailImageVC = DetailImageViewController(image: image)
present(detailImageVC, animated: true, completion: nil)

UIViewControllerAnimatedTransitioningを準拠したクラスの用意

このクラスは、遷移アニメーションの生成に必要な以下の要素を渡してイニシャライズするようにします。

  • 画面遷移にかかる時間
  • アニメーションさせるオブジェクト
    今回はCollectionViewのセル画像がアニメーションして、遷移先画面のImageViewに描画されるトランジションなので、「遷移元画面のUICollectionView」と「遷移先画面のUIImageView」を渡すようにしています。
    ※ 実際は以下の抽象化クラスを渡すようにしています。
ImageTransitionProtocol.swift
import UIKit
// 遷移元用
protocol ImageSourceTransitionType: UIViewController {
    var collectionView: UICollectionView! { get }
}
// 遷移先用
protocol ImageDestinationTransitionType: UIViewController {
    var imageView: UIImageView! { get }
}

ViewController.swift
/// 遷移元
class ViewController: UIViewController, ImageSourceTransitionType {
DetailImageViewController.swift
/// 遷移先
class DetailImageViewController: UIViewController, ImageDestinationTransitionType {

UIViewControllerAnimatedTransitioningの準拠クラス

ImagePresentedAnimator.swift
final class ImagePresentedAnimator: NSObject, UIViewControllerAnimatedTransitioning {
    weak var presenting: ImageSourceTransitionType?
    weak var presented: ImageDestinationTransitionType?
    let duration: TimeInterval
    let selectedCellIndex: IndexPath

    init(presenting: ImageSourceTransitionType, presented: ImageDestinationTransitionType, duration: TimeInterval, selectedCellIndex: IndexPath) {
        self.presenting = presenting
        self.presented = presented
        self.duration = duration
        self.selectedCellIndex = selectedCellIndex
    }

    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return duration
    }

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        guard let presenting = presenting, let presented = presented else {
            transitionContext.cancelInteractiveTransition()
            return
        }

        let containerView = transitionContext.containerView
        // 遷移先のViewのFrameに最終配置位置のFrameをset
        presented.view.frame = transitionContext.finalFrame(for: presented)
        presented.view.layoutIfNeeded()
        // 遷移先のsuperViewをaddしないと画面が描画されない
        containerView.addSubview(presented.view)
        presented.view.alpha = 0

        guard let transitionableCell = presenting.collectionView.cellForItem(at: selectedCellIndex) as? CollectionViewCell else {
            transitionContext.cancelInteractiveTransition()
            return
        }

        let animationView = UIView(frame: presenting.view.frame)
        animationView.backgroundColor = .white
        let imageView = UIImageView(frame: transitionableCell.imageView.superview!.convert(transitionableCell.imageView.frame, to: animationView))
        imageView.image = transitionableCell.imageView.image
        imageView.contentMode = transitionableCell.imageView.contentMode
        animationView.addSubview(imageView)
        containerView.addSubview(animationView)

        let animation = UIViewPropertyAnimator(duration: duration, dampingRatio: 0.8) {
            imageView.frame = presented.imageView.frame
        }
        animation.addCompletion { (_) in
            presented.view.alpha = 1
            animationView.removeFromSuperview()
            transitionContext.completeTransition(true)
        }
        animation.startAnimation()
    }
}

UIViewControllerTransitioningDelegateを準拠して用意した遷移アニメーションを適用させる

遷移元のViewControllerにUIViewControllerTransitioningDelegateを適用させます。

ViewController.swift
extension ViewController: UIViewControllerTransitioningDelegate {

    // 遷移を開始したタイミングで呼び出されるため、このメソッド内で用意した遷移アニメーションクラスを返すよにする
    func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        guard let detailImageVC = presented as? ImageDestinationTransitionType else {
            return nil
        }

        return ImagePresentedAnimator(presenting: self, presented: detailImageVC, duration: 1, selectedCellIndex: selectedCellIndex)
    }

    // 遷移先から戻る(dismiss)するタイミングで呼び出されるため、戻る遷移用のアニメーションクラスを返すようにする
    func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        guard let detailImageVC = dismissed as? ImageDestinationTransitionType else {
            return nil
        }

        return ImageDismissedAnimator(presenting: self, presented: detailImageVC, duration: 1, selectedCellIndex: selectedCellIndex)
    }
}

遷移時にデリゲートを準拠する

ViewController.swift
let detailImageVC = DetailImageViewController(image: image)
// 注意: 遷移先のViewControllerのtransitioningDelegateを準拠する
detailImageVC.transitioningDelegate = self
present(detailImageVC, animated: true, completion: nil)

これで画面遷移をカスタマイズできます。

ソースコード

上記で実装したサンプルは以下のリポジトリにあります。
https://github.com/ddd503/Transition-Image-Sample

備考

上記のサンプルで行っている画面遷移は present/dismiss で行っていますが、NavigationControllerを使った場合の push/pop での画面遷移カスタマイズは若干やり方が異なります。
その場合のサンプルは以下に置いてあります。
https://github.com/ddd503/Transition-Image-NavigationController-Sample