UIViewControllerの遷移をカスタムアニメーションにする上で注意すべき点

UIViewControllerの遷移は、[UIViewControllerAnimatedTransitioning](https://developer.apple.com/library/prerelease/tvos/documentation/UIKit/Reference/UIViewControllerAnimatedTransitioning_Protocol/index.html)を使ってカスタマイズするのが、近頃では当たり前になってきています。

その中でも[UIViewController](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UIViewController_Class/)で`presentViewController()`や`dismissViewController()`とする場合と、[UINavigationController](https://developer.apple.com/library/ios/documentation/UIKit/Reference/UINavigationController_Class/)で`pushViewController()`や`popViewController()`をする場合で、使い方が多少異なります。その異なる部分には内部的にも大きな違いがあり、注意しなければいけない点がいくつかあります。

ここでは違いと注意点、普段見落としがちなプチ情報を書いていこうと思います。遷移周りのアニメーションがカクついたり、メモリが増え続けたりする場合の原因だったりすることも多々あるのでご覧頂けると幸いです。

## `UIViewControllerAnimatedTransitioning`の違い

### `UIViewController`の場合

[UIViewControllerTransitioningDelegate](https://developer.apple.com/library/prerelease/ios/documentation/UIKit/Reference/UIViewControllerTransitioningDelegate_protocol/index.html)の下記のデリゲートメソッドを使って`UIViewControllerAnimatedTransitioning`が実装されたクラスを返します。

```swift
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](https://developer.apple.com/library/prerelease/ios/documentation/UIKit/Reference/UINavigationControllerDelegate_Protocol/index.html)のデリゲートメソッドを使って`UIViewControllerAnimatedTransitioning`が実装されたクラスを返します。

```swift
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()`された状態で
返されるので、それぞれ

```swift
// `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)`