24/7 twenty-four seven

iOS/OS X application programing topics.

Auto Layoutの静的な制約で実現するカラム幅が可変のテーブル

次に示すような見出しと各カラムが右寄せ、ラベルの文字数によってカラムの幅が伸縮し、広くなった場合は隣の列を押し出し、短くなった場合は少なくとも見出しの幅に収まり、各列の間には一定のマージンを置くというテーブルレイアウトを、静的なAuto Layoutの制約だけで作ることを考えます。

https://user-images.githubusercontent.com/40610/47085526-d7d45180-d251-11e8-9293-7a82bbc6d6c7.gif


このような、UIコンポーネントが持つコンテンツの大きさによって隣接するコンポーネントを押し出すような場面ではAuto Layoutがとても効果的に働きます。

Auto Layoutなしで実現しようとすると、列ごとの各行の文字幅を計算し、最大の幅に合わせて再配置する、という処理をコンテンツが変わるたびに行うということになりますが、Auto Layoutの制約を使用する場合ではそもそもレイアウトの再計算を自分でやる必要はないので、コンテンツの変わったタイミングなどを気にする必要はありません。

ただデータを再代入するだけで適切にレイアウトが変化します。状態を監視してバインドするような仕組みがあれば完全に宣言的に書けるでしょう。

サンプルコードは下記のリポジトリで公開しています。

github.com

見出し行を作る

まず見出しとなる行を作ります。 UIViewに適当な高さ(今回の例では44pt)の制約を付けて上端と左右両端を固定します。行を示すこのビューは厳密には無くてもよいですがあったほうが作りやすいです。多くのビューに対してAuto Layoutの制約を組み立てる場合はフラットな構造よりも、適当にビューでグルーピングすると問題がシンプルになります。

高さも固定ではなくコンテンツの高さで決まるようにもできますが、今回の本質とは関係がないので簡単に44pt固定の行ということにします。

ラベルを3つ配置して、一番左のラベルは左寄せ、残りの2つは右寄せに設定します。

それぞれをタテ方向の中央配置にして、左右のラベルはそれぞれビューの左端と右端に固定、真ん中のビューは右のラベルから10pt固定のマージン、左のラベルに10pt以上のマージンの制約を付けます。

制約が成り立つかどうかだけでいうと、最後に付けた左のラベルと真ん中のラベルの10pt以上のマージンは必要ありませんが、コンテンツの幅が想定よりも大きくなったときでも、ラベル同士が重なってしまわないようにするためのものです。

f:id:KishikawaKatsumi:20181106031637p:plain

実際のStoryboardは次のようになります。

f:id:KishikawaKatsumi:20181106031824p:plain:w320

データ行を作る

データ行のレイアウトは見出し行と一緒なので見出し行のビューごとコピーして、それを修正していく形で作ると簡単です。

コピーしたビューは自身の内部の制約は保持されますが、外部との制約はすべて外れている状態なので、まず同様にビューの両端と、見出し行の下端と自身の上端を合わせる制約を付けます。

f:id:KishikawaKatsumi:20181106033042p:plain

実際のStoryboardは次のようになります。

f:id:KishikawaKatsumi:20181106033109p:plain:w320

ここで、ラベルにとても多くの文字を入力してみます。残念ながら10pt以上のマージンの制約があるにもかかわらず、ラベルのサイズがそれ以上に大きくなってしまうため制約がコンフリクトしてラベルが重なってしまいました。

f:id:KishikawaKatsumi:20181106033245p:plain:w320

この問題は、水平に並んだ2つ以上のラベルのうち、どれかのContent Compression Resistance Priorityを下げる(もしくは他のラベルのContent Hugging Priorityを上げる)ことで解決します。 そうするとそれぞれのラベルのサイズの合計が外側のビューより大きくなったとしても、外側のビューに収まるように優先度の低いラベルが小さくなって制約が解決します。

f:id:KishikawaKatsumi:20181106034208p:plain:w320

同様にして、残りの行を表すビューを追加します。行のビューはそれぞれをUIStackViewに入れるとさらに簡単です。サンプルコードではタテのレイアウトはUIStackViewに任せています。

f:id:KishikawaKatsumi:20181106034550p:plain:w320

カラム幅を最大文字数に合わせて揃える

さて、ここまでの状態ではカラム幅が不揃いなので、文字数に合わせて揃うようにしていきましょう。

f:id:KishikawaKatsumi:20181106034912p:plain:w320

見出し行と各データ行について、右端のラベルをすべて選択します。その状態で互いにEqual Widthの制約を付けます。つまり、同じ列のラベルはすべて同じ幅になるという制約になります。

f:id:KishikawaKatsumi:20181106035207p:plain:w320

この時点でStoryboardを見てわかるように、もっとも幅の大きいラベルに合わせて、列の幅が揃うようになりました。

真ん中のカラムの各ラベルに対しても、同様にEqual Widthの制約を互いに付けます。

f:id:KishikawaKatsumi:20181106035641p:plain:w320

これで完成です。 Storyboard上でラベルにいろいろなテキストを入力して意図したとおりにレイアウトが変わるかどうか試してみてください。 実行せずにさまざまな状態を確認できることはStoryboardで制約を組み立てる大きな利点です。

f:id:KishikawaKatsumi:20181106035932p:plain:w320

f:id:KishikawaKatsumi:20181106035928p:plain:w320

いかがでしょうか。文字数が多い場合でも少ない場合でもきれいに列の幅が調整され、端が揃っていることがわかります。

最初に制約を付けておくだけで、あとはコンテンツに応じて自動的に調整されるので、コードを書く必要がなく、バグが入り込む余地が少なくなります。

アップルのAuto Layout Cookbookというガイドには、今回紹介したテクニックをはじめ、具体的なレイアウトのサンプルに対する制約の例がたくさん載っています。 ひと通り手を動かしながら読んでみると、Auto Layoutをより高いレベルで使いこなせるようになるのでオススメです。

developer.apple.com