SwiftでiPhone標準写真アプリのアニメーションを再現してみる

Artboard 1 Copy 2

こんにちは!
CouplesでiOSの開発を担当している遊佐です。

今回はiPhoneの純正の写真アプリやPinterestに使われているズームアニメーションを再現してみたいと思います。

ズームアニメーションとは、一覧画面で写真をタップするとその写真が拡大しながら詳細画面へ遷移し、戻るボタンをタップすると一覧画面の元いた位置に写真が縮小しながら戻っていくというアニメーションです。簡単に実現できるので、こちらのチュートリアルを通して試していただけたら嬉しいです!

iPhoneZoomAnimation


ズームアニメーション

ズームアニメーションを実現させるためには、UIViewControllerAnimatedTransitioningというプロトコルを利用します。

手順としては、
1. UIViewControllerAnimatedTransitioningを採用したTransitionControllerを作成
2. このControllerを画面遷移のデリゲートで指定
以上の手順でデフォルトのアニメーションを置き換えることが可能となります。

これは、Custom Transitionsといって、UINavigationController、UITabBarController、UICollectionViewController、ModalViewControllerの画面遷移で利用することが可能となっております。

それでは、実際にアニメーションをカスタマイズしていきましょう。


実装の方針

今回はUINavigationControllerの遷移アニメーションをカスタマイズしたいと思います。

まず、UINavigationControllerで画面遷移する際には、UINavigationContorollerのDelegateである下記のメソッドが呼ばれます。

