AutoLayoutをコードで書く時にハマったこと

#はじめに
今年からiPhoneアプリ開発の勉強を始めた者です。
最近AutoLayoutを勉強し、画面回転してもなんか自動で位置合わせしてくれる…何これめっちゃ便利やん…と感動していたのですが、AutoLayoutをInterfaceBuilderではなくコード上で設定しようとしたら、見事にハマりました。

自分はこれでかなりの時間をとられたので、備忘録として残しておきます。
#コード上で制約を追加する
InterfaceBuilder上では、サイズや位置などの制約を比較的少ない操作で追加することができますが、動的な変更などのためにコード上で制約を追加しようとすると、非常に長いコードを書くことになります。

例えば、self.viewの8ピクセル内側にsubviewを広げようとすると、上下左右端に1つずつ、合計4つの制約が必要になり、次のようなコードを書くことになります。

~~~objc
    [self.view addConstraint:[NSLayoutConstraint constraintWithItem:subview attribute:NSLayoutAttributeTop relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTop multiplier:1.0 constant:8.0]];
    [self.view addConstraint:[NSLayoutConstraint constraintWithItem:subview attribute:NSLayoutAttributeBottom relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeBottom multiplier:1.0 constant:-8.0]];
    [self.view addConstraint:[NSLayoutConstraint constraintWithItem:subview attribute:NSLayoutAttributeLeading relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeLeading multiplier:1.0 constant:8.0]];
    [self.view addConstraint:[NSLayoutConstraint constraintWithItem:subview attribute:NSLayoutAttributeTrailing relatedBy:NSLayoutRelationEqual toItem:self.view attribute:NSLayoutAttributeTrailing multiplier:1.0 constant:-8.0]];
~~~
流石にこれでは長すぎますよね。VisualFormatを使うことで、もう少し短く書くことができます。

~~~objc
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-8-[sub]-8-|" options:0 metrics:nil views:@{@"sub" : subview}]];
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-8-[sub]-8-|" options:0 metrics:nil views:@{@"sub" : subview}]];
~~~
最初のコードと比べるとずいぶんシンプルになったのが分かると思います。

#制約の競合
これを使って動的にUIViewを追加し、そのUIViewに対して制約を設定しようとすると、上手くいかない場合があります。
例えば次のコードです。

~~~objc
    UIView *view = [[UIView alloc] init];
    view.backgroundColor = [UIColor redColor];
    [self.view addSubview:view];
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"H:|-8-[view]-8-|" options:0 metrics:nil views:@{@"view" : view}]];
    [self.view addConstraints:[NSLayoutConstraint constraintsWithVisualFormat:@"V:|-8-[view]-8-|" options:0 metrics:nil views:@{@"view" : view}]];
~~~
このコードは、赤い背景色のUIViewをself.viewに追加して、そのUIViewを8ピクセル内側に配置するというものです。
しかし実際にこのコードを実行してもUIViewは期待通りに配置されず、代わりに大量のログが出力されます。

~~~
    Unable to simultaneously satisfy constraints.
    Probably at least one of the constraints in the following list is one you don't want. Try this: (1) look at each constraint and try to figure out which you don't expect; (2) find the code that added the unwanted constraint or constraints and fix it. (Note: If you're seeing NSAutoresizingMaskLayoutConstraints that you don't understand, refer to the documentation for the UIView property translatesAutoresizingMaskIntoConstraints) 
(
    "<NSLayoutConstraint:0x7fe45ac66210 H:|-(8)-[UIView:0x7fe45ac64800]   (Names: '|':UIView:0x7fe45ad90070 )>",
    "<NSLayoutConstraint:0x7fe45ac42380 H:[UIView:0x7fe45ac64800]-(8)-|   (Names: '|':UIView:0x7fe45ad90070 )>",
    "<NSAutoresizingMaskLayoutConstraint:0x7fe45ac65c20 h=--& v=--& H:[UIView:0x7fe45ac64800(0)]>",
    "<NSLayoutConstraint:0x7fe45ac761c0 'UIView-Encapsulated-Layout-Width' H:[UIView:0x7fe45ad90070(375)]>"
)

Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x7fe45ac42380 H:[UIView:0x7fe45ac64800]-(8)-|   (Names: '|':UIView:0x7fe45ad90070 )>

Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKit/UIView.h> may also be helpful.
~~~
ログの内容は、制約が競合しているためにAutoLayoutでUIViewを配置できないというものです。
しかしよく見ると、先ほどのコードでは設定していない制約が暗黙的に追加されていることが分かると思います。

~~~
    "<NSAutoresizingMaskLayoutConstraint:0x7fe45ac65c20 h=--& v=--& H:[UIView:0x7fe45ac64800(0)]>",
    "<NSLayoutConstraint:0x7fe45ac761c0 'UIView-Encapsulated-Layout-Width' H:[UIView:0x7fe45ad90070(375)]>"
~~~
この2つです。
#AutoresizingMaskとの競合
実は、UIViewのautoresizingMaskに設定された値は、暗黙的にAutoLayoutの制約に変換されているようなのです。
InterfaceBuilder上で制約を設定した場合はこの問題が起こりませんが、コード上でviewを作成して制約を追加すると、AutoresizingMaskを変換した制約と競合し、結果として期待通りの配置にならない場合があります。
上のコードではautoResizingMaskに何も設定していませんが、既定の値がAutoLayoutの制約に自動的に変換されるためこのようなことになっているようです。

そのような場合は、translatesAutoresizingMaskIntoConstraintsプロパティをNOに設定すると、AutoresizingMaskの値が制約に変換されなくなり、うまくいくようです。

~~~objc
    view.translatesAutoresizingMaskIntoConstraints = NO;
~~~