Lento con forza

大学生気分のIT系エンジニアが色々書いてく何か。ブログ名決めました。

Dynamic IslandがあるiPhone 14 ProなどでナビゲーションバーのminYとsafeAreaInsets.topが一致しなくなっている

こういうツイートをしたんだけど何も反応がなかったのでブログにも・・・・

iPhone 14 ProにおけるSafe areaとナビゲーションバーの扱い

iPhone 14 ProでDynamic Islandが導入されましたが、Safe areaの扱いも色々と変わっていそうで、以下のスクショを見てもらえるとわかります。

view.safeAreaInsets.topと ---------- の下の計算結果は、これまでのデバイスでは一致していました。つまり、ナビゲーションコントローラーなしのSafe areaにナビゲーションバーの高さを足した部分からコンテンツ領域が始まることになります。言い換えると、ナビゲーションバーの開始位置はSafe areaの枠の一番上からでした。

iPhone 13 Proでも

iPhone 8でも

iPadでも

これまでのデバイスでは、値が一致しています。

Dynamic Islandを持つデバイスでは、その定説が崩れ、ナビゲーションバーがSafe areaに少し(iPhone 14 Proで5.333...)かぶっているのです!

Dynamic Islandのために、Safe areaを少し広く取ることになったので、その分を稼ぐためにナビゲーションバー自体を少しSafe areaにかぶせることで確保した、という想像をしていますが、真意はわかりません。

確認に使ったコードです!

class ViewController: UIViewController {
    @IBOutlet weak var label: UILabel!

    override func viewDidLoad() {
        super.viewDidLoad()
        let appearance = UINavigationBarAppearance()
        appearance.configureWithDefaultBackground()
        navigationItem.scrollEdgeAppearance = appearance
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        label.text = """
        view.safeAreaInsets.top: \(view.safeAreaInsets.top)
        navigationController!.view.safeAreaInsets.top: \(navigationController!.view.safeAreaInsets.top)
        navigationBar.frame.minY: \(navigationController!.navigationBar.frame.minY)
        navigationBar.frame.height: \(navigationController!.navigationBar.frame.height)
        ----------
        safeAreaInsets.top + navigationBar.frame.height:
            \(navigationController!.view.safeAreaInsets.top + navigationController!.navigationBar.frame.height)
        """
    }
}

どういうときに困る?

さて、この変更はどういうときに困るのでしょうか。例えば、ナビゲーションバーの背景を透明にして、ナビゲーションバーとコンテンツを一体的に見せているようなアプリでは、Safe areaに対してinsetを置く場合があります。そのようなケースでは、見え感が違ってくるでしょう。以下のスクリーンショットを見てもらうとわかるように、iPhone 14 Proでは上に詰まったように見えてしまいます。

確認に使ったコードは以下です。

class ViewController: UIViewController {
    private var topConstraint: NSLayoutConstraint!

    override func viewDidLoad() {
        super.viewDidLoad()
        let appearance = UINavigationBarAppearance()
        appearance.configureWithTransparentBackground()
        navigationItem.standardAppearance = appearance
        navigationItem.scrollEdgeAppearance = appearance

        navigationItem.leftBarButtonItem = UIBarButtonItem(systemItem: .close)
        navigationItem.rightBarButtonItem = UIBarButtonItem(systemItem: .action)

        let content = UIView()
        content.backgroundColor = .systemGray4
        content.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(content)
        topConstraint = content.topAnchor.constraint(equalTo: view.topAnchor)
        NSLayoutConstraint.activate([
            topConstraint,
            content.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            content.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            content.heightAnchor.constraint(equalToConstant: 200),
        ])

    }

    override func viewSafeAreaInsetsDidChange() {
        super.viewSafeAreaInsetsDidChange()
        topConstraint.constant = navigationController!.view.safeAreaInsets.top
    }
}

これを解消するために、ナビゲーションバーのボタンの上部にマージンを加える方法と、コンテンツ領域のインセットを減らす方法が思いつきます。前者のナビゲーションバーのマージン調整は、既存の挙動を壊してしまうことになりかねないので、コンテンツ領域のインセットをナビゲーションバーに合わせて減らす方が良いのではないかと考えました。調整した結果が以下のスクリーンショットです。

これで見え感も揃って良い感じではないでしょうか。viewDidLayoutSubviews()でナビゲーションの開始位置を計算して算出します。navigationController!.navigationBar.frame.minYでも良いかな、と思ったんですが、ビューの内部構造に依存するのは怖いので、これくらいの出し方が良いのではないかと思います。

class ViewController: UIViewController {
    // 省略...
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()

        topConstraint.constant = view.safeAreaInsets.top - navigationController!.navigationBar.frame.height