Help us understand the problem. What is going on with this article?

UIViewPropertyAnimatorを用いて独自の画面遷移を実装する

More than 3 years have passed since last update.

この記事はOthloTech Advent Calendar 2016の16日目の記事です。


iOS10から利用可能なUIViewPropertyAnimatorクラスを用いることでアニメーションに対して柔軟に対応できるようになりました。
今回はそれを用いて画面遷移を自作してみました。

自分のメモとして少し事細かく書いている部分があるので、さらっと読みたい方は最初のアニメーションの実装例と最後のアニメーションを用いた画面遷移の部分を読むといいかと思います。

開発環境

Xcode 8.1
Swift 3.0.1

アニメーションの実装例

まずは簡単なアニメーションの実装を行いたいと思います。
以下が基本的なアニメーションを行う部分になります。

let timing: UITimingCurveProvider = UICubicTimingParameters(animationCurve: .easeIn)
let animator = UIViewPropertyAnimator(duration: 2.0, timingParameters: timing)  // 2秒かけて

animator.addAnimations {
    // アニメーションの処理
}

animator.addCompletion {_ in
    // アニメーション終了時の処理
}

animator.startAnimation()  // アニメーションを開始する

UITimingCurveProviderでアニメーションのタイミングを設定することができます。

種類 説明
easeIn 最初がゆっくり
easeInOut 最初と最後がゆっくり
easeOut 最後がゆっくり
linear 一定の速度

また、以下のように書くことで自分でイージングの設定を行えます。

let c1: CGPoint = CGPoint(x:0.0, y:0.7)
let c2: CGPoint = CGPoint(x:1.0, y:0.3)
let timing = UICubicTimingParameters(controlPoint1: c1, controlPoint2: c2)


そして、UIViewPropertyAnimatorを用いることでアニメーションに対する様々なアプローチが行えます。

実装例を書いてみました。

ViewController.swift
import UIKit

class ViewController: UIViewController {

    var squareView: UIView!


    override func viewDidLoad() {
        super.viewDidLoad()

        // UIView
        squareView = UIView(frame: CGRect(x:0, y:350, width:50, height:50))
        squareView.backgroundColor = UIColor.red
        self.view.addSubview(squareView)
    }

    override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
    }

    override func viewDidAppear(_ animated: Bool) {
        let timing: UITimingCurveProvider = UICubicTimingParameters(animationCurve: .easeIn)
        let animator = UIViewPropertyAnimator(duration: 2.0, timingParameters: timing)  // 2秒かけて

        // アニメーション
        animator.addAnimations {
            self.squareView.center = CGPoint(x: self.view.frame.width-25, y: self.squareView.center.y)
            self.squareView.transform = CGAffineTransform(rotationAngle: CGFloat(M_PI))  // 180度回転
        }

        // アニメーション後
        animator.addCompletion {_ in
            self.squareView.backgroundColor = UIColor.orange
        }

        animator.startAnimation()  // 開始
    }


}

このように書くと以下のようになります。

squareViewAnimation

自作の画面遷移を実装する

次は先ほどのアニメーションの作り方を元に画面遷移を実装していきます。

とりあえず画面遷移をコードで行う

下準備としてボタンを押したら画面遷移(ViewController.swift -> SecondViewController.swift)するコードを書いていきます。


UIButton
let button = UIButton()
button.setTitle("next", for: .normal)
button.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
button.center = self.view.center
button.backgroundColor = UIColor.orange
button.setTitleColor(UIColor.white, for: .normal)
button.addTarget(self, action: #selector(touchButton(sender:)), for: .touchUpInside)
self.view.addSubview(button)

UIButtonを作成したら、押されたときに下記メソッドを呼んでSecondViewControllerに遷移します。

func touchButton(sender: UIButton) {
    let storyboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
    let nextView = storyboard.instantiateViewController(withIdentifier: "second") as! SecondViewController

    present(nextView, animated: true, completion: nil)
}

このときに遷移がうまく行かない方は、Main.storyboardで以下を確認してください。

  • ViewControllerに Is Initial View Controller のチェックがついていない
  • SecondViewControllerのIdentityの Storyboard ID を書いていない(今回はIDを second とする)


次にSecondViewControllerからViewControllerに戻るときですが、これは先ほどと同様にボタンが押されたときのメソッドを作り、その中に以下の一文を入れるだけで戻れます。

self.dismiss(animated: true, completion: nil)


これで画面遷移を行える状態になりました。

defaltAnimation

しかし、まだ遷移の仕方がデフォルトの状態なのでここから独自の画面遷移を実装していきます。

アニメーションを用いた画面遷移

Animatorクラスを作っていきます。
このクラスに先ほど紹介したアニメーション処理を書いていきます。

通常は、UIViewControllerTransitioningDelegateを加えた時に以下の2つのメソッドでアニメーション処理を行います。

func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return 2.0  // 遷移する時間
}

func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        // アニメーション処理
}

今回は、UIViewPropertyAnimatorを使用したいため、 interruptibleAnimatorメソッドを用いります。
実際に書いてみるとこのような感じになります。

Animator.swift
import UIKit

class Animator: NSObject, UIViewControllerAnimatedTransitioning {

    let animationDuration = 1.0


