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
Like what you read? Give eureka_developers a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.