はじめに
UIViewControllerAnimatedTransitioningを使ってカスタム画面遷移アニメーションを実装する方法になります。
検索した情報が古かったり、よくわからないところがあったので、現時点での実装方法とつまずいたところをまとめました。
対象バージョン
- Xcode ver.11.6
- iOS ver 13.5.1
Present/DismissとPush/Popで実装方法が変わる
ViewController間で直接画面遷移する方法(Present/Dismiss)と、NavigationControllerを使用した画面遷移(Push/Pop)では実装方法が変わります。
Present/Dismissで画面遷移する場合
画面遷移イメージ
UIViewControllerAnimatedTransitioningを使って画面遷移アニメーション(Present/Dismiss)を上下に遷移するようにカスタマイズしてみた
— TatsunoriMorita@フリーエンジニア (@king_of_morita) July 30, 2020
# swift pic.twitter.com/VxJ6ScvIsv
概要と注意点
最初に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)で画面遷移する場合
画面遷移イメージ
UIViewControllerAnimatedTransitioningを使って画面遷移アニメーション(Push/Pop)を上下に遷移するようにカスタマイズしてみた#Swift pic.twitter.com/LmitkZNDBj
— TatsunoriMorita@フリーエンジニア (@king_of_morita) July 30, 2020
概要と注意点
画面遷移のアニメーションを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())
}
// 省略
}
まとめ
なかなか最近の情報が見当たらなく動くところまで行くのに時間がかかってしまいました。
なにか参考になれば嬉しいです。
参考記事