Skip to content

Instantly share code, notes, and snippets.

@shinyaohira
Last active November 23, 2019 14:50
Show Gist options
  • Save shinyaohira/7057375 to your computer and use it in GitHub Desktop.
Save shinyaohira/7057375 to your computer and use it in GitHub Desktop.
Auto Layout Guide

例えば、buttonの左辺をcontainer viewから20ポイント離す場合

button.left = container.left + 20

と表すことができる。

一般的には、y = m*x + b で表すことができる。ここで

  • y と x はviewの属性
  • m と b は浮動小数点数

ここで属性は、left、right、top、bottom、leading、trailing、width、height、centerX、centerY、baselineのいずれか。

leadingとtrailingは、英語のように左から右へ書く言語の場合はそれぞれleftとrightと同じだが、ヘブライ語やアラビア語のように右から左へ書く言語の場合は、leadingとtrailingはそれぞれrightとleftと同じ意味になる。 制約の定義には、leadingおよびtrailingを使うのが原則。ただし、split viewにおけるmaster painとdetail painのように)言語によらず左右が同じであるべき制約の場合を除く。

constraintのプロパティには、constant、relation、priorityなどがある。

  • constant

    CGFloatの定数値。大きさやオフセット。

  • relation

    NSLayoutRelationLessThanOrEqual、NSLayoutRelationEqual、NSLayoutRelationGreaterThanOrEqual。 viewの幅をwidth >= 20としたり、text viewとsuperviewの関係をtextview.leading >= superview.leading + 20とするなど。

  • priority

    優先度。priorityの高いconstraintを満たした後で、priorityの低いconstraintを考慮するようになっている。デフォルトのpriorityは、UILayoutPriorityRequiredで最も高いpriorityとなっており、constraintを完全に満たさなければならない。これよりも低いpriorityについては、完全ではないにしても、できるだけ満たすように調整される。

constraintが複数あればすべて同時に適用され、一方が一報を打ち消すことはない。例えば、あるconstraintに対して、同じ種類の別のconstraintをさらに適用しても、元のconstraintが無効になるわけではない。必要なら明示的に削除する必要がある。

view階層をまたがるconstraintも可能。たとえばOS X用のメールアプリの場合、ツールバーの削除ボタンはメッセージテーブルと揃うように配置されている。

ただし、view階層をまたがるconstraintを設定できない場合がある。例えば、view階層内にsubviewのフレームをlayoutSubviewsで設定するviewがある場合や、scroll viewのように座標変換機能があるviewがある場合など。

buttonなど末端レベルのviewは、通常何らかのコンテンツを含んでおり、コードで位置を指定するよりも、そのサイズでレイアウトする方が良い。このサイズをIntrinsic Content Sizeと呼ぶ。これは、Editor > Size To Fit Contentで設定することができる。

compression resistance priorityは、UIコンポーネントが推奨サイズより小さくなる場合の、小さくなりにくさを表す。例えば、buttonがそのテキストよりも小さくなり切れるのを防ぐ。 content hugging priorityは、UIコンポーネントが推奨サイズより大きくなる場合の、大きくなりにくさを表す。例えば、buttonが[    Click Me    ]のようになるのを防ぐ。 buttonの場合、content hugging priorityはcompression resistance priorityよりも低い。

例えば、実行時にビューを追加、削除する場合、constraintもプログラムで実行時に追加して、大きさや向きの変化に適切に応じられるようにしなければならない。

constraintはNSLayoutConstraintのインスタンスで表す。通常constraintの生成には、constraintsWithVisualFormat:options:metrics:views:メソッドを使う。

第1引数にはvisual format stringを指定する。レイアウト上の制約を視覚的に表した文字列。viewは角括弧で囲み、view間の接続はハイフンで表し、2つのハイフンの間の数字はview間の間隔のポイント数を指定する。

two buttons

visual format string:

[button1]-12-[button2]

ハイフンが1つの場合、標準の間隔を表す。

[button1]-[button2]

view名はviews dictionaryから取得する。view名がキーで、値が対応するviewオブジェクト。dictionaryの生成には、NSDictionaryOfVariableBindingsを使うと良い。

NSDictionary *viewsDictionary = NSDictionaryOfVariableBindings(_button1, _button2);
NSArray *constraints = [NSLayoutConstraint constraintsWithVisualFormat:@"[_button1]-[_button2]" options:0 metrics:nil views:viewsDictionary];

visual format stringでは表現できないconstraintが存在する。例えば、アスペクト比を固定するconstraintは表現できない。

このような制約を生成するためには、constraintWithItem:attribute:relatedBy:toItem:attribute:multiplier:constant:メソッ ドを使用する。