    /**
     * 遷移時間を返す
     */
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return animationDuration
    }

    /**
     * アニメーションを開始する処理
     */
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        self.interruptibleAnimator(using: transitionContext).startAnimation()
    }

    /**
     * UIViewPropertyAnimatorを返す
     */
    func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
        let timing: UITimingCurveProvider = UICubicTimingParameters(animationCurve: .linear)
        let animator = UIViewPropertyAnimator(duration: animationDuration, timingParameters: timing)

        animator.addAnimations {
        }

        animator.addCompletion {_ in
            transitionContext.completeTransition(true)
        }

        return animator
    }


}


もちろん遷移時にアニメーションを呼びたいのでViewControllerにもUIViewControllerTransitioningDelegateを定義して、delegateの設定をしたらanimationControllerメソッドを呼べるようにします。

ViewConrtoller.swift
/**
 * buttonが押された時の処理
 */
func touchButton(sender: UIButton) {
    let storyboard: UIStoryboard = UIStoryboard(name: "Main", bundle: nil)
    let nextView = storyboard.instantiateViewController(withIdentifier: "second") as! SecondViewController

    nextView.transitioningDelegate = self  // delegate設定

    present(nextView, animated: true, completion: nil)
}

/**
 * 遷移時のアニメーション処理
 */
func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
    return animator
}

/**
 * 戻り時のアニメーション処理
 */
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
    return animator
}

遷移時と戻り時でアニメーションの処理を分けたい時は、例えばBool型の変数を用意しておいて、animationControllerの中でどちらかをtrue、falseにして、それぞれのアニメーション処理をAnimatorクラス内で書けばいいかと思います。




以上を踏まえて実際に作ってみたものがこちらです。

slideTransitionAnimation


Animatorクラスはこのように実装しています。

Animator.swift
import UIKit

class Animator: NSObject, UIViewControllerAnimatedTransitioning {

    let animationDuration = 0.5
    var transition: Bool = false


    /**
     * 遷移時間を返す
     */
    func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
        return animationDuration
    }

    /**
     * アニメーションを開始する処理
     */
    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        self.interruptibleAnimator(using: transitionContext).startAnimation()
    }

    /**
     * UIViewPropertyAnimatorを返す
     */
    func interruptibleAnimator(using transitionContext: UIViewControllerContextTransitioning) -> UIViewImplicitlyAnimating {
        let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)
        let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)

        let fromView = fromVC?.view  // 元のview
        let toView = toVC?.view  // 遷移先のview

        let containerView = transitionContext.containerView
        containerView.addSubview(fromView!)
        containerView.addSubview(toView!)

        let timing: UITimingCurveProvider = UICubicTimingParameters(animationCurve: .linear)
        let animator = UIViewPropertyAnimator(duration: animationDuration, timingParameters: timing)

        if transition {
            // ----------遷移時----------
            // 画面右側に遷移先のviewをセットする
            toView?.frame = CGRect(x:containerView.frame.size.width,
                                   y:0,
                                   width:containerView.frame.size.width,
                                   height:containerView.frame.size.height)

            // アニメーション処理
            animator.addAnimations {
                fromView?.alpha = 0.0
                fromView?.frame = CGRect(x:-containerView.frame.size.width,
                                         y:0,
                                         width:containerView.frame.size.width,
                                         height:containerView.frame.size.height)
                toView?.frame = containerView.frame
            }
        } else {
            // ----------戻り時----------
            // アニメーション処理
            animator.addAnimations {
                toView?.alpha = 1.0
                fromView?.frame = CGRect(x:containerView.frame.size.width,
                                         y:0,
                                         width:containerView.frame.size.width,
                                         height:containerView.frame.size.height)
                toView?.frame = containerView.frame
            }
        }

        // アニメーション終わりの処理
        animator.addCompletion {_ in
            transitionContext.completeTransition(true)
        }

        return animator
    }


}

アニメーションの内容は、このような流れです。

  1. 遷移先の画面を右側にセットする
  2. 遷移したときに画面中央に移動。同時に元の画面を画面左に移動しながらalpha値を下げて徐々に暗くする
  3. 戻るときは1, 2と逆のことをする


このように書くだけでとても簡単にアニメーションを用いた画面遷移を行うことができました。
これらを参考に自分なりの画面遷移を作って楽しんでみてはいかがでしょうか:upside_down:

おまけ

springAnimation

以下のパラメータを用いるとバネのようなアニメーションを実装することができます。

let springParameters: UISpringTimingParameters = UISpringTimingParameters(dampingRatio: 0.3, initialVelocity: CGVector.zero)
  • dampingRatio : 減衰比(値が0.0から1.0に大きくなるほど揺れが弱くなる)
  • initialVelocity : 初期速度


これらを組み合わせるとこのような画面遷移が作れたりします!

springTransitionAnimation


おわりに

自分でアニメーションから作ろうとするとすごく大変そうだなと思っていましたが、UIViewPropertyAnimatorなどを用いることで簡単に実装できました。
また、普段はMain.storyboardを使用して書いているところをコードでかいたので、そちらも勉強になりました。
やりすぎはよくないですが、シンプルな遷移に飽きたという方はこれを機にオリジナルの画面遷移を作って楽しんでみてください!

最後に作ったおまけはGithubに載せておくので、良ければ参考にしてください:laughing:

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away