optional func navigationController(_ navigationController: UINavigationController,
animationControllerForOperation operation: UINavigationControllerOperation,
fromViewController fromVC: UIViewController,
toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning?

ここでUIViewControllerAnimatedTransitioningを返すことにより、
デフォルトのpush・popアニメーションを置き換えることができます。

次に、アニメーションの実装部分であるTransitionControllerについてです。TransitionControllerにはUIViewControllerAnimatedTransitioningを採用します。
UIViewControllerAnimatedTransitioningでは、以下の2つのメソッドを定義する必要があります。両者とも引数はtransitionContextとなっています。

  • アニメーションの定義をするanimateTransitionメソッド
func animateTransition(_ transitionContext: UIViewControllerContextTransitioning)
  • アニメーションの時間を指定するtransitionDurationメソッド
func transitionDuration(_ transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval

transitionContextは画面遷移コンテキストと呼ばれており、UIViewControllerContextTransitioningプロトコルに準拠したオブジェクトです。UIViewControllerAnimatedTransitioningを採用したTransitionControllerに画面遷移の情報を伝える役割をします。

具体的には、画面遷移のアニメーションをカスタムするのに必要なfromViewController(遷移元のコントローラー)、toViewController(遷移先のコントローラー)、containerView(アニメーションの土台となるビュー)を伝えてくれます。これらを利用して、UIViewControllerAnimatedTransitioningを採用したTransitionControllerに画面遷移のアニメーションを書いていきます。

UIViewControllerAnimatedTransitioningの使い方がわかったところで実際に実装していきましょう。


実装

登場するクラス

  • TransitionController
    UIViewControllerAnimatedTransitioningを採用したController。ここでアニメーションを実装します。

  • TransitionNavigationController
    UINavigationControllerのサブクラス。デリゲートでTransitionControllerを指定します。

  • ViewController
    写真一覧画面のViewController

  • DetailViewController
    写真詳細画面のViewController

TransitionController

まず、UIViewControllerAnimatedTransitioningを採用したTransitionController Classを作成します。

class TransitionController: NSObject, UIViewControllerAnimatedTransitioning {

    // push -> forward == true | pop -> forward == false
    var forward = false
    
    // アニメーションの時間
    func transitionDuration(transitionContext: UIViewControllerContextTransitioning?) -> NSTimeInterval {
        
        return 0.4
    }
    
    // アニメーションの定義
    func animateTransition(transitionContext: UIViewControllerContextTransitioning) {
        
        if self.forward {
            // push時のアニメーション
            forwardTransition(transitionContext)
        } else {
            // pop時のアニメーション
            backwardTransition(transitionContext)
        }
    }

    private func forwardTransition(transitionContext: UIViewControllerContextTransitioning) {
        // ここでpush時のアニメーションを書きます(後ほど説明します)
        // .....
        // .....
    }

    private func backwardTransition(transitionContext: UIViewControllerContextTransitioning) {
        // ここでpop時のアニメーションを書きます(後ほど説明します)
        // .....
        // .....
    }
}

ここでは、アニメーション時間の指定とアニメーションの定義をします。
pushとpopではアニメーションが異なるため、条件分けをしておきます。
アニメーションの中身については後ほど説明します。

TransitionNavigationController

次に、UINavigationControllerを継承するTransitionNavigationController Classを作成します。前述したUINavigationControllerのDelegateで、上記で作成したTransitionControllerを指定します。これでデフォルトのpush・popアニメーションをTransitionControllerで実装したアニメーションに置き換えることが出来ます。

class TransitionNavigationController: UINavigationController, UINavigationControllerDelegate {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        self.delegate = self
    }
    
    func navigationController(navigationController: UINavigationController,
        animationControllerForOperation operation: UINavigationControllerOperation,
        fromViewController fromVC: UIViewController,
        toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning?
    {
        
        let transitionController = TransitionController()
        
        // pushとpopでは異なるアニメーションをさせるので条件を分ける
        switch operation {
        case .Push:
            transitionController.forward = true
            return transitionController
        case .Pop:
            transitionController.forward = false
            return transitionController
        default:
            break
        }
        return nil
    }
}

これで、実装はほぼ完了です。
最後に、先ほどTransitionControllerの作成時に省略したアニメーションの中身を見ていきましょう。

pushアニメーション

アニメーションの実装はpushとpopで異なりますが、まずpushアニメーションから説明いたします。ここでは写真一覧画面のViewControllerクラスから写真詳細のDetailViewControllerクラスへpushしています。

TransitionController

class TransitionController: NSObject, UIViewControllerAnimatedTransitioning {

    //...
    //...
    
    // push時のアニメーション
    private func forwardTransition(transitionContext: UIViewControllerContextTransitioning) {
        
        // 遷移元のViewController
        guard let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey) else {
            return
        }
        
        // 遷移先のViewController
        guard let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) else {
            return
        }
        
        // アニメーションの土台となるビュー
        guard let containerView = transitionContext.containerView() else {
            return
        }
        
        // 遷移先のviewをaddSubviewする(fromVC.viewは最初からcontainerViewがsubviewとして持っている)
        containerView.addSubview(toVC.view)
        
        // addSubviewでレイアウトが崩れるため再レイアウトする
        toVC.view.layoutIfNeeded()
        
        // アニメーション用のimageViewを新しく作成する
        guard let sourceImageView = (fromVC as? ViewController)?.createImageView() else {
            return
        }
        guard let destinationImageView = (toVC as? DetailViewController)?.createImageView() else {
            return
        }
        
        // 遷移先のimageViewをaddSubviewする
        containerView.addSubview(sourceImageView)
        
        toVC.view.alpha = 0.0
        
        UIView.animateWithDuration(transitionDuration(transitionContext), delay: 0.05, options: .CurveEaseInOut, animations: { () -> Void in
            
            // アニメーション開始
            // 遷移先のimageViewのframeとcontetModeを遷移元のimageViewに代入
            sourceImageView.frame = destinationImageView.frame
            sourceImageView.contentMode = destinationImageView.contentMode

            // cellのimageViewを非表示にする
            (fromVC as? ViewController)?.selectedImageView?.hidden = true
            
            toVC.view.alpha = 1.0
            
            }) { (finished) -> Void in

                // アニメーション終了
                transitionContext.completeTransition(true)
        }
    }
    //...
    //...
}

ちょっと長いので、アニメーション開始前、開始時、完了時に分けて説明します。

アニメーション開始前
  • transitionContextから、遷移元、遷移先のViewControllerそれぞれfromVC、toVC、アニメーションを行う土台となるcontainerViewを取得します。
  • 遷移先のDetailViewControllerのviewをaddSubviewします。遷移元のViewControllerはすでにcontainerViewがsubviewとして持っているので、addSubviewする必要はありません。
  • ViewController、DetailViewControllerそれぞれに乗っているimageViewからアニメーション用のimageViewを作成し、containerViewにaddSubviewします。こちらはViewControllerとDetailViewControllerに定義したcreateImageViewメソッドを使って作成します。createImageViewメソッドの中身は以下のようになっています。

ViewController

        class ViewController: UIViewController {
            //...
            //...
            func createImageView() -> UIImageView? {
        
               guard let selectedImageView = self.selectedImageView else {
                   return nil
               }
               let imageView = UIImageView(image: selectedImageView.image)
               imageView.contentMode = .ScaleAspectFill
               imageView.frame = selectedImageView.convertRect(selectedImageView.frame, toView: self.view)
               return imageView
            }
            //...
            //...
        }

