UIViewControllerの遷移は、UIViewControllerAnimatedTransitioningを使ってカスタマイズするのが、近頃では当たり前になってきています。
その中でもUIViewControllerでpresentViewController()
やdismissViewController()
とする場合と、UINavigationControllerでpushViewController()
やpopViewController()
をする場合で、使い方が多少異なります。その異なる部分には内部的にも大きな違いがあり、注意しなければいけない点がいくつかあります。
ここでは違いと注意点、普段見落としがちなプチ情報を書いていこうと思います。遷移周りのアニメーションがカクついたり、メモリが増え続けたりする場合の原因だったりすることも多々あるのでご覧頂けると幸いです。
UIViewControllerAnimatedTransitioning
の違い
UIViewController
の場合
UIViewControllerTransitioningDelegateの下記のデリゲートメソッドを使ってUIViewControllerAnimatedTransitioning
が実装されたクラスを返します。
optional func animationControllerForPresentedController(_ presented: UIViewController,
presentingController presenting: UIViewController,
sourceController source: UIViewController) -> UIViewControllerAnimatedTransitioning?
optional func animationControllerForDismissedController(_ dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning?
UIViewControllerContextTransitioning
のインスタンスからtransitionContext.containerView()
でcontainerViewを取得した際に、UIView
ではなくUITransitionView
が返されます。UIViewController
から別なUIViewController
にprensetViewController()
する度に、新しいUITransitionView
が生成されます。popViewController()
した際には、それ以前の階層に紐づくUITransitionView
の同じインスタンスが返されます。
例えば3回presentViewController()
して、3回dismissViewController()
をすると...
presentViewController() [1] <UITransitionView: 0x7f941ae38e90; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x7f941ae33c70>>
presentViewController() [2] <UITransitionView: 0x7f941ca93c80; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x7f941ca8c9e0>>
presentViewController() [3] <UITransitionView: 0x7f941ad17490; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x7f941ad23eb0>>
dismissViewController() [1] <UITransitionView: 0x7f941ad17490; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x7f941ad23eb0>>
dismissViewController() [2] <UITransitionView: 0x7f941ca93c80; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x7f941ca8c9e0>>
dismissViewController() [3] <UITransitionView: 0x7f941ae38e90; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x7f941ae33c70>>
1回目のpresentViewController()
と3回目のdismissViewController()
のUITransitionView
、2回目のpresentViewController()
と2回目のdismissViewController()
のUITransitionView
、3回目のpresentViewController()
と1回目のdismissViewController()
のUITransitionView
が同じアドレスなのがわかります。
UINavigationController
の場合
UINavigationControllerDelegateのデリゲートメソッドを使ってUIViewControllerAnimatedTransitioning
が実装されたクラスを返します。
optional func navigationController(_ navigationController: UINavigationController,
animationControllerForOperation operation: UINavigationControllerOperation,
fromViewController fromVC: UIViewController,
toViewController toVC: UIViewController) -> UIViewControllerAnimatedTransitioning?
UIViewControllerContextTransitioning
のインスタンスからtransitionContext.containerView()
でcontainerViewを取得した際に、UIView
ではなくUIViewControllerWrapperView
が返されます。UIViewController
から別なUIViewController
にpushViewController()
する際に、すべて同一のUIViewControllerWrapperView
が返されます。
例えば3回pushViewController()
して、3回popViewController()
をすると...
pushViewController() [1] <UIViewControllerWrapperView: 0x7f941ca8d7d0; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x7f941cab8a60>>
pushViewController() [2] <UIViewControllerWrapperView: 0x7f941ca8d7d0; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x7f941cab8a60>>
pushViewController() [3] <UIViewControllerWrapperView: 0x7f941ca8d7d0; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x7f941cab8a60>>
popViewController() [1] <UIViewControllerWrapperView: 0x7f941ca8d7d0; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x7f941cab8a60>>
popViewController() [2] <UIViewControllerWrapperView: 0x7f941ca8d7d0; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x7f941cab8a60>>
popViewController() [3] <UIViewControllerWrapperView: 0x7f941ca8d7d0; frame = (0 0; 375 667); autoresize = W+H; layer = <CALayer: 0x7f941cab8a60>>
すべてのUIViewControllerWrapperView
が同じアドレスなのがわかります。
UINavigationController
の遷移をカスタムする際に、toViewController.view
とfromViewController.view
の間に透過の黒のViewを挟むとします。アニメーションが完了した時点でその透過のViewをremoveFromSuperview()
しなければ、pushやpopをする度に透過のViewがaddSubview()
され続けてしまう状態になります。
遷移を繰り返していくと徐々にアニメーションは重くなっていったりしますが、ただの透過のViewだったりするとメモリも大きく増えたりなどはしないので非常に気づきにくいです。後から生成して追加したUIView
はアニメーションが完了した際にremoveFromSuperview()
を忘れないようにしましょう。
プチ情報
どちらの場合でも、containerView
はfromViewController.view
がaddSubview()
された状態で
返されるので、それぞれ
// `presentViewController()`や`pushViewController()`をする際は
containerView.addSubview(toViewContoller.view)
// `dismissViewController()`や`popViewController()`をする際は
containerView.insertSubview(toViewController.view, belowSubview: fromViewController.view)
と書くだけで、階層に合った場所にViewを配置することができます。
まとめ
-
UIViewControllerTransitioningDelegate
で返されるUIViewControllerAnimatedTransitioning
のcontainerViewはUITransitionView
-
UINavigationControllerDelegate
で返されるUIViewControllerAnimatedTransitioning
のcontainerViewはUIViewControllerWrapperView
- containerViewに後から生成した
UIView
をaddSubview()
した際は、アニメーション完了時に必ずremoveFromSuperview()
するようにする。 - containerView取得した時点で
fromViewController.view
がaddSubview()
されているので、再度addSubview()
する必要はない。 - Viewが画面に表示されるときは、
containerView.addSubview(toViewController.view)
- Viewが画面から消えるときは、
containerView.insertSubview(toViewController.view, belowSubview: fromViewController.view)