[Objective-C] コードからAutoLayoutを制御する方法

  • 116
    いいね
  • 3
    コメント
この記事は最終更新日から1年以上が経過しています。

AutoLayoutは「Constraint(制約)」を課すことで、レイアウトを維持する方法です。
例えば、親Viewの端から10px離す、みたいなことを定義していきます。
こちらの記事(iOSのAutoLayoutの基本的な記述方法とエラーメッセージの種類)を参考にさせて頂きました。

Storyboardを使っている場合はGUIで設定するのが基本だと思いますが、コードから生成するViewがまったくない、ということもあまりないでしょう。
その際にも、複雑なレイアウトのための計算をしないで済むように、Constraintを適切に定義することで画面サイズによらないレイアウトが可能です。

iPhone6から画面サイズが変わると言われているし、iPadに至っては画面分割も出てくるかもしれません。
その際に、色々な条件を考慮して数値的に計算するのはだいぶきびしくなっていくと思います。
今後はやはりAutoLayout必須になっていくでしょう。

サンプルコード

以下に、画面の上下左右にぴったりフィットする制約のサンプルコードを載せます。

※ちなみに、constantに値を入れるとその分、距離が離れるようになります。
また、右端などから離す場合はマイナスの値を入れる必要があるようです。

NSLayoutConstraint *layoutTop =
[NSLayoutConstraint constraintWithItem:aView
                             attribute:NSLayoutAttributeTop
                             relatedBy:NSLayoutRelationEqual
                                toItem:self.view
                             attribute:NSLayoutAttributeTop
                            multiplier:1.0
                              constant:0.0];

// --- 中略 ---

NSArray *layoutConstraints = @[layoutTop,
                               layoutBottom,
                               layoutLeft,
                               layoutRight];

// AutoResizingMaskでのレイアウトをオフにする
[aView setTranslatesAutoresizingMaskIntoConstraints:NO];

// 生成したレイアウトを配列でまとめて設定する
[self.view addConstraints:layoutConstraints];

NSLayoutConstraintクラスがConstraintを表現するクラスです。
Constraint生成時に渡している2つのビューは、それぞれがどういう関係になるかを示すものです。
(記事冒頭の、「親Viewの端から10pxうんぬん」の話)

constraintWithItemtoItemに渡すビューはどちらが先でも、相互の関係の定義なので問題ないようです。

ちなみに参考にさせてもらった記事から注意点を引用すると

  • 指定するViewは先にaddSubviewで追加されている必要がある。
  • 2つのattribute引数で指定できる組み合わせは決まっている。(例:NSLayoutAttributeTopとNSLayoutAttributeBottomはOKだが、NSLayoutAttributeTopとNSLayoutAttributeLeftはNG等)
  • 第一引数とtoItem引数で指定できるViewの順番は逆でも可能だが、統一しておいた方がわかりやすい。
  • attribute引数で単純に横幅(NSLayoutAttributeWidth)や高さ(NSLayoutAttributeHeight)を指定する場合は、対になるViewはnilにする。

とのこと。

iOS6から使えるのでiOS5対応がいらなければ積極的に使っていきたいですね。

[追記]

constraintsWithVisualFormat:options:metrics:views メソッドを使ってレイアウトする

コメントで、「Visual Format Language」によるレイアウトについて指摘頂きました。
「Language」と名がつく通り、NSStringでテキストを渡して柔軟にレイアウト処理を書ける簡易言語のようです。

なにはともあれコードを見てもらうとなにかが分かると思います。

サンプルコード

// Create constraints
self.childView.translatesAutoresizingMaskIntoConstraints = NO;
NSNumber *vPadding = @0;
NSNumber *hPadding = @0;

UIView *childView   = self.childView;
NSDictionary *views = NSDictionaryOfVariableBindings(childView);
NSDictionary *metricsDictionary = NSDictionaryOfVariableBindings(vPadding, hPadding);
NSArray *vConstraints = [NSLayoutConstraint constraintsWithVisualFormat:@"V:|-vPadding-[childView]-vPadding-|"
                                                                options:0
                                                                metrics:metricsDictionary
                                                                  views:views];
[self.view addConstraints:vConstraints];

NSArray *hConstraints = [NSLayoutConstraint constraintsWithVisualFormat:@"H:|-hPadding-[childView]-hPadding-|"
                                                                options:0
                                                                metrics:metricsDictionary
                                                                  views:views];
[self.view addConstraints:hConstraints];
[self.view layoutIfNeeded];

NSDictionaryOfVariableBindingsマクロで辞書を生成

どうも実装を見ると、可変引数で受け取った変数名をキーに、その参照を値にしてくれるマクロのようです。
サンプルコードではビューの辞書と、metricsの辞書のふたつを生成しています。

※ ちなみに、self.childViewchildViewに入れていますが、ドットが含まれるとエラーになってしまったので、いったんローカル変数に入れてから辞書を生成しています。

NSStringで柔軟にレイアウト

formatを見てもらうとなんとなく分かるかと思います。(@"V:|-vPadding-[childView]-vPadding-|"
レイアウト対象となるビューは[childView]として、[]で囲みます。
また、vPaddinghPaddingは、metricsDictionaryで定義されているキーです。
今回の例で言えばどちらも0が設定されます。

また、テキストの頭にあるVHは「vertical」と「horizontal」の頭文字です。
つまり左右方向なのか上下方向なのか、ということですね。

そして|が隣接する要素や画面端などの「端」を表します。

辞書をうまく使えばメンテナンス性もあがり、結構柔軟にレイアウトを定義できそうな感じですね。

ハマりポイント

AutoLayoutを実装していると以下のようなエラーメッセージが出てクラッシュする場合があります。

The view hierarchy is not prepared for the constraint ...

一番ありがちな問題としては、Constraintを設定しようとしているビューたちがまだどのビューにもaddSubviewされていない場合に起こります。
その場合はまず最初にビューを必ずなにかしらのビューに属するようにしてください。

設定したConstraintsは親に設定する

自分がハマったのはこちらでした。
なんとなく、設定したいビューに対してaddConstraintしたくなりますが、すべき対象は、AutoLayoutしたい要素を内包している一番TOPにある親ビューです。

例えば、parent, A, Bという3つのビューがあり、AとBに対してAutoLayoutを指定した場合でも、addConstraintを実行するのはparentに対してになります。
そうしないと実行時にクラッシュします。

translatesAutoresizingMaskIntoConstraintsNO

UIViewにはtranslatesAutoresizingMaskIntoConstraintsというプロパティがあります。
AutoLayoutで制御したいビューの上記プロパティをNOにしないと、クラッシュこそしないもののコンソールにエラーが出力されるので注意が必要です。