【iOS】readableContentGuide使用時の左右のマージンに対応する

2022/01/25に公開

iPad対応してますか?

とりあえずiPadで表示したときにコンテンツが全部表示される、というのが最低限の対応ですが、
できればiPadのときは、端末の画面に対して、横いっぱいに伸びるレイアウトを調整できると望ましいですよね。

アプリの中でiPad用の定数を持たせてもいいですが、実はreadableContentGuide というレイアウトガイドをAppleが用意してくれています。
これにViewの横幅をそろえると、「読みやすい範囲」に調整することができます。

https://techlife.cookpad.com/entry/2018/12/10/090000

詳しくは↑のクックパッド開発者ブログをご参照ください。
今回の記事は、この対応をした後に生じた問題とその対応について扱うので、readableContentGuide の使い方は省きます。

ただざっくり雰囲気だけ書くと、↓こんな感じでAutoLayoutの制約をつけていました。

NSLayoutConstraint.activate([
    childView.leadingAnchor.constraint(equalTo: parentView.readableContentGuide.leadingAnchor),
    childView.trailingAnchor.constraint(equalTo: parentView.readableContentGuide.trailingAnchor)
])

今後の説明の都合上、サンプルコードがあった方がいいと思ったので、もしコードの方がわかりいい方がいたら↓を見てください。

ViewController

import UIKit

class ViewController: UIViewController {
let childView: UIView = {
let view = UIView()
view.backgroundColor = .darkGray
return view
}()

override func viewDidLoad() {
    super.viewDidLoad()
    layoutChild()
}

private func layoutChild() {
    childView.translatesAutoresizingMaskIntoConstraints = false
    view.addSubview(childView)
    NSLayoutConstraint.activate([
        childView.topAnchor.constraint(equalTo: view.topAnchor, constant: 200),
        childView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
        childView.leadingAnchor.constraint(equalTo: view.readableContentGuide.leadingAnchor),
        childView.trailingAnchor.constraint(equalTo: view.readableContentGuide.trailingAnchor)
    ])
    
}

}

生じた問題

↑のviewControllerをiPhone/iPadでビルドすると、それぞれこのような幅になります。

よーし、いい感じですね。
しかしよく見ると、iPhoneで左右に微妙なマージンが生じています。
readableContentGuide に対して左右の制約をつけるのではなく、直接view.leadingAnchor/trailingAnchorにつけると、こうなります。

サンプルアプリだとコンテンツが乗ってないんであんまり気になりませんが、本番アプリでちゃんとデータが入った状態だと気になります。

このマージンは何

このマージンはlayoutMarginsです。
Appleのデザインシステム的にいい感じのマージンを入れてくれるものとなります。

readableContentGuide はlayoutMarginに影響を受けます。
Appleの表現だと、"The readable content guide never extends beyond the view’s layout margin guide.")

layoutMargin

layoutMarginは、iPhone端末だと左右それぞれ20pt、小型のiPhone端末だと16pt付与されます。
(20ptなのか16ptなのかの基準が未だに正確にわかってません)

いろいろ調べる中で、Human Interface Guidlinesにもちゃんと載ってることに気づきました。

https://developer.apple.com/design/human-interface-guidelines/ios/visual-design/adaptivity-and-layout/

layoutMarginの大きさについて、ちょっと正確な定義が見当たらなかったんですが、
いろいろ調べたりデバッグしたりした感じだと、ルートViewだと左右20pt(16pt)、その子Viewだと上下左右に8pt付与される雰囲気でした。
layoutMarginはgetだけかと思ったら、setもできるプロパティだったので、開発者側で更新可能でした。

また、ViewController のライフサイクルメソッドの中だと、viewWillLayoutSubviews の以降で更新された値が取得できるみたいです。
また変更タイミングで、layoutMarginsDidChange() が呼ばれるらしいので、これを使う手もあります。

最初に試みたけどダメだったやつ

というわけで、

override func viewDidLayoutSubviews() {
    view.layoutMargins = .zero
}

これをセットして終わりだ! と思ったんですよ。
結果は、あるViewでは上手くいって、あるViewでは全く効かない、という挙動になりました。

なぜ?

https://useyourloaf.com/blog/changing-root-view-layout-margins/

↑こちらのブログを読むと、

  • そもそもiOS 11までlayoutMarginを操作することは不可能だった
  • iOS 11で可能になった
  • けれどルートViewに対しては、System Minimum Layout Marginsという仕様があって、開発者の更新が効かない

と書いてありました。

System Minimum Layout Marginsをオフる

そんなSystem Minimum Layout Marginsですが、
UIViewController の方にviewRespectsSystemMinimumLayoutMargins というプロパティがあるので、
これをfalseにすることでオフにできます。

override func viewDidLoad() {
    super.viewDidLoad()
    viewRespectsSystemMinimumLayoutMargins = false
    view.layoutMargins = .zero
    layoutChild()
}

こんな感じでオフれます。

その他の対応

もっと安直に、左右の制約をマイナス20しとく、という方法でも対応できたりはします。
でもたぶん小型端末だとマージンが16ptになるので、4pt微妙にはみ出るみたいな問題は出ますかね……

ちなみにですが、Interface BuilderでreadableContentGuide に準拠する対応をしようとすると、

制約をつけるときに、SuperViewのmarginに対してつけることになる(詳しくは記事冒頭で紹介したクックパッドのブログを参照)んですが、
「Relative to margin」にチェックを入れた時点で、勝手にマイナス20(またはマイナス8。子Viewのときは恐らく8なんだと思われる)されます。

(おまけ)LayoutGuideとは

今回、readableContentGuide をはじめて使ったんですが、そもそもLayoutGuide自体使うのがはじめてでした。
LayoutGuideの存在はなんとなく知ってたんですが、AutoLayoutをガッツリ使ったことが今の会社入るまでなかったので、ずっと謎の存在でした。

たとえばSafeAreaに沿ったレイアウトをアプリ内で実現するために、透明なUIViewがあったら便利ですよね?
(昔Unityでアプリ開発したときにつくったことがあります)
コンテンツを配置する際の基準となるframe。
それがLayoutGuideです。

iOS 9.0から登場したようです。
AutoLayoutの登場がiOS 6.0なので、3年ぐらい開発者が透明なUIView(Appleのドキュメントだと"placeholder view"という表現になってます)使ってた時期があったんですかね。

LayoutGuideは開発者が独自で定義することもできますが、Appleが便利なものを用意してくれていて、今回使ったreadableContentGuide もその一つです。

  • safeAreaLayoutGuide
  • layoutMarginsGuide
  • readableContentGuide
  • keyboardLayoutGuide

などがあります。
safeAreaLayoutGuide がたぶん一番有名ですかね。
UIViewのプロパティなので、だいたいのViewがこのLayoutGuideを持っています。

AutoLayout使ってないアプリだと使えない概念だとずっと思ってたんですが、layoutFrame ってプロパティがあって、
frame情報も持っているので、一応frameベースな処理もできるみたいです。

Discussion