4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

UIViewControllerAnimatedTransitioningでカスタム画面遷移アニメーションを実装

Posted at

はじめに

UIViewControllerAnimatedTransitioningを使ってカスタム画面遷移アニメーションを実装する方法になります。
検索した情報が古かったり、よくわからないところがあったので、現時点での実装方法とつまずいたところをまとめました。

対象バージョン

  • Xcode ver.11.6
  • iOS ver 13.5.1

Present/DismissとPush/Popで実装方法が変わる

ViewController間で直接画面遷移する方法(Present/Dismiss)と、NavigationControllerを使用した画面遷移(Push/Pop)では実装方法が変わります。

Present/Dismissで画面遷移する場合

画面遷移イメージ

概要と注意点

最初にViewController間で直接画面遷移する方法になります。真ん中のボタンを押下すると上から遷移先の画面が降りて遷移します。
注意点としてはmodalPresentationStyleを.fullScreenにします。
.customだとUIViewControllerTransitioningDelegateのイベントが着火されないです。
UIViewControllerTransitioningDelegateは遷移先のViewControllerにデリゲートさせます。

遷移元(FirstViewController)

import UIKit

class FirstViewController: UIViewController {
    let btn = UIButton(type: .system)

    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .green
        btn.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
        btn.setTitle("FirstView", for: .normal)
        btn.center = view.center
        view.addSubview(btn)
        btn.addTarget(self, action: #selector(touch), for: .touchUpInside)
    }
    
    @objc func touch(sender: UIButton) {
        let vc = SecondViewController()
        vc.modalPresentationStyle = .fullScreen
        present(vc, animated: true, completion: nil)
    }
}

遷移先(SecondViewController)

UIViewControllerTransitioningDelegateのPresentとDismissで呼ばれるメソッド内でどちらが呼ばれたのかフラグを切り替えて、アニメーションする際の判定で使用しています。

import UIKit

class SecondViewController: UIViewController {
    
    let animator = Animator()
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.transitioningDelegate = self
        
        view.backgroundColor = .orange
        let btn = UIButton(type: .system)
        btn.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
        btn.setTitle("SecondView", for: .normal)
        btn.center = view.center
        view.addSubview(btn)
        btn.addTarget(self, action: #selector(touch), for: .touchUpInside)
    }
    
    @objc func touch(sender: UIButton) {
        dismiss(animated: true, completion: nil)
    }
}

extension SecondViewController: UIViewControllerTransitioningDelegate {
    func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        // Presentで呼ばれる
        animator.presenting = true
        return animator
    }

    func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        // Dismissで呼ばれる
        animator.presenting = false
        return animator
    }
}

カスタムアニメーション

PresentとDismissの際のアニメーションを実装しています。

import UIKit

class Animator: NSObject, UIViewControllerAnimatedTransitioning {
    let movedDistance: CGFloat = 70.0 // 遷移元のviewのずれる分の距離
    let duration = 0.3
    var presenting = false // 遷移するときtrue(戻るときfalse)

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

    func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
        let fromVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.from)
        let toVC = transitionContext.viewController(forKey: UITransitionContextViewControllerKey.to)

        // 遷移するときと戻るときとで処理を変える
        if presenting {
            presentTransition(transitionContext: transitionContext, toView: toVC!.view, fromView: fromVC!.view)
        } else {
            dismissTransition(transitionContext: transitionContext, toView: toVC!.view, fromView: fromVC!.view)
        }
    }

    // 遷移するときのアニメーション
    func presentTransition(transitionContext: UIViewControllerContextTransitioning, toView: UIView, fromView: UIView) {
        let containerView = transitionContext.containerView
        containerView.insertSubview(toView, aboveSubview: fromView)

        // 遷移先のviewを画面の上部に移動させておく
        toView.frame = toView.frame.offsetBy(dx: 0, dy: -containerView.frame.size.height)

        UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0.05, options: .curveEaseInOut, animations: { () -> Void in
            // 遷移元のviewを下げながらalphaを暗くする
            fromView.frame = fromView.frame.offsetBy(dx: 0, dy: self.movedDistance)
            fromView.alpha = 0.7

            // 遷移先のviewを画面全体に移動
            toView.frame = containerView.frame
        }) { (finished) -> Void in
            // 変更をもとに戻してアニメーション終了
            fromView.frame = fromView.frame.offsetBy(dx: 0, dy: -self.movedDistance)
            fromView.alpha = 1.0
            transitionContext.completeTransition(true)
        }
    }

    // 戻るときのアニメーション
    func dismissTransition(transitionContext: UIViewControllerContextTransitioning, toView: UIView, fromView: UIView) {
        let containerView = transitionContext.containerView
        containerView.insertSubview(toView, belowSubview: fromView)

        // 遷移先のviewを画面の下部に移動させておく
        toView.frame = toView.frame.offsetBy(dx: 0, dy: containerView.frame.size.height)

        UIView.animate(withDuration: transitionDuration(using: transitionContext), delay: 0.05, options: .curveEaseInOut, animations: { () -> Void in
            // 遷移元のviewを上げながらalphaを暗くする
            fromView.frame = fromView.frame.offsetBy(dx: 0, dy: -self.movedDistance)
            fromView.alpha = 0.1
            
            // 遷移先のviewを画面全体に移動
            toView.frame = containerView.frame
        }) { (finished) -> Void in
            // 変更をもとに戻してアニメーション終了
            fromView.frame = fromView.frame.offsetBy(dx: 0, dy: self.movedDistance) // 元の位置に戻す
            fromView.alpha = 1.0
            transitionContext.completeTransition(true)
        }
    }
}