[NSLayoutConstraint constraintWithItem:self.button1 attribute:NSLayoutAttributeRight relatedBy:NSLayoutRelationEqual toItem:self.button2 attribute:NSLayoutAttributeLeft multiplier:1.0 constant:-12.0];

constraintを有効にするには、viewに登録する必要がある。このviewは、constraintを含むviewの祖先である必要があり、通常はconstraintに関係するviewの直近のものを選ぶ。ただし、viewは自分自身の0世代前の祖先と考えることができることに注意。constraintは、このviewの座標系に基づいて解釈される。

[zoomableView1]-80-[zoomableView2]というconstraintを、zoomableView1とzoomableView2の共通の祖先であるcontainer viewに組み込む。

この80はcontainerの座標系における間隔なので、zoomableView1やzoomableView2のboundsが変わっても、viewの間隔は変わらない。

containerのboundsが変われば、間隔も変わる。

UIViewには、constraintを追加するaddConstraint:メソッド、constraintを削除するremoveConstraint:メソッド、設定されているconstraintを調べるconstraintsメソッドなどがある。

Content Size Is Undefined

いくつかのcontainer viewは、自分自身のサイズを決めるのに実行時のコンテンツのサイズに依存しているものがある。つまり、そのサイズは設計時にはわからない。この問題を解決するには、placeholderのminimum widthを設定するなど、placeholderのconstraintを設定する。このplaceholderはビルド時に削除される。

placeholder constraint

The Size of Custom View Content Is Unknown

標準のviewと異なり、custom viewはintrinsic content sizeが定義されていない。そのためInterface Builderは、costom viewがどのような大きさになるかわからない。この問題を解決するために、placeholderのintrinsic content sizeを設定する。

  1. Size Inspectorを開く
  2. Intrinsic SizeメニューからPlaceholderを選択
  3. 適切な幅と高さの値を設定

placeholder intrinsic content size

レイアウト上の問題のデバッグは、2段階に分けて行う。

  1. 誤った位置に配置されるビューを認識し、これをもとに不適切なconstraintを見つける
  2. 不適切なconstraintを元に、不適切なコードを見つける

Auto Layoutの問題のデバッグ

  1. 正しくないframeを持つviewを特定する

不明確な場合は、UIViewのrecursiveDescriptionメソッドを使用すると良い。view階層をテキストで表現してくれる。

  1. 可能であれば、InstrumentsのAuto Layoutテンプレート上で実行し、問題を再現する。

  2. 問題のconstraintを見つける

特定のviewに影響しているconstraintを取得するには、constraintsAffectingLayoutForAxis:メソッドを使用する。 constraintはデバッガで調べることができる。デバッガは、constraintをvisual formatで表示してくれる。

<NSLayoutConstraint: 0x400bef8e0 H:[refreshSegmentedControl]-(8)-[selectViewSegmentedControl] (Names: refreshSegmentedControl:0x40048a600, selectViewSegmentedControl:0x400487cc0 ) >
<NSLayoutConstraint: 0x400cbf1c0 H:[NSSegmentedControl:0x40048a600]-(8)-[NSSegmentedControl:0x400487cc0]>
  1. この時点で、どのconstraintに問題があるか明らかでない場合は、visualizeConstraints:メソッドでconstraintをwindowに渡し、画面に表示する。

constraintをクリックすると、その内容がコンソールに表示される。このようにして、どのconstraintに問題があり、何が誤っているのかを特定していく。

  1. 問題のあるコードを見つける

constraintのポインタを検索するために、Instrumentsを使用する。constraintの生成、修正、ウインドウへの組み込みなどのイベントが 表示される。それぞれについて、実際に発生した箇所のバックトレースも参照できる。

constraintをうまく組み合わせれば、従来のようにコードで管理するのは難しい配置も実現できる。例えば、向きや大きさの変化に応じてviewが等間隔になるよう並べ直す、scroll view内に、その中身の大きさを調整する要素を置く、あるいはスクロールしても常に同じ位置にとどまる要素を設ける、などが考えられる。

Auto Layoutを利用してアプリを開発する場合、scroll viewの取り扱いにはいくつかの課題がある。スクロールするcontentの大きさを適切に設定しなければ、スクロールして全体を見るようにはできない。また、scroll viewの一番上に、例えばマップの場合にスケールや凡例を固定する、なども難しい。

Controlling Scroll View Content Size

scroll viewのcontentの大きさは、子孫要素のconstraintによって決まる。

  1. scroll viewを作成する。
  2. その内部にUI要素を配置する。
  3. scroll viewのcontentの幅と高さが完全に決まるよう、constraintを作成する。

