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

f:id:vasilyjp:20160517111453p:plainiOSアプリを開発しているエンジニアの庄司です。

今回は、iPhoneでのテザリング中や通話中に、ステータスバーの高さが変わることによる表示崩れの対応について紹介します。

TL;DR

  • iPhoneでテザリング中、UITabBarが画面からはみ出したりすることへの対応方法です。
  • RootViewControllerのviewにUITabBarControllerのviewをaddSubview:するときは、親viewの中に収まるようにAutoLayoutを設定します。
  • scrollView.contentInsetの調整にはtopLayoutGuide.topを使います。
  • サンプルアプリをGitHubにあげています。[GitHub]

何が起きていたか

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

f:id:vasilyjp:20160517103856p:plain

ViewController構成


UIViewController   // RootViewController
|- UITabBarController
   |- UINavigationController
   |  |- UITableViewController
   |- UINavigationController   
      |- UITableViewController
  • RootViewController内のviewDidLoadUITabBarControllerをコードで追加しています
  • UITabBarControllerをRootViewControllerとするXcode Projectでは、この問題は発生しません

Viewデバッガで見てみる

下記のような位置関係になっているため、UITabBarがはみ出して見えます。

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

f:id:vasilyjp:20160517103900p:plain

UITabBarがはみ出してしまう問題の対応

UITabBarControllerのviewをaddSubviewした後、AutoLayoutを設定してUITabBarControllerのviewがsuperviewの中に収まるようにします

Before

// RootViewController.swift

override func viewDidLoad() {
    super.viewDidLoad()

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

After

// 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)
}

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)
    }
}

修正結果

f:id:vasilyjp:20160517103853p:plain

f:id:vasilyjp:20160517103849p:plain

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

コンテンツ開始位置のズレ

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

ステータスバーのサイズ

UIApplicationstatusBarFrameが変わります。 テザリング中は見た目通り、高さが40で返ってきます。

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

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

しかし、上記のViewデバッガで見てわかるように、RootViewControllerが20だけ下がります。

下記のようなコードを書くと、通常時と比べてコンテンツ開始位置が20だけ下がって見えてしまうでしょう。

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

topLayoutGuide を使う

UIViewControllertopLayoutGuideはテザリング中でも値が変わりません。

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

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

所感

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

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

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