Xcode
iOS
Storyboard
AutoLayout

怖くない!AutoLayout 〜多画面対応 with Storyboard〜

AutoLayoutとは

AutoLayoutはiPhone SE, iPhone 7, iPhone 7Plusなど、様々なサイズのiPhone全てでいい感じにパーツを配置するための仕組みです。
Storyboard上でパーツを配置するときに、絶対座標で並べるのではなく、「上端を揃える」「y軸中心から10px」「このパーツとこのパーツは等幅」のような、制約(Constraints)によって、自動で位置を計算することで実現します。

昔はiPhoneのサイズはバリエーションが少なく、Storyboardで適当に配置していても許されていましたが(?)、最近はバリエーションも多いので、リリースするにはAutoLayoutはほぼ必須になっています

自動でもできるけど

AutoLayout使ってると言っても、「逃げるは恥だが役に立つ!」と言ったりして、Add missing Constraints を押していませんか?自動で追加されるものは、一見その端末ではうまくいってるような気もしますが、違う端末の場合とか、余白を少し調整したいときとか、非常にメンテナンスしにくいものになってしまいます。

いろいろな束縛方法

束縛を行うには、いろいろな方法が用意されています。Storyboardの右下のボタンポチポチするとでてきます。

DiariesTableViewCell_xib.png

いろいろな束縛方法がありますが、自分的に一番わかりやすくてやりやすいと思う、Spacing to nearest neighbor を中心に、束縛方法を紹介します

絶対値

名前 本記事内表記 内容
width 幅固定 固定の幅、不等号にもできる
height 高さ固定 固定の高さ、不等号にもできる
Aspect ratio アスペクト比 アスペクト比

他要素との兼ね合い

名前 本記事内表記 内容
Equal Widths 等幅 等幅
Equal Heights 等高 等高
leading edge 左端揃え 左端揃え
trailing edge 右端揃え 右端揃え
top edge 上端揃え 上端揃え
bottom edge 下端揃え 下端揃え
Horizontal Centers 横中央揃え 横中央揃え
Vertical Centers 縦中央揃え 縦中央揃え
Spacing to nearest neighbor 余白 隣接アイテムとの相対距離(上下左右それぞれ)
Horizontaly in container X軸中心 親要素のX軸中心からの距離
Vertically in container Y軸中心 親要素のY軸中心からの距離

スクリーンショット_2017_02_28_22_39.png

余白は数字を入れて、濃い赤線でつながれた部分のみに制約が追加されます。その他はチェックを入れたところです。

束縛対象を意識する

Viewのレイアウトを考える時、それぞれの要素に対して、

  • x
  • y
  • width
  • height

の4つが、一意に決まるようにルールを作ります。これを意識しながら、想像して、制約を追加します

例えば、固定幅100という制約はwidth=100、アスペクト比の制約はwidth = K * height、のように、方程式で表すことができます。
4つの値がわからない、4次方程式の解が定まるには、基本的には4つの制約が必要になりますので、基本的に制約は4つがメンテもしやすくてよいです。
5個以上制約がある場合は、冗長な制約はないか、確認してみてください。冗長な制約があると、矛盾を生みやすいし、後からの調整もしにくくなります!

※これめっちゃ大事です!!

冗長でもなく4つ以上の制約が付く場合は、不等号制約です。
例えばエディタを作る時、文字の内容に応じてのみサイズを変えていると、文字がない時に高さが0になってしまって、入力開始したくてもタップが反応しません。
そこで、最小の高さを大きめに設定したりします。

不等号制約は、一旦固定幅などの制約をつけた後、右側のメニューから編集すると設定できます。

他にも、4つ以上の制約をつけた上でプライオリティを設定して、より複雑な制御を行うこともあります。

想像クイズ

例えば、画面に対して「X軸方向に中心」「高さを固定」「幅固定」という制約があったら、束縛できているでしょうか?

正解はNOです。
後ろ2つで、width, heightは固定されています。
画面のサイズは当然決まっているのと、幅が固定かつX軸方向に中心なので、x座標は束縛されています。
しかし、y座標は動いても制約は満たされますので、y座標が束縛されていません。

よって、y座標を束縛する制約を付け加える必要があります。
Spacing to nearest neighborのleading」や、「Y軸中心」などを付け加えると、これを満たすことができて、束縛成功になります。

トップダウン、ボトムアップ

束縛する際、まず子要素に合わせてボトムアップに決めていくか、親要素に合わせてトップダウンに決めていくか決める必要があります。

1画面の中にきれいに配置したい場合や、固定サイズのセル・ビューの中に配置したい場合は、トップダウンに外側から、
文章の長さや画像サイズなどによって、セルやスクロールViewの中身の高さなどを自動的に調整する場合などはボトムアップに、それぞれの高さから全体を組み上げていくように、
決めていきます。

トップダウンに決めていく場合

トップダウンに決めていくには、親ビューが、画面全体であるとか、あるサイズの固定されたUIViewの中であるとか、サイズが固定されている必要があります。

各Viewについて、どこかの角から順番に固定していきます。

画面いっぱいに広げる

一番簡単なのは、画面いっぱいに広がるViewです。背景のViewなどに便利です。

Spacing to nearest neighbor をすべて0にすると固定できます。もしくはAlign edgesを4つ全てに反映させる。(どっちでも良いと思います)

具体的な例

DiariesTableViewCell_xib_—_Edited.png

セルのサイズは決まっているものとして、上の例のように固定してみます。数字の背景のビューから固定してみましょう

日付の背景の青いViewの固定

