前書き
ご無沙汰しております。元 Auto Layout 絶対◯すマンです。
元、と聞くと、まるで筆者が Auto Layout と和解したかのように見えますが、そんなことは一切ありません。単純に SwiftUI の登場により、アップルが事実上自ら Auto Layout を◯してくれた1から、もう私の役目は果たしたと思って、「元」と名乗っただけです。
ところが残念ながら、登場して 2 年経ちましたが、SwiftUI がまだまだ UIKit を完全に取っ払えるほど強力とは言えませんしバグや罠もたくさん潜んでいます。筆者自身の個人プロジェクトですら、SwiftUI のプロジェクトにもかかわらずどうしても仕方なく中で UIView
を使ってるところがあります。新規プロジェクトですらこうですので、長年保守されてきたプロジェクトは尚更 UIKit にさようならを言いにくいのが実情です。
では、Auto Layout をまだ使ってるプロジェクト開発チームに配属されたら、レイアウトを保守するときどうすればいいか?そんなあなたの悩みを、この記事で解決してあげられたらと思います。
※ この記事は Auto Layout のマニュアルだったり逆引きだったりではありません。この記事で取り扱うのは、Auto Layout を使うときの「メソッド」ではなく、「アプローチ」です。
レイアウトの本質
Auto Layout の話に入る前に、まず一回原点に立ち戻って、そもそも「レイアウト」とはどんなものかを考えてみましょう。これを理解しなければ、どんなレイアウトツールだろうか正しく使えません。
レイアウトに必要な情報
レイアウト、すなわち「配置」のことです。言ってしまえば、単純に何かのオブジェクトを、どこかのスペースに置く、それだけのことです。ここの「配置」には、二つの情報が含まれています:「どこ」に置くか、そして「どれだけのスペース」を使うか。
ここまで読んだら、一部の人は何か気づいたことあるかもしれません。そう、frame
の型である CGRect
です。CGRect
の origin
と size
は、まさにこの二つの情報を指しています。
そして UIKit のような二次元の空間上に配置するためには、それぞれさらに「横軸」と「縦軸」の二つの情報が含まれます。これらがそれぞれ CGPoint
の x
と y
と、CGSize
の width
と height
です。
一次元のレイアウト
ところがいきなり二次元のことを考えるとちょっと難しいかもしれません、なのでここで解説するのに一旦次元数を落として、一次元の空間で考えてみましょう。そう、線です。
試しに、下記の一次元の座標で、一本の線を置きたいと考えましょう。
ではこの線を正しく置くためにはどんな情報が必要か?そう、上に書いた通り、「どこ」に置くか、そして「どれだけのスペース」を使うかです。もっとわかりやすく言えば、この線の起点をどこに置くか、そしてこの線の長さがどれくらいか、です。
例えば、この線の起点座標を 2、そしてこの線の長さを 3 とすると、我々はこのような線を得られます:
ところで、お気づきの方もいるでしょう、この線を正しく置くためには、何も決められるのは起点と長さだけではない。起点座標が 2、終点座標が 5、と言うような決め方でも問題ないし、なんなら中点座標が 3.5、終点座標が 5 と言うような決め方でも問題ない。我々が必要なのは、あくまでこの座標軸においての、二つのダブらない情報です。この二つの情報さえあれば、最終的には「起点情報」と「長さ情報」に落とし込めるからです。
ただし気をつけないといけないのは、三つ以上の情報を与えてしまうと、逆に配置が無理な可能性もあります。例えば心の中で想像してみてください:この線を起点座標が 2、終点座標が 3、そして長さが 4、と言うような配置の仕方を与えてしまうと、どうなってしまうのか。頭が爆発しますね。
ところが今回の場合は、自由に伸ばせる線を配置する問題ですが、一方で自ら好ましい長さ情報を持つ線もあり得ます。例えば自分の固有長さが 5 の線があるとしましょう。この場合、もちろん配置する際に敢えてその長さ以外の数値を指定することも可能ですが、何も指定しなければ自分で 5 の長さで自動的に配置します。このような自分で自分の「スペース」を決められる性質を「Self-Sizing」と言います。Self-Sizing が可能な線を配置する場合は、起点情報や終点情報と言った「位置情報」を一つだけ指定すれば、あとは自動で正しく配置できます。例えばこの固有長さが 5 の線の起点座標を 3 に指定すれば、このように配置されます:
またこれらは全て 1 本の線を配置するだけですが、例えば線を 2 本を配置するとき、これらの情報を間接的に落とし込むこともあります。例えば 1 本目の線の起点を 1 に、2 本目の線の終点を 8 に、この 2 本の線を同じ長さに、さらにこの 2 本の線の間には 1 の間隔を空ける、と言ったような配置もあり得ます。この場合少々回りくどいですが、ちょっと計算すれば、同じくこの 2 本の線のそれぞれの起点座標と長さを計算できますね。直接計算することもできますが、説明が面倒なので、ここは中学校で習った方程式を使って計算してみましょう。ここで同じ長さの線が 2 本ありますので、その長さを $x$ としましょう。そうするとこのように方程式が立てられます:
$1 + x + 1 + x = 8$
↓
$2x + 2 = 8$
↓
$2x = 6$
↓
$x = 3$
つまり、1 本目の線は起点座標が 1、長さが 3、そして 2 本目の線は起点座標が 1 + 3 + 1 = 5、長さが 3 ですね。これを図にすればこうなります:
二次元のレイアウト
さて、一次元の場合が理解できたら、二次元の場合も全く同じことだとわかります。例えば起点座標を (1, 2)、そしてサイズを (3, 4) にすると、このような矩形が得られます:
これがこの矩形のレイアウト情報です。そして一次元の場合と同じ、最終的に起点座標とサイズに落とし込めれば、どんな情報でも大丈夫です。つまり我々が本質的に必要なのは、二つの横座標のダブらない情報と、二つの縦座標のダブらない情報です。
ただし二次元の場合、サイズに関して一次元の場合存在しないものがあります。例えばアスペクト比がその代表格でしょう。アスペクト比自身は横座標の情報でも縦座標の情報でもありませんが、別の幅もしくは高さと合わせれば、足りない高さもしくは幅情報が得られます。例えばもしアスペクト比が 1:1 の矩形があるとします。この段階ではまだこの矩形の幅も高さもわかりませんが、幅が 5 であれば高さも 5、逆も然りで高さが 5 であれば幅も必ず 5 になります。これがアスペクト比です。
他にも、固定したアスペクト比ではありませんが、幅がによって高さが決まることもあり得ます。例えばテキストを表示する矩形があるとします、この場合幅が決まれば、テキストの内容に応じて高さが決まります。
Auto Layout を使う
さて、レイアウトの本質がわかったら、Auto Layout についても理解しやすくなると思います。そう、Auto Layout は本質的には、単純にさまざまな制約を使って、一つ一つのビューの起点座標情報とサイズ情報を計算しているだけです。
Auto Layout の制約
Auto Layout の制約は、大きく分ければ 2 種類あって、一つはビューとビューの関係性を用いた制約、もう一つはビュー自身の固有制約です。
ビュー自身の固有制約はとても簡単です、これは単純に例えば自身の幅がいくら、高さがいくら、アスペクト比がいくら、と言った情報の制約です。これらの制約は自分自身の固有制約なので、他のビューに一切依存しません。
そしてビューとビューの関係性を用いた制約は多少面倒です、なぜならこの二つのビュー自身の関係性がまた様々ですし、そしてこれらのビューのどんな要素に関しての関係性なのかもまた様々です。
比較的に一番良くあるのは、親ビューと子ビューの関係性を用いた制約でしょう、例えば子ビューの top を親ビューの top に合わせて 8 pt のマージンを空ける、例えば子ビューの width を親ビューの width の 50% にセットする、これらは全て親ビューと子ビューの関係性を用いた制約です。
次に子ビュー同士の関係性を用いた制約も同じくらいよく見かけます、例えば子ビュー A の bottom と子ビュー B の top を合わせて 16 pt の間隔を空ける、例えば子ビュー C の height と子ビュー D の height を同じ大きさにする、これらは全て子ビュー同士の関係性を用いた制約です。
他にも、それぞれの階層関係が非常に複雑ですが一応同じ画面には出るビュー同士の関係性の制約だったりもあります、Auto Layout の制約は、意外と何も直接な階層関係があることを求めておらず、巡り巡って最終的にはビューの座標情報に落とし込めればなんでもいいのです。人間がそれを読めるかどうかは別ですが。
どんな制約にせよ、最終的には、全てのビューに関して、親ビューという座標軸において、二つのダブらない横座標の情報と、二つのダブらない縦座標の情報さえ落とし込めれば、Auto Layout は正しく画面をレイアウトできます。逆に言えば、そう落とし込めなければ、Auto Layout は正しく画面をレイアウトできません。
Auto Layout 特有の問題
Auto Layout は上記のように、レイアウトを決めるための情報を一つ一つの制約に分散させているため、レイアウトを決める際に気をつけないといけないところもあります。
子ビューのサイズで親ビューのサイズが変われる
これまで我々がレイアウトについて語ってきた一つの暗黙な前提条件として、まず親ビュー自身のサイズが決まっていて、それを持って親ビューが子ビューのレイアウトを決めているという流れがあります。しかし Auto Layout の場合は、必ずしもそうする必要がありません。レイアウトの本質のチャプターで書いたように、自分で自分のサイズを決められる Self-Sizing 機能が備わっているビューがあります。例えば UIImage
なら中で所持してる画像のサイズに合わせてサイズを決められるし、UILabel
も中で所持してるテキストに合わせてサイズを決められます。これらの子ビューを配置する際に、例えば親ビューと子ビューの関係性を用いた制約を決めた場合、もし親ビュー自身のサイズが決まっていなければ、そのサイズが子ビューに合わせて変わることもあります。ただ逆に言うと、この特性を利用すれば、本来できなかった親ビューにも Self-Sizing 機能をつけることが可能です。
例えば UILabel
を持つ UIView
があるとしましょう。UILabel
は自分で自分のサイズを決められるので、この UILabel
と親の UIView
の leading
trailing
top
bottom
それぞれについての制約を作れば、親ビューの UIView
も、この UILabel
の内容に応じて自分自身のサイズが決められる Self-Sizing
機能が付きます。
制約には優先度があり不等式が使える
これがもしかすると Auto Layout がわかりにくい一番大きな理由かもしれません。実はこれはさほど難しい仕様ではなく、単純に優先度や不等式が入ってくると、頭の中で整理しないといけない情報が格段に増えるだけです。
まずは優先度から話しましょう。優先度は即ちこの制約がどれほど守るべきかです。この値は最大が 1000、つまり優先度が 1000 なら絶対守らなければなりません;そして制約同士がどうしても衝突してしまう場合、優先度が高い方の制約が優先的に守られます。
次に不等式の話をしましょう。これまで話してきたレイアウトを決める際の条件は全て等式でした、例えば子ビューの top を親ビューの top に合わせて 8 pt のマージンを空ける制約、この場合「マージン = 8」という等式が成立します。ところが Auto Layout では不等式も使えます、例えばこのマージンを「最低 8 pt 空ける」と言うように決めることもできます、この場合「マージン >= 8」と言う不等式が成立します。
不等式と優先度に関してはよくセットで来ます、と言うのももし不等式がなかったらそもそも全部 1000 の優先度でレイアウトを決められるはず2ですし、逆に不等式だけではレイアウトが決められないので、必ず優先度が 1000 未満の等式が必要です。
先ほど書いたこのマージン >= 8 の話をしましょう。マージンが 8 以上であればいくらでも OK ですので、8 でもいいですし、800 でもいいです。これではマージンが決まりません。なので我々は追加で、例えば子ビューの top を親ビューの「セーフエリア」の top にぴったり合わせる、と言う制約をつけるとします。そしてこの制約の優先度を 999 とします。そうすれば、もし親ビューの top のセーフエリアが 8 pt 以上あれば、両方の制約に満足できるので、普通に子ビューの top が親ビューのセーフエリアの top に合わせられます。逆にもし親ビューの top のセーフエリアが 8 pt 未満であれば、子ビューはなるべく親ビューの top のセーフエリアに近づけるように 8 pt のマージンを空けます。
Auto Layout をデバッグする
ここまで読んだら、もう Auto Layout を用いたレイアウトはどう言うふうにアプローチすればいいかイメージできてきたと思いますので、逆に Auto Layout がエラーを吐き出したり、画面のレイアウトが崩れたりした時、更に言ってしまえばそもそもレイアウトの実行する前に制約のコードを読むだけで、どこに問題があるのかある程度予想できるのではないでしょうか。
Auto Layout に一番良くあるエラーは 2 種類3あります:Unsatisfiable Layouts と Ambiguous Layouts です。
Unsatisfiable Layouts
これは要するに制約解決していくと、そのビューに関わる全ての(優先度が 1000 の)制約を満足できない、と言うエラーです。レイアウトの本質のチャプターに書いたように、例えば起点を 2、終点を 3、幅を 4 のように制約つけてしまったら、当然これらの制約は全て満足できないですね。起点と終点を取るなら幅は必然的に 1 になるし、起点と幅を取るなら終点は必然的に 6 になります。
原因がわかったら、解決も簡単ですね、不必要な制約を取り外せば OK です。ものによっては優先度を下げることも可能です。
Ambiguous Layouts
これは要するに制約を解決していくと、そのビューに関わる全ての制約を満たすレイアウトが 2 つ以上ある、逆にいうと上とは逆に制約が足りないということですね。例えば Self-Sizing ができないビューを起点だけ定めていたら、当然サイズが決まらなくてレイアウトできないですね。また、Self-Sizing ができるビューであっても、自分でサイズの制約だけ上書きしてあげて、起点についての制約がなかったら、それも同じくレイアウトができないです。
なのでこれを解決するには、足りない制約を追加してあげれば OK です。ただし気をつけないといけないのは、例えば二つの優先度が 1000 未満の同じ数値の制約が同時に満足できない場合、これも Unsatisfiable Layouts ではなく Ambiguous Layouts になります。この場合はどっちかの制約の優先度を上げることも可能です。
まとめ
いかがでしたでしょうか。レイアウトの本質を理解できたら、実は Auto Layout の利用も、そこまで難しくないと少しは感じていただけたでしょうか。
Auto Layout に限らず、どんなレイアウトツールを利用しても、「レイアウトに必要なのは二つのダブらない横座標の情報と、二つのダブらない縦座標の情報」だということを意識していれば、きっと思う通りにレイアウトができると思います。Auto Layout も、仕組みとしては単純に「制約」というツールを通じて、それらの情報に落とし込もうとしているだけです。
後書き
実は iOS Advent Calendar の枠を予約してから、長い間何を書けばいいかずっと悩んでいました。心の中では実はいくつもネタはありましたが、よく考えてみたらどれも「iOS」ではなく、どっちかと言うと「Swift」のネタでした。実は自分が好きなのは iOS 開発ではなく、Swift を使った開発だと言うことを、改めて痛感しました。
そんな中で自分の Qiita の下書きリストを眺めてみたら、一昨年からずっと眠ってるこのタイトルとアウトラインしか書かれてないこの記事を思い出しました。実はこの記事を最初に書き始めたのは、まさに 2019 年の WWDC 前のことでした。その時とりあえずアウトラインだけ書いて、WWDC 終わったら肉付けをしようと思ったのですが、皆さんご存知 2019 年の WWDC で SwiftUI が発表されました。「あー、もう Auto Layout の役目終わったし、まだアウトラインしか書いてないからこの記事もう要らないか」と、何度も思って消そうとしましたが、なんとなく勿体無くて消せずに下書きリストに眠らせたままでした。
まあ Auto Layout のことだから、純粋な Swift ネタよりは少しは iOS 寄りではあるはずなので、こうして iOS Advent Calendar で完成させることを決意しましたが、来年こそ、SwiftUI がちゃんとまともに大規模案件で大規模導入できるほど進化してることをお祈りしたいところですね。