scroll vew内のsubviewすべてにconstraintが必要。例えば、intrinsic content sizeを持たいないviewにconstraintを定義する場合、leading edge constraintだけでなく、trailing edge、width、height constraintも必要となる。

Creating Anchored Views Inside a Scroll View

scroll view内に、スクロールしても動かない領域を作りたいことがある。これは別のcontainer viewを用意することで実現できる。

  1. scroll viewを保持するcontainer viewを作成する。
  2. scroll viewを作成し、container viewの中に配置し、全ての端からの距離を0ポイントにする。
  3. subviewを作成し、scroll viewの中に配置する。
  4. subviewからcontainer viewに対してconstraintを作成する。

subviewの位置は、container viewを基準として決まるため、スクロールしても動かない。

デバイスの向きによらず、いくつかのviewを等間隔で並べるには、viewの間に間隔調整用のspacer viewを作る。

  1. 表示するviewを作成する。
  2. 表示するviewの数より1つ多い数のspacer viewを作成する。
  3. spacer viewを端に置き、表示するviewと交互に並べる。

spacer1 | view1 | spacer2 | view2 | spacer3

  1. spacer viewの長さが同じになるようconstraintを作成する。
  2. 先頭のspacer viewからcontainer viewへのleading constraintを作成する。
  3. 末尾のspacer viewからcontainer viewへのtrailing constraintを作成する。
  4. spacer viewと表示するviewの間にconstraintを作成する。

Auto Layoutによって起こる変化のアニメーションを完全にコントロールするには、constraintの変化をプログラム的に起こさなければならない。

[containerView layoutIfNeeded]; // Ensures that all pending layout operations have been completed
[UIView animateWithDuration:1.0 animations:^{
     // Make all constraint changes here
     [containerView layoutIfNeeded]; // Forces the layout of the subtree animation block and then captures all of the frame changes
}];

buttonのような末端レベルのviewは、通常自身の大きさがどのようであるべきか、それらを配置する側のコードよりも良く知っている。これは、intrinsicContentSizeメソッドを通してレイアウトシステムに伝えられる。

単一行のtext fieldを例にすると。レイアウトシステムは、表示されるテキストの中身には関与しない。知らなければならないのは、viewの中に何かが存在し、それを表示するためには、ある領域を占めること。レイアウトシステムは、intrinsicContentSizeを呼び出してこの情報を取得し、viewの中身を縮小したり切りとられるべきではない、viewは中身ををできるだけ小さく内包する、というconstraintを設定する。

intrinsicContentSizeメソッドで幅と高さを具体的な数値で返すか、固有のサイズがないことを表すUIViewNoInstrinsicMetricを返す。

  • buttonのintrinsic sizeは、表示するラベルや画像によって決まる。buttonのintrinsicContentSizeメソッドは、ラベルや画像を完全に表示できる幅と高さを返さなければならない。
  • 水平方向のsliderには、固有の高さはあるが固有の幅はない。UISliderオブジェクトは、{UIViewNoInstrinsicMetric, <slider height>}というCGSizeを返す。sliderを使う側が幅を決めるためのconstraintを与える必要がある。

viewのpropertyが変化し、それによりintrinsic sizeが変化した場合、invalidateIntrinsicContentSizeを呼び出して、レイアウトシステムにその旨を通知し、レイアウトを 計算し直す機会を与えなければならない。例えば、text fieldは、その文字列の値が変わったら、invalidateIntrinsicContentSizeを呼び出さなければならない。

レイアウトに関しては、コントロールのframeよりも、表示されている大きさの方が重要。よって影や溝線のような飾りは、レイアウトを決める際には通常無視される。

レイアウトをframeではなくコンテンツの表示をベースに行うために、viewはalignment rectangleを提供する。

controlをbaselineに揃えたい場合がある。これはconstraintとしてNSLayoutAttributeBaselineを指定することにより実現できる。

Auto Layout対応のviewは、そうでないviewが含まれるwindowと共存できる。よって、既存のプロジェクトを徐々にAuto Layoutに対応したものに更新していくことができる。translatesAutoresizingMaskIntoConstraintsプロパティを使って、viewをひとつずつAuto Layoutを用いる形に書き替えていけばよい。

このプロパティがYES(デフォルト)の場合、viewのautoresizing maskはconstraintに変換される。例えばviewが下記のように設定されている場合、constraintが|-20-[button]-20-|V:|-20-[button(20)] としてviewのsuperviewに追加される。

Auto Layoutに対応済みのviewは、多くの場合translatesAutoresizingMaskIntoConstraintsをNOとすることになる。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment