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)