まず位置について、
x軸方向に関しては左のスペースを0にすると固定できそうです。
y軸方向に関しては、上下のスペースを設定するか、中央にするかで固定できそうです。

サイズについては、子要素にラベルがあるので幅が小さすぎると困るため、幅を固定したいです。
高さはセルの高さを超えないようにしたいですね。

よって、「左余白固定0」「幅固定」「上余白固定10」「下余白固定10」とすることで、背景のビューが固定できますね。

数字、曜日のラベル固定

数字の背景は既に固定されているので、数字の背景の中での、数字・曜日の位置・サイズを束縛します。

ラベルの数字がはみ出すと困るので、はみ出さない高さに固定します。

どちらもX軸中心にしたいですね。もしくは、両サイドの余白を0にした上で、labelのtextAlignmentをcenterにすることで中心にするほうが考えやすいかもしれません(X軸中心のときは幅の制約が必要)

あとはy位置は、これも趣味ですが、自分ならそれぞれY軸中心-5, Y軸中心+15のような感じで固定します。もちろん上下余白で制約つけても構いません

ラベル固定

まず、本文とタイトルの2つのラベルは左端、右端ともに揃えたほうがきれいだと思うので、左端揃え、右端揃えの制約をつけます。
その後、どちらかに左余白、右余白の制約を入れて、両方の左右の余白を決定します(こうすることで、それぞれに余白を設定したときよりも、余白を変更したいと思ったときに一箇所変更するだけで両方に反映される)

タイトルのラベルは上からの余白で決定し、必ず一行なので高さも固定してしまいましょう。
本文は複数行の可能性もあるので、上余白(タイトルラベルの下端からの距離になる)と下余白を決定することで、残りの領域すべて本文のラベルにしてしまいます!

まとめ

これですべてのレイアウトが決定されました!必要な制約は、4*5(パーツ数)=20こでしたね!

ルールを重要な順に考えて、そしてさらに束縛対象を意識して、どういう制約(横方向のことなのか縦方向のことなのか、など)をつければ良いのか考えて、それを追加することで、きれいに制約を追加します!

ボトムアップに決めていく場合

親要素のサイズが定まっていない時、すなわちScrollViewのスクロール領域(ContentView)のサイズを子要素のサイズから確定させていく時や、テーブルのセルのサイズを中身によって変えたりする時(Twitterアプリのような)は、ボトムアップに決めていく必要があります。

preferredContentSize

画像のセットされたUIImageViewや、テキストがセットされてかつスクロールの禁止されたUITextView、numberOfLines=0のUILabelなどは、preferredContentSizeといって、高さもしくは幅を固定するだけで、推奨されるサイズを計算することができます。つまり、画像のアスペクト比固定であったり、テキストがちょうど収まるサイズであったりになるようにできます。

これもしくは幅固定などの制約を使うことで、うまくボトムアップに外側のレイアウトを束縛します。

例えば、テキスト全体が埋まるようなセルを作る場合には、
まずTextViewのscrolling Enabledのチェックを外し、
TextViewの左右余白を0にするなどで幅を固定して、上下余白も0にすることで、セルサイズもTextViewのpreferredContentSizeと同じにすれば実現できる。
上下にマージンを入れたければ上下余白を0でなくて10とかにすればよいですね。
(セルの横幅はTableViewと同じなので固定されている)

AutomaticDimention

tableViewのセルは、デフォルトでは高さは固定なので、ボトムアップに設定するには、viewDidLoadで、tableViewのrowHeightを以下のように設定しておく

tableView.rowHeight = UITableViewAutomaticDimension

※デフォルトになったので設定不要になりました。設定しても問題はないです。

ScrollViewのAutoLayout

ScrollViewを使っている場合のAutoLayoutはやや特殊です。

UIScrollView自体の束縛は、SuperViewに合わせます。大抵の場合、上下左右余白をすべて0にして、画面いっぱいに広げる事が多いと思います。

そして、ContentViewとしてUIViewを置き、そのUIViewの幅はUIScrollViewと等幅の制約を加えた後、ボトムアップにUIViewの高さを計算させることが多いです
(もちろん、レイアウトが固定の場合はUIViewに高さ固定の制約を加えてトップダウンに決めても問題ありません)

Adjust ScrollView Insets

今この文脈であまり関係ないが、よく陥る罠。

実はデフォルトからチェックが入っているもので、ScrollViewやTableViewなどは、navigationBarやTabBarで隠れる分インセット(padding)を追加する、というものである。
意味を知らないと、navBarと重ならないようにtableViewをおいたのになぜか上に余白ができる〜と悩んでしまう

つけてもつけなくても良いが、意識しないとはまる。

関連付け

各制約も、Swiftのコードに対して関連付けさせることができます。

たとえば、固定幅の制約をコードから動的に書き換えてレイアウトを再計算させる、なんてこともできます

上下左右余白設定の問題

上下左右余白の設定は、html, cssのmarginのように自動的に近いものに対して適用されるのではなく、「その制約を入れたときに横にあったもの」に対して適用される。
よって、パーツを追加したり削除したりすると束縛をし直さなければいけないし、また動的に変わる場合などは束縛できない

動的に子要素の数が変わる場合

  • UIStackViewの導入を検討する
  • 幅の制約を1にすることでごまかす(裏技)

UIStackViewは子要素を自動的にリサイズするもので、上手く使うととても便利。
裏技として、幅を1にしてごまかすことで、小要素の数を変わったように見せかけて自動でレイアウトかけることができる。

関連付けを行って、高さを1にすることで(0にするとなぜか次200とかにしても表示されない)