エムスリーテックブログ

エムスリー(m3)のエンジニア・開発メンバーによる技術ブログです

Auto Layout の警告を Stack View で全部修正した話

エンジニアリンググループの田根です。主にiOSエンジニアをやっていますが、サーバサイド(Java, Scala, Rubyなど)もやったり、Tableauでレポートを見える化したりと何でもやっています。

弊社では医療従事者向けにいくつかのiOSアプリをリリースしています。
今回はこの中のm3.comアプリの話になります。

m3.comアプリとは

f:id:y-tane-m3:20181129140210p:plain
m3.comアプリ
弊社が運営している日本最大級の医療従事者専門サイトm3.comのアプリ版になります。
1stリリースは2014年ですので Objective-C で作られていましたが、現在は9割近くが Swift に書き換えられています。

経緯

事の発端は iOS 12 のリリース直後、アプリがフリーズするという問い合わせが急増したことです。
とはいえ社内の検証端末でいくらテストしてもフリーズするようなことが発生しませんでした。
そもそもそんなに簡単に再現するようであれば iOS 12 beta の頃から動作確認していたのですぐに発覚するはずです。
諦めかけていたところ、たまたま別件で開発中にエンジニアがフリーズする現象に遭遇し原因が判明することになります。

フリーズの原因

フリーズの原因は Auto Layout の補完によるものでした。
なぜ Auto Layout の補完によってフリーズするのかというと、Auto Layout には制約に不足や不整合な制約があると自動で制約を補完してくれる機能があります。
このため適当にレイアウトしても、補完により崩れることなくiOSは表示してくれます。
この機能により、 ある Cell の中の View に不整合が起き、補完により自動で制約が追加されました。
この追加された制約により今度は隣り合っている別の View の制約に不整合が起き、更に補完しようと制約を追加します。
これにより今度は最初に補完された View の制約に不整合が起き補完、そしてまた隣の View の制約に不整合と補完とループしてフリーズしてしまったようです。

原因の特定が遅れた理由

弊社ではパーソナライズして記事を配信しているため、一人として同じ画面になることはありません。
ある特定の記事がある位置に表示されたときに別の記事の制約が影響を受け今回のような事象になってしまったためなかなか再現しなかったのでした。

再発防止策

2014年にリリースされたアプリなので、その頃は Auto Layout の機能も貧弱で Stack View ももちろんありませんでした。 このため実行時に Auto Layout の警告が出ていても動いているので問題ないと放置してきました。
しかし iOS12 で上記の様な問題が発生したため、Auto Layout の警告が出ているところを全部修正することにしました。

Auto Layout の警告箇所の特定

Auto Layout の警告があると以下のようなログが出力されるのですが、複雑な画面だと View が無数にありこれがどの View で発生しているのか特定するのが非常に難しいです。

[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:0x2825840f0 UILayoutGuide:0x283f9cb60'UIViewSafeAreaLayoutGuide'.trailing == UILabel:0x119e1dad0'Label'.trailing + 200   (active)>",
    "<NSLayoutConstraint:0x2825842d0 UILabel:0x119e1dad0'Label'.leading == UILayoutGuide:0x283f9cb60'UIViewSafeAreaLayoutGuide'.leading + 200   (active)>",
    "<NSLayoutConstraint:0x282584550 'UIView-Encapsulated-Layout-Width' UIView:0x119e1d8f0.width == 375   (active)>",
    "<NSLayoutConstraint:0x282584190 'UIViewSafeAreaLayoutGuide-left' H:|-(0)-[UILayoutGuide:0x283f9cb60'UIViewSafeAreaLayoutGuide'](LTR)   (active, names: '|':UIView:0x119e1d8f0 )>",
    "<NSLayoutConstraint:0x282584230 'UIViewSafeAreaLayoutGuide-right' H:[UILayoutGuide:0x283f9cb60'UIViewSafeAreaLayoutGuide']-(0)-|(LTR)   (active, names: '|':UIView:0x119e1d8f0 )>"
)

Will attempt to recover by breaking constraint 
<NSLayoutConstraint:0x2825840f0 UILayoutGuide:0x283f9cb60'UIViewSafeAreaLayoutGuide'.trailing == UILabel:0x119e1dad0'Label'.trailing + 200   (active)>

Make a symbolic breakpoint at UIViewAlertForUnsatisfiableConstraints to catch this in the debugger.
The methods in the UIConstraintBasedLayoutDebugging category on UIView listed in <UIKitCore/UIView.h> may also be helpful.

その場合以下の記事の SymbolicBreakpoint によるデバッグが非常に役立ちました。 qiita.com

Auto Layout の警告の解読

Viewの特定が出来たら警告ログの解読なのですが、非常に読むのがつらいです。
この場合は WTF Auto Layout? というサイトにログを貼り付けると視覚化して表示してくれるのでこれを見ながら修正しました。
上記のログを WTF Auto Layout? に貼り付けたリンクがこちらです。
www.wtfautolayout.com

† This constraint was added by a table or collection view to enforce its cell size. と書かれている部分が制約に問題が発生したために自動で追加された制約になります。
この部分を解消するように修正します。

Stack View化

リリース当初は UIStackView なんてものはなかったので古くからある View は Stack View を使っていませんでした。
iOS 9以降であれば UIStackView が使えるので、どんどん Stack View化していきます。
これにより View を非表示にした時の制約を書かなくて済むようになり、かなりシンプルになります。(これだけで大部分の警告が解消できました)

最後に

動いているからといって警告を放置するのよくないということを学びました。
皆様もログに警告が出力されていたら出来るだけ早く対応するように心がけましょう。

We are hiring

エムスリーではiOSエンジニアも募集中です。
興味を持たれた方はぜひ以下のリンクからご応募ください:

jobs.m3.com