Push/Pop(NavigationController)で画面遷移する場合

画面遷移イメージ

概要と注意点

画面遷移のアニメーションをPresent/Dismissと同じになります。
NavigationControllerを使用しているので、UIViewControllerTransitioningDelegateではなくUINavigationControllerのサブクラスとしてCustomNavigationControllerを作成し、
UINavigationControllerDelegateをを継承させてnavigationController(_:animationControllerFor:from: to:)でPush/Popの判定をしています。
アニメーション用のクラスとしてはPresent/Dismissで作成したAnimatorクラスをそのまま使用しています。

遷移元(FirstViewController)

NavigationControllerのpushViewControllerを使用して遷移先を呼び出しています。
こちらではvc.modalPresentationStyle = .fullScreenは設定しなくてもいいようです。

import UIKit

class FirstViewController: UIViewController {
    let btn = UIButton(type: .system)

    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .green
        btn.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
        btn.setTitle("FirstView", for: .normal)
        btn.center = view.center
        view.addSubview(btn)
        btn.addTarget(self, action: #selector(touch), for: .touchUpInside)
    }
    
    @objc func touch(sender: UIButton) {
        let vc = SecondViewController()
        navigationController?.pushViewController(vc, animated: true)
    }
}

遷移先(SecondViewController)

前の画面に戻るときはのpopViewControllerで遷移元を呼び出す。

import UIKit

class SecondViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        view.backgroundColor = .orange
        let btn = UIButton(type: .system)
        btn.frame = CGRect(x: 0, y: 0, width: 100, height: 100)
        btn.setTitle("SecondView", for: .normal)
        btn.center = view.center
        view.addSubview(btn)
        btn.addTarget(self, action: #selector(touch), for: .touchUpInside)
    }
    
    @objc func touch(sender: UIButton) {
        navigationController?.popViewController(animated: true)
    }
}

NavigationControllerのカスタマイズ

UINavigationControllerのサブクラスとしてCustomNavigationControllerを作成し、UINavigationControllerDelegateを継承します。
そうすることで画面遷移(Push/Pop)が発生した際にnavigationController(_:animationControllerFor:from: to:)でアニメーションを指定できます。

class CustomNavigationController: UINavigationController, UINavigationControllerDelegate {
    override init(rootViewController: UIViewController) {
        super.init(rootViewController: rootViewController)
        delegate = self
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    func navigationController(_ navigationController: UINavigationController, animationControllerFor operation: UINavigationController.Operation, from fromVC: UIViewController, to toVC: UIViewController) -> UIViewControllerAnimatedTransitioning? {
        switch operation {
        case .push:
            let animator = Animator()
            animator.presenting = true
            return animator
        case .pop:
            let animator = Animator()
            animator.presenting = true
            return animator
        default:
            return nil
        }
    }
}

遷移元のNavigationControllerにCustomNavigationControllerを使用する

CustomNavigationControllerが使用できるように、遷移元であるFirstViewControllerをrootViewControllerに設定します。
今回はFirstViewControllerをSceneDelegateで呼び出しているのでそちらで設定します。

import UIKit

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?
    func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
        guard let scene = (scene as? UIWindowScene) else { return }
        let window = UIWindow(windowScene: scene)
        self.window = window
        window.makeKeyAndVisible()
        window.rootViewController = CustomNavigationController(rootViewController: FirstViewController())
    }
    // 省略
}

まとめ

なかなか最近の情報が見当たらなく動くところまで行くのに時間がかかってしまいました。
なにか参考になれば嬉しいです。

参考記事

4
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?