ViewControllerでは、self.selectedImageView(cellに乗っている実体のimageView)のimageからアニメーション用にimageViewのコピーを作成しています。

DetailViewController

        class DetailViewController: UIViewController {
            //...
            //...
            func createImageView() -> UIImageView? {
        
               guard let detailImageView = self.imageView else {
                   return nil
               }
               let imageView = UIImageView(image: self.image)
               imageView.contentMode = .ScaleAspectFit
               imageView.frame = detailImageView.frame
               return imageView
            }
            //...
            //...
        }

DetailViewControllerでは、self.imageView(DetailViewControllerに乗っている実体のimageView)のimageからアニメーション用にimageViewのコピーを作成しています。

アニメーション開始時
  • このアニメーション用のimageViewに遷移先のimageViewのframeとcontetModeを代入します。
  • cellのimageViewを非表示にします。
アニメーション完了時
  • transitionContextに終了を通知して完了です。

popアニメーション

次に、popのアニメーションに関してですが、pushアニメーションとまったく逆のことをすればよいです。

TransitionController

class TransitionController: NSObject, UIViewControllerAnimatedTransitioning {

    //...
    //...
    
    // pop時のアニメーション
    private func backwardTransition(transitionContext: UIViewControllerContextTransitioning) {
        
        // pushと逆のアニメーションを書く

        guard let fromVC = transitionContext.viewControllerForKey(UITransitionContextFromViewControllerKey) else {
            return
        }
        guard let toVC = transitionContext.viewControllerForKey(UITransitionContextToViewControllerKey) else {
            return
        }
        guard let containerView = transitionContext.containerView() else {
            return
        }

        // 最初からcontainerViewがsubviewとして持っているfromVC.viewを削除
        fromVC.view.removeFromSuperview()
        
        // toView -> fromViewの順にaddSubview
        containerView.addSubview(toVC.view)
        containerView.addSubview(fromVC.view)
        
        guard let sourceImageView = (fromVC as? DetailViewController)?.createImageView() else {
            return
        }
        guard let destinationImageView = (toVC as? ViewController)?.createImageView() else {
            return
        }

        containerView.addSubview(sourceImageView)
        
        UIView.animateWithDuration(transitionDuration(transitionContext), delay: 0.05, options: .CurveEaseInOut, animations: { () -> Void in

            sourceImageView.frame = destinationImageView.frame
            fromVC.view.alpha = 0.0
            
            }) { (finished) -> Void in
                
                sourceImageView.hidden = true
                
                (toVC as? ViewController)?.selectedImageView?.hidden = false

                transitionContext.completeTransition(true)
        }
    }
    //...
    //...
}
アニメーション開始前
  • pushの時と同じようにtransitionContextから、fromVC、toVC、containerViewを取得します。pushの時と異なり、DetailViewControllerがfromVC、ViewControllerがtoVCとなります。
  • containerViewがfromVCのviewを最初からsubviewとして持っているため、fromVCのviewを一旦削除します。
  • toView、fromViewの順でcontainerViewにaddSubviewします。
  • pushの時と同じように、アニメーション用のimageViewであるsourceImageViewをaddSubviewします。
アニメーション開始時
  • アニメーション用のimageViewに遷移先のimageViewのframeを代入します。
アニメーション完了時
  • 非表示にしていたcellのimageViewを表示します。
  • transitionContextに終了を通知して完了です。

以上で完成となります。

これで、このように綺麗にアニメーションしてくれます。
ZoomAnimation

最後に

今回はUIViewControllerAnimatedTransitioningを利用して、ズームアニメーションを再現してみました。
UIViewControllerAnimatedTransitioningを使うことによって、ズームアニメーションだけでなく、自由自在の画面遷移アニメーションを表現することが可能なので、いろんなアプリのアニメーションを真似て見ると面白いかもしれません。

次回は、よりiPhoneの写真アプリに近づけるために、今回のズームアニメーションを利用してインタラクティブにpopさせる方法をご紹介したいと思います。お楽しみに!

サンプルコードはこちらのリポジトリにございます。
https://github.com/yusayusa/ZoomAnimation

  • このエントリーをはてなブックマークに追加

エウレカでは、一緒に働いていただける方を絶賛募集中です。募集中の職種はこちらからご確認ください!皆様のエントリーをお待ちしております!

Recommend

Go言語初心者がハマった2つのポイント

FalbaTech製ErgoDoxを使ってみた