[iOS] 遷移元の画像が遷移先にズームイン・アウトするような画面遷移(UITableView及びUICollectionViewの場合)

2017.03.06

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

1 はじめに

テーブルビューやコレクションビューに一覧されている写真などを選択すると、ズームして表示されるような画面遷移を試してみました。

最初に、実行しているようすです。

2 画面遷移のカスタマイズ

present modallyで画面遷移をカスタマイズする手順は、下記のとおりです。

  1. 遷移を表現するクラスを作成 UIViewControllerAnimatedTransitioning
  2. 1.を指定するdelegateクラスを作成 UIViewControllerTransitioningDelegate
  3. 遷移先ビューのtransitioningDelegateに、2.を設定する

今回は、1.及び2.を実装したクラスCustomTransitionを作成しました。

class CustomTransition: NSObject, UIViewControllerTransitioningDelegate, UIViewControllerAnimatedTransitioning {
    // ・・・・
}

(1) UIViewControllerTransitioningDelegate

UIViewControllerTransitioningDelegateプロトコルを実装したクラスでは、animationController(forPresented:presenting:source:)及びanimationController(forDismissed:)を実装し、それぞれ、遷移と復帰の処理を実装したクラスを返します。

サンプルでは、遷移か復帰かのフラグをセットし、自分自身を返しています。

// MARK: - UIViewControllerTransitioningDelegate
public func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
    isPresent = true
    return self
}

public func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
    isPresent = false
    return self
}

(2) UIViewControllerAnimatedTransitioning

UIViewControllerAnimatedTransitioningプロトコル実装したクラスでは、遷移にかかる時間を返すtransitionDuration(using:)と、遷移時のアニメーション等(本体)animateTransition(using:)が必要です。

// MARK: - UIViewControllerAnimatedTransitioning
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
    return 0.7
}

func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
    if isPresent {
        presentTransition(transitionContext: transitionContext)
    } else {
        dissmissalTransition(transitionContext: transitionContext)
    }
}

遷移処理の本体は、presentTransition(transitionContext:)に、復帰処理の本体は、dissmissalTransition(transitionContext:)によれぞれ実装しました。

3 ズーム処理

ズームでの登場人物は次の5人です。
① ズーム前のイメージ
② アニメーション用のイメージ
③ ズーム後のイメージ
④ 遷移前のビュー
⑤ 遷移後のビュー

001

そして、この登場人物でズームの手順を簡単に表現すると次のようになります。

  • ⑤「遷移後のビュー」を非表示にする
  • ①「ズーム前のイメージ」から②「アニメーション用イメージ」を作成する
  • ④「遷移元のビュー」から①「ズーム前のイメージ」だけを非表示にする
  • ②「アニメーション用イメージ」を、ズーム前のサイズからズーム後のサイズにアニメーションさせる
  • ⑤「遷移後のビュー」を表示する
  • ②「アニメーション用イメージ」を削除する

遷移の本体を実装するanimateTransition(using:)のパラメータであるUIViewControllerContextTransitioningからは、遷移前のビューコントローラー及び遷移後のビューコントローラーが取得可能なので、この2つから、上記の登場人物を芋づる式に引っ張り出してきて操作します。

func presentTransition(transitionContext: UIViewControllerContextTransitioning) {

    // 遷移元ビューコントローラー
    let firstViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from) as! FirstViewController
    // 遷移先ビューコントローラー
    let secondViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to) as! SecondViewController

4 ズームイン

先の手順を実装したものです。ちょっとややこしいのは、セルを取得するところぐらいでしょうか。

