AutoLayoutのデバッグをする(1)

  • 48
    いいね
  • 0
    コメント

今回はAutoLayoutでうまく行かなかった時のデバッグ法のうち、ログを解読して頑張る方法と、Document Outline Controlを使う方法、SymbolicBreakpointを使う方法について書こうと思います。「AutoLayoutのデバッグする(2)」ではrecursiveDescriptionなどObjective-Cのプライベートメソッドを用いて怪しいレイアウトを見つける方法、「AutoLayoutのデバッグする(3)」ではViewデバッガーなど視覚的に見て解決する方法について(いつか)書く予定です。

明らかにしたいこと

スクリーンショット 2016-10-08 22.34.36.png

下記のエラーが出ているので何がダメなのかを知りたい

[LayoutConstraints] 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. 
(
    "<NSLayoutConstraint:0x170088840 UILabel:
      0x100b0cc30'hogehogehogehogehogehogeh...'.width == 300   (active)>",
    "<NSLayoutConstraint:0x170088e30 UILabel:
      0x100b0cc30'hogehogehogehogehogehogeh...'.leading == 
      UIView:0x100b0c890.leadingMargin + 100   (active)>",
    "<NSLayoutConstraint:0x170088e80 UIView:
      0x100b0c890.trailingMargin == 
      UILabel:0x100b0cc30'hogehogehogehogehogehogeh...'.trailing + 100   (active)>",
    "<NSLayoutConstraint:0x17408a4b0 'UIView-Encapsulated-Layout-Width' 
      UIView:0x100b0c890.width == 375   (active)>"
)

Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x170088840 UILabel:
0x100b0cc30'hogehogehogehogehogehogeh...'.width == 300   (active)>

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.

アプローチ方法

1.ログを見る

常套手段ですね。ログを見れば何がまずいのかを知ることができるので見てみます。

最初に「この中から予期せぬ制約を見つけて直してね」と言われているので()に囲まれている部分を見てみます。※hogehoge...は長いので[hoge]に置き換えています。

"<NSLayoutConstraint:0x170088840 UILabel:0x100b0cc30'[hoge]'.width == 300   (active)>",
"<NSLayoutConstraint:0x170088e30 UILabel:0x100b0cc30'[hoge]'.leading == 
 UIView:0x100b0c890.leadingMargin + 100   (active)>",
"<NSLayoutConstraint:0x170088e80 UIView:0x100b0c890.trailingMargin == 
 UILabel:0x100b0cc30'[hoge]'.trailing + 100   (active)>",

この3つは自分で追加した制約です。どれもUILabelの水平方向に関しての制約について言及していますね。
ちなみに一番最後のUIView-Encapsulated-Layout-xxx
システムが生成した制約で、変えることはできません。これは一つ目の制約と明らかに衝突しています。

次のログ文を見てみましょう。

Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x170088840 UILabel:
0x100b0cc30'hogehogehogehogehogehogeh...'.width == 300   (active)>

つまり「width = 300の制約を取り除いて表示しといたよ」ということです。

以上のことから、UILabelの水平方向の制約がまずいということに気づくことができます。

ちなみに最後に書いてあるUIViewAlertForUnsatisfiableConstraintsがどうのとかはアプローチ3で説明します。

おまけ:ログを見やすくする

制約にIdentifierを付けましょう。制約を選択した上で以下のように設定すればログでIdentifierが出るようになります。

スクリーンショット 2016-10-09 0.32.31.png

"<NSLayoutConstraint:0x170088480 'hogeLabelWidth' 
 UILabel:0x14fe0cfc0'hogehogehogehogehogehogeh...'.width == 300   (active)>"

2.Document Outline Controlを見る

Document Outline Controlを見ればどの制約がコンフリクトしているのかがわかる上に、解決の手助けもしてくれます。

スクリーンショット 2016-10-08 22.52.07.pngスクリーンショット 2016-10-08 22.50.25.png

Document Outline Controlは画像で赤枠で囲ってある部分です。1枚目の画像の赤枠内右上の「→」マークをクリックすると2枚目の画像の状態になります。ここではコンフリクトしている制約の一覧を見ることができます。今回の場合はleadingMargin, trailingMargin, widthでエラーが出ているので、水平方向の制約がコンフリクトしていると考察することができます。

赤枠内右上の赤丸はコードを書いている時によく見慣れているエラーマークと同義です。なのでこの部分をクリックすれば修正のお手伝いをしてくれます。

スクリーンショット 2016-10-08 23.00.12.png

試しにwidth = 300を削除します。すると以下の状態になり、エラーが消えました。

スクリーンショット 2016-10-08 23.51.01.png

ちなみにこの黄色三角は警告で「制約で指定している位置とstoryboardで配置している位置がずれてるよ」と教えてくれています。この三角をクリックすると以下の表示が出てきます。

スクリーンショット 2016-10-08 23.55.30.png

Update framesは現在の制約に合わせてstoryboardの配置を修正してくれます。
Update constraintsはstoryboardの配置に合わせて制約を変更します。
Reset to suggested constraintsは現在の制約を削除した上で、Canvas上の座標をもとに制約を自動で追加してくれます。

3.SymbolicBreakpointを使う

シンボリックブレークポイント(特定の条件にマッチした時に処理をストップしてくれる)を活用すればViewの階層を知ることもできます。追加をするにはBreakpoint Navigatorを開いて左下の「+」をクリックして「Add Symbolic Breakpoint...」を選択します。

スクリーンショット 2016-10-08 20.06.19.png

出てきたポップアップの「Symbol」にUIViewAlertForUnsatisfiableConstraintsを、「Action」にexpr -l objc++ -O -- [[UIWindow keyWindow] _autolayoutTrace]を入力してください。最初「Action」の部分は「Add Action」になっているので、クリックして「Debugger Command」に変えてください。

スクリーンショット 2016-10-10 0.13.51.png

これで実行すると次のような表示が出てきて実行がブレークします。この時画面上はまだレイアウトがされていない状態です。

スクリーンショット 2016-10-09 23.23.55.png

UIWindowの上にUIViewがあって...などの階層構造がわかります。で、今回のレイアウトだと単純すぎてありがたみがないのですが、怪しいレイアウトのところにはAMBIGUOUS LAYOUTが付くのでこれを頼りにレイアウトの修正をすることもできます。

さらに便利なことに、まだオブジェクトが画面に描画されていないのでこんなコマンドを打つこともできます。

スクリーンショット 2016-10-10 10.30.01.png

UILabel *の後の0x...は上の階層図のUILabel:の後に書かれているものと同じものにしています。このコマンドを叩いてからスクリーンショット 2016-10-10 10.31.06.pngをクリックすると次のような画面になります。

スクリーンショット 2016-10-10 10.31.18.png

いろいろ使えそうな予感がしてきましたね。

おまけ:注意

Xcode8.0はシミュレータで実行するとログがべろべろ出てくるというバグがあります。不要なログを除去するためにProduct -> Scheme -> Edit Scheme...でEnvironment Variablesに OS_ACTIVITY_MODE = disableを設定するという方法がありますが、これを設定してしまうとAutoLayoutのエラーログまで出なくなってしまいます。Xcode8.1 beta2では修正されているようなので、AutoLayoutのエラーログを見逃さないためにもXcode8.1 beta2に上げる(もしくは常に実機ビルドするようにする)のが良さそうです。