iPhoneでテザリング中にレイアウトが崩れる問題の解決方法

# TL;DR
- iPhoneでテザリング中、`UITabBar`が画面からはみ出したりすることへの対応方法です。
- RootViewControllerのviewに`UITabBarController`のviewを`addSubview:`するときは、親viewの中に収まるようにAutoLayoutを設定します。
- `scrollView.contentInset`の調整には`topLayoutGuide.top`を使います。
- [サンプルアプリ](https://github.com/WorldDownTown/TetheringStatusBarSample)

# 何が起きていたか
- テザリング中や通話中などにレイアウトが崩れる
- `UITabBar`が20pts下がって、画面からはみ出しまう

<img src="https://qiita-image-store.s3.amazonaws.com/0/11454/dc7c7241-efd6-1040-659d-3b37ff5059de.png" width="375px" height="667px" alt="before.png" />

## ViewController構成 
```
UIViewController   // RootViewController
|- UITabBarController
   |- UINavigationController
   |  |- UITableViewController
   |- UINavigationController   
      |- UITableViewController
```

- RootViewController内の`viewDidLoad`で`UITabBarController`をコードで追加しています
- `UITabBarController`をRootViewControllerとするXcode Projectでは、この問題は発生しません

## Viewデバッガで見てみる

下記のような位置関係になっているため、 `UITabBar` が切れて見えます。


```
// UIWindowからの相対的なframe
UIWindow: (0, 0, 375, 667)
RootViewController: (0, 20, 375, 647)
UITabBarController: (0, 40, 375, 647)
```


<img src="https://qiita-image-store.s3.amazonaws.com/0/11454/e626b6e8-8c63-85eb-4af1-f890460f93fd.png" alt="before_view_debugger.png" width="615px" height="650px" />

# `UITabBar`がはみ出してしまう問題の対応
## `UITabBarController`のviewを`addSubview`した後、AutoLayoutを設定して`UITabBarController`のviewがsuperviewの中に収まるようにします

### Before

```swift
// RootViewController.swift

override func viewDidLoad() {
    super.viewDidLoad()

    let tc: UITabBarController = createTabBarController()
    addChildViewController(tc)
    view.addSubview(tc.view)
    tc.didMoveToParentViewController(self)
}
```


### After

```swift
// RootViewController.swift

override func viewDidLoad() {
    super.viewDidLoad()

    let tc: UITabBarController = createTabBarController()
    addChildViewController(tc)
    view.addSubview(tc.view)

    // view の中に収まるように、tabBarController.view に constraintを設定
    view.addFittingConstraintsFor(tc.view)

    tabBarController.didMoveToParentViewController(self)
}
```

```swift
extension UIView {

    /**
     childViewが同じサイズに収まるように、constraintsを設定する

     - parameter childView: 子View
     */
    func addFittingConstraintsFor(childView: UIView) {
        let constraints = [.Top, .Leading, .Bottom, .Trailing].map {
            NSLayoutConstraint(
                item: childView,
                attribute: $0,
                relatedBy: .Equal,
                toItem: self,
                attribute: $0,
                multiplier: 1.0,
                constant: 0.0)
        }
        childView.translatesAutoresizingMaskIntoConstraints = false
        addConstraints(constraints)
    }
}
```

 ## 修正結果

<img src="https://qiita-image-store.s3.amazonaws.com/0/11454/af9d3bdf-328a-1c05-7b8e-449c71d621a0.png" width="375px" height="667px" alt="after.png" />

<img src="https://qiita-image-store.s3.amazonaws.com/0/11454/59990814-aa7f-01ab-eacc-ae360a36870a.png" alt="after_view_debugger.png" width="687px" height="678px" />

`UITabBarController`のviewはRootViewControllerのviewと同じ位置、サイズになりました。

# コンテンツ開始位置のズレ
このサンプルでは特に問題はありませんが、テザリング中に`UITableView`のコンテンツ開始位置がズレる現象もよく見かけます。

## ステータスバーのサイズ
`UIApplication`の`statusBarFrame`が変わります。
テザリング中は見た目通り、高さが40で返ってきます。

```swift
// 通常時
statusBarFrame: (0, 0, 375, 20)

// テザリング中
statusBarFrame: (0, 0, 375, 40)
```

しかし、上記のViewデバッガで見てわかるように、RootViewControllerが`20`だけ下がります。<br /> 下記のようなコードを書くと、通常時と比べてコンテンツ開始位置が`20`だけ下がって見えてしまうでしょう。

```swift
// 通常時: 20 / テザリング時: 40
scrollView.contentInset.top = statusBarFrame.height
```

## topLayoutGuide を使う
`UIViewController`の`topLayoutGuide`はテザリング中でも値が変わりません。

`topLayotGuide.top`はステータスバーやナビゲーションバーの高さも考慮した値を返します。
ランドスケープ時にステータスバーが消えた場合は、ナビゲーションバーの高さだけ返してくれます。

```swift
// 通常時: 20 / テザリング時: 20
scrollView.contentInset.top = topLayoutGuide.top
```


# 所感
特に情報が見つからなかったので、独自の解決策です。
もっと良い方法や、Appleの公式なドキュメントがあれば教えて下さい。

`UITabBarController`をRootViewControllerとしてStoryboardで実装した場合は、今回の問題は発生しませんでした。

国内外・有名無名問わず、いろんなアプリで同じようなレイアウトの崩れがいくつか見られます。
開発者が意識することなく、うまいことiOS側で管理してほしいものです。