// 遷移時のTrastion処理
func presentTransition(transitionContext: UIViewControllerContextTransitioning) {

    // 遷移元、遷移先及び、遷移コンテナの取得
    let firstViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from) as! FirstViewController
    let secondViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to) as! SecondViewController
    let containerView = transitionContext.containerView

    // 遷移元のセルの取得

    let cell:CollectionViewCell = firstViewController.collectionView?.cellForItem(at: (firstViewController.collectionView?.indexPathsForSelectedItems?.first)!) as! CollectionViewCell
    // 遷移元のセルのイメージビューからアニメーション用のビューを作成
    let animationView = UIImageView(image: cell.photoView.image)
    animationView.frame = containerView.convert(cell.photoView.frame, from: cell.photoView.superview)
    // 遷移元のセルのイメージビューを非表示にする
    cell.photoView.isHidden = true

    //遷移後のビューコントローラーを、予め最後の位置まで移動完了させ非表示にする
    secondViewController.view.frame = transitionContext.finalFrame(for: secondViewController)
    secondViewController.view.alpha = 0
    // 遷移後のイメージは、アニメーションが完了するまで非表示にする
    secondViewController.photoView.isHidden = true

    // 遷移コンテナに、遷移後のビューと、アニメーション用のビューを追加する
    containerView.addSubview(secondViewController.view)
    containerView.addSubview(animationView)

    UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
        // 遷移後のビューを徐々に表示する
        secondViewController.view.alpha = 1.0
        // アニメーション用のビューを、遷移後のイメージの位置までアニメーションする
        animationView.frame = containerView.convert(secondViewController.photoView.frame, from: secondViewController.view)
    }, completion: {
        finished in
        // 遷移後のイメージを表示する
        secondViewController.photoView.isHidden = false
        // セルのイメージの非表示を元に戻す
        cell.photoView.isHidden = false
        // アニメーション用のビューを削除する
        animationView.removeFromSuperview()
        transitionContext.completeTransition(true)
    })
}

5 ズームアウト

ズームアウトの処理も、ズームインと殆ど同じです。

// 復帰時のTrastion処理
func dissmissalTransition(transitionContext: UIViewControllerContextTransitioning) {
    // 遷移元、遷移先及び、遷移コンテナの取得
    let secondViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from) as! SecondViewController
    let firstViewController = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to) as! FirstViewController
    let containerView = transitionContext.containerView

    // 遷移元のイメージビューからアニメーション用のビューを作成
    let animationView = secondViewController.photoView.snapshotView(afterScreenUpdates: false)
    animationView?.frame = containerView.convert(secondViewController.photoView.frame, from: secondViewController.photoView.superview)
    // 遷移元のイメージを非表示にする
    secondViewController.photoView.isHidden = true

    // 遷移先のセルを取得
    let cell:CollectionViewCell = firstViewController.collectionView?.cellForItem(at: secondViewController.indexPath) as! CollectionViewCell

    // 遷移先のセルのイメージを非表示
    cell.photoView.isHidden = true

    //遷移後のビューコントローラーを、予め最後の位置まで移動完了させ非表示にする
    firstViewController.view.frame = transitionContext.finalFrame(for: firstViewController)

    // 遷移コンテナに、遷移後のビューと、アニメーション用のビューを追加する
    containerView.insertSubview(firstViewController.view, belowSubview: secondViewController.view)
    containerView.addSubview(animationView!)

    UIView.animate(withDuration: transitionDuration(using: transitionContext), animations: {
        // 遷移元のビューを徐々に非表示にする
        secondViewController.view.alpha = 0
        // アニメーションビューは、遷移後のイメージの位置まで、アニメーションする
        animationView?.frame = containerView.convert(cell.photoView.frame, from: cell.photoView.superview)
    }, completion: {
        finished in
        // アニメーション用のビューを削除する
        animationView?.removeFromSuperview()
        // 遷移元のイメージの非表示を元に戻す
        secondViewController.photoView.isHidden = false
        // セルのイメージの非表示を元に戻す
        cell.photoView.isHidden = false
        transitionContext.completeTransition(true)
    })
}

コードは下記に置きました。気になるところが有りましたら、ぜひ教えてやってください。


github [GitHub] https://github.com/furuya02/CustomTransitionSample02

6 UITableViewのズーム

UITableViewの場合も、セルの取得要領以外は、全く同じです。 コードを置きましたので、不明な点があればご参照ください。


github [GitHub] https://github.com/furuya02/CustomTransitionSample01

7 最後に

今回は、present modallyの画面遷移で、遷移前の画像が、次の画面にズームしているような表現を書いてみました。

UINavigationControllerや、UITabBarControllerの場合は、遷移の実装方法が変わりますが、ズームを表現する考え方は同じです。 遷移元のビューコントローラと、遷移先のビューコントローラーから、登場人物を捕まてズームさせてやってください。

8 参考リンク


遷移元のビューが透過で見えるモーダルビュー(UIPresentationControllerを使わないで、既存の画面遷移をいじる感じで実装してみました)
iOS7 interactive transitions
UIViewControllerTransitioningDelegate
UIViewControllerAnimatedTransitioning