こういうツイートをしたんだけど何も反応がなかったのでブログにも・・・・
お客様の中にNavigation BarのY位置がDynamic Islandを持つデバイスでhttps://t.co/IZANgdh7Fwと一致しなくなっていることにお困りの方はいらっしゃいませんか〜〜〜
— こーき@だん (@kouki_dan) 2022年10月17日
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