HOME » Natsu note » iOS » Size Classで柔軟なレイアウトを実現

Size Classで柔軟なレイアウトを実現 2015/02/12/|iOS, , ,

iOS 8とXcode 6より導入されているSize Classは、使えば使うほどその便利さに気がつきます。この便利なSize Classを理解すべく、IBとアセットカタログのみを利用したサンプルを用意してみました。

Size Classとは

何ができるのか

Size Classを使えば、画面のサイズ(デバイスの種類や向き)に応じた画面レイアウトを実現できるだけでなく、フォントサイズや画像も画面サイズごとに設定できます。さらには、特定のサイズのときだけ表示されるビューも、IB(Interface Builder)上で簡単に実装できてしまいます。

iOS 8では、「Size Class」に関連するクラスやプロトコルもいくつか追加されており、回転時の処理などにも変更が出ています。もちろん、これらの内容を理解することも重要です。しかし何よりも、「Size Class」という概念そのものを正しくとらえることが大切であり、概念が分かってしまえばそれをコードに応用することは難しくないはずです。

どんなものなのか(CompactとRegular)

Size Classでは、画面の幅(W)と高さ(H)を「Compact」と「Regular」という2つのグループに分けて表現します。

iOS 8.1の現時点では、各デバイスのSize Classは次のようになっています。Size Classはあくまでもサイズを表現するためのものですので、PortraitとLandscapeでは異なるSize Classになることに注意してください。

デバイス 向き 幅(W) 高さ(H)
iPad Portrait Regular Regular
Landscape Regular Regular
iPhone 5.5インチ Portrait Compact Regular
Landscape Regular Compact
5.5インチ以外のiPhone Portrait Compact Regular
Landscape Compact Compact

このような分類はよく見かけますが、実はこれ、実装時にはあまり便利ではありません。そもそも、Size Classでは、幅と高さという2通りのサイズを、CompactとRegularというこれまた2通りの定義で表現するため、その組み合わせは全部で4通りです。この「4通り」に主眼を置いて上の表を書き換えます。

高さ デバイス(向き)
1 Regular Regular iPad(Portrait・Landscape)
2 Regular Compact 5.5インチのiPhone(Landscape)
3 Compact Regular 全てのiPhone(Portrait)
4 Compact Compact 5.5インチ以外のiPhone(Landspace)

幅と高さのSize Classを必ず指定しなくてはならないわけではありません。例えば、「幅がCompactで高さは任意」といった表現も可能で、この場合、上の表の3番と4番が該当します。すなわち、「幅がCompactで高さは任意」というSize Classは、「全てのiPhoneのPortraitと、5.5インチ以外のiPhoneのLandscape」を指し示すことになります。

デフォルトのレイアウト

起点は(Any/Any)

Xcode 6で新規プロジェクトを作成すると、Storyboardに含まれる画面が正方形になっています。これは、Size Classが幅、高さともに任意である状態を意味します(以降、これを(wAny, hAny)と表現します)。Storyboard下部にSize Classコントロールがあり、クリックするとサイズを変更するための3x3のマス目が表示されます。

Size Classコントロール

このマス目は、1列(行)でCompact、2列(行)でAny、3列(行)でRegularを示します。例えば、1列3行の縦長のエリアを選択すると、それはSize Class=(wCompact, hRegular)を意味し、上の表の3番に該当します。すなわち、全iPhoneのPortraitはこのレイアウトになるということです。ちなみに、Size Classコントロールの下にも該当するデバイスが表示されますので、いちいち上の表を見なくても大丈夫です。

Size Classコントロール

Size Classコントロールを使うと、キャンバスのSize Classを変更できます。

例えば、iPad専用のアプリを作るときはあらかじめキャンバスのSize Classを(wRegular, hRegular)にしておいてもいいかもしれません。将来的に他のSize Classへ対応する必要が出てくるかもしれませんが、(wRegular, hRegular)を基準にして実装しても問題ないでしょう。

しかし、基本的には(wAny, hAny)で基準となる実装を行い、それに対してSize Class依存の変更を施す方が、IBとの親和性が高いように感じています(ここは好みの問題もあるとは思いますが)。

これより、チュートリアル形式でSize Classの使い方を説明します。サンプルなんて作らなくていいからXcodeの使い方が知りたいという方は、レイアウトの拡張(Size Class対応)までスキップしてください。

Anyでレイアウト

サンプルアプリ(ユニバーサルアプリ)では、まずはじめにデフォルトである(wAny, hAny)でレイアウトをしていきます。表示するのは、UIImageViewが2つ、UITextFieldが2つ、UILabelが2つです(世間はバレンタインですね)。デフォルトのレイアウトが少々気になると思いますが、ここは無視して先に進んでください。

DefaultLayout.png

部品を配置したら制約(Constraint)を設定します。

UIImageView (Cake)

水平方向は親ビューの中心に合わせ、Top Layout Guideまでの距離を8とします。

UIImageView Contraint 1

UIImageView Constraint 2

UIImageView (Line)

水平方向は親ビューの中心に合わせ、Bottom Layout Guideまでのマージンを28とします。

UIImageView Constraint 4
UIImageViewConstraint5.png

UILabel (AND)

水平方向、垂直方向共に親ビューの中心に合わせます。なんとなく、もう少し下に配置したくなると思いますが、このあとプレビューで確認しながら修正していくので、今はこのままでOKです。

UILabelConstraint3.png

サンプルで使っているフォントは”Noteworthy Light 40.0″です。

UITextField (You)

UILabel (AND)とY中心を合わせ、さらに左右のマージンを、左(コンテナマージン)0、右(UILabel)8とします。

UITextField Constraint 1

UITextField Constraint

サンプルではさらに、UITextFieldの高さを50としていますが、これは必須ではありません。

UITextField (Me)

“You”のUITextFiledと左右対称で同等のことをします。

UILabel (2015.02.14)

水平方向は親ビューの中心に合わせ、UIImageView (Line)までの距離を8とします。サンプルでは、フォントは”Hoefler Text 20.0″としています。

UILabel Constraint 1

UILabel Constraint

プレビューでレイアウトの確認

アシスタントエディタ(画面右側)のPreviewでレイアウトを確認してみます。表示するデバイスは、Previewエディタ左下の「+」ボタンから追加できます。

確認してみると、iPadでは問題ありませんが、iPhoneではPortrait、Landscape共に問題があることが分かりました。スクリーンショットは4インチのものですが、5.5インチでも程度の差こそあれやはり問題は残っています。

DefaultLayoutPhone4-1.png

DefaultLayoutPhone4-2.png

レイアウトの方針を決める

デフォルトレイアウトのままではダメなことが分かったので、レイアウトの方針を決めます。

UIImageView (Cake)

これはiPhoneの画面には大きすぎます。この画像はiPad専用とし、iPhoneではもう少し小さい画像を使いましょう(デコレーションケーキは諦めてカップケーキにします…)。画像の置き換えにはアセットカタログを使います。

UIImageView (Line)

幅が狭いとはみ出してしまいます。この画像は幅が狭い画面(=Compact)のときには表示をやめてしまいましょう。

UILabel (2015.02.14)

iPhoneではバランスが取れていますが、iPadでは少々小さすぎるようです。もう少しフォントサイズを大きくしましょう。

UITextField

幅が狭いと窮屈です。もし、高さが十分なのであれば、横並びに配置するのではなく、縦並びに配置することにします。幅が狭い=Compactで、かつ高さが十分=Regularのときにレイアウトの調整を行います。

レイアウトの拡張(Size Class対応)

方針が決まったので、デフォルトのレイアウトを各Size Classに対応させていきます。

UIImageViewの中身を変える

Size Classに応じてUIImageViewのコンテンツを変更するためには、アセットカタログ(images.xcassets)を使います。アセットカタログで該当する画像を選んだらAttributes Inspectorを開きます。Width、Heightという欄があるので、ここで必要なサイズを追加できます。ここでは、ともにAny & Compactを選びます。これで、4通りの画像が配置できるようになります。

アセットカタログのSize Class

幅、高さのいずれかもしくは両方がCompactのときは、別の画像を表示します。アセットカタログ上では、「*」がAny、「-」がCompact、「+」がRegularを示しますので、該当箇所に画像を配置します(サンプルでは@2xしか配置していませんが、必要に応じて@1xや@3xも配置してください)。

AssetCatalogueAddImages.png

再びStoryboardに戻り、Previewを見てみます。

AppendSmallImage.png

いい感じになりました。アセットカタログを使うことで、Storyboardやコード上の画像名を変更することなくSize Classに応じた画像を適用できるのでとても便利です。

※ ちなみに、ここでは違いが分かりやすいように、デコレーションケーキの代わりにカップケーキを使いましたが、やり過ぎ(違い過ぎ)は禁物です。

特定のSize Classでは表示しない

続いて一番下のライン画像です。幅がCompactのときは非表示とします。Size Classが導入される前は、ビューコントローラなどで画面のサイズに応じてビューのhiddenプロパティを設定する必要がありましたが、Size Classを使えばIB上で数クリックです。

該当するUIImageViewを選択し、Attributes Inspectorを表示します。一番下に、「Installed」と書かれたチェックボックスがあります。これが、表示・非表示をコントロールするためのチェックボックスです。デフォルトでは「Installed(表示)」状態になっています。左側の「+」ボタンをクリックすると追加したいSize Class一覧が表示されるので、「Compact Width」「Any Height」を選びます。

AttributesInspectorAddSize.png
Size Classを追加

“wC hAny”と書かれた欄が表示されたら、Installedのチェックボックスを外します。これで、Size Class=(wCompact, hAny)のときにはこのUIImageViewは表示されません。

ビューのアンインストール

Installedではないビューは、hiddenプロパティがYESになるのではなく、ビュー階層から削除されます。

ここで設定した(wComapct, hAny)とは、最初の表で「3」と「4」に該当する組み合わせです。すなわち、全iPhoneのPortraitと、5.5インチ以外のiPhoneのLandscapeで、この画像が非表示となります。5.5インチのiPhoneでは、Portraitでは表示され、Landspaceでは表示されません。

ここでは単独のビュー(UIImageView)の表示・非表示を切り替えましたが、サブビューを持つようなコンテナビューでも同様のことができます。

非表示ビューに影響する制約の設定

(wCompact, hAny)のとき、一番下の画像をビュー階層から取り除いてしまったため、レイアウトが影響を受けたビューがあります。それが日付を記載したUILabelです。このUILabelは、下のUIImageViewまでのマージンを8としていたため、この画像がなくなると垂直方向の位置を決定する要素がなくなってしまいます。そのため、UILabelが一番上に来てしまっています。

LabelConstraint.png

この問題に対処するために、UILabelに制約を追加します。ただし、下の画像が非表示になるとき、すなわち(wCompact, hAny)のときのみ制約を追加します。

手順はこうなります。

  1. UILabelからBottom Layout Guideまでの距離を28とする制約を追加
  2. 追加した制約を選択しSize Inspectorを表示
  3. “wC hAny”のときのみInstalledとなるようにチェックボックスの状態を変更(下図)

Size Classに応じた制約を追加

キャンバスのSize Classを(wAny, hAny)のままで上の操作を行うと、ステップ1のところで制約に不整合が生じて警告が表示されます。しかし、ステップ3まで進めば不整合は解消されますので心配はいりません。

気になる場合は、キャンバスのSize Classを(wCompact, hAny)に変更してから制約を追加することもできます。なお、キャンバスのSize Classを変更してから制約を追加する例は、UITextFieldのレイアウト変更で解説しています。

UILabel (2015.02.14) のフォントサイズを変更する

UILabelのフォントサイズ(現状20.0)を、Size Classが(wRegular, hRegular)のときのみ30.0に変更します。(wRegular, wRegular)は、iPadのPortraitとLandspaceに対応しますので、この設定で、iPadでは30.0ポイント、iPhoneでは20.0ポイントのフォントサイズが採用されることになります。

UILabelを選択し、Attributes InspectorのFont欄の左側にある「+」ボタンをクリックします。すると、これまでと同様にSize Classを示す組み合わせが表示されるので、ここから(wRegular, hRegular)を選択します。入力欄が表示されたら、フォントサイズを30.0にします。

Fontサイズを追加

ここではフォントサイズを変更しましたが、フォントそのものの変更もできます。画面サイズに応じて適したフォントは異なると思いますので、Size Classを利用して上手にカスタマイズしたいものです。

UITextFieldのレイアウトを変更する

最後は少々複雑です。Size Classに応じてビューの並びを横並びから縦並びに変更します。

デフォルトでは、横並びのレイアウトになっているため、これを縦並びにします。縦並びにするのは、Size Class=(wCompact, hRegular)のときのみです。これは、全てのiPhoneのPortraitに該当します。

ここで、(wCompact, hAny)としていないことに注意してください。幅のみに注目し、高さをAnyとしてしまうと、高さがより小さなiPhoneのLandscapeモードでも縦並びになってしまいます。そのため、必ず(wCompact, hRegular)と、幅、高さともに指定することが重要です。

今回は、キャンバスのサイズを(wCompact, hRegular)に変更してから作業します。Storyboardエディタの下に表示されるSize Classコントローラで(wCompact, hRegular)を選んでください。キャンバスのSize Classを変更すると、エディタ上の画面サイズも変更されます(経験上、Previewを開いたままキャンバスのSize Classを切り替えるとXcodeがクラッシュすることが多い気がします)。

まずは、不要な制約の削除からです。左側のUITextField (You)から、以下の2つの制約を削除します。また、右側のUITextFieldからも、これと同等の2つの制約を削除します。

UnusedConstraints.png

特定のSize Classのときのみ削除するために、該当する制約を選んでSize Inspectorを表示したらInstalledの横の「+」ボタンをクリックします。すると、「Compact Width | Regular Height (current)」というように現在のキャンパスのSize Classが一番上に表示されるので、ここを選択します。

Size Classの追加
UninstallConstraint.png

チェックボックスが追加されたら、Installedのチェックをはずします。同様の作業を他の3つの制約に対しても行います。

現在のキャンバスのSize Classでのみ制約を削除するためのキーボードショートカットも用意されています。削除したい制約を選択し、Cmd-Deleteで現在のSize Classでのみ制約がアンインストールされます(Cmdキーを押さずにDeleteを押すと、全てのSize Classから削除されてしまうので注意してください)。

続いて、YouのUITextFieldを少し上に、MeのUITextFieldを少し下にずらし、両サイドのレイアウトガイドまで引き伸ばします。Youの方は、右側のマージンとUILabel (AND)までの下マージンを、Meの方は、左側のマージンとUILabel (AND)までの上マージンを追加します。左右のマージンは0、上下のマージンは12とします。

マージンの追加

ここで追加した制約をSize Inspectorで見てみると、Size Class=(wCompact, hRegular)のときのみInstalledになっていることが分かります。これは、キャンパスのSize Classをあらかじめ変更してから作業したためです。

言い換えると、キャンパスを特定のSize Classに固定した状態で制約の追加などを行っても、そのSize Classでしか有効となりません。制約の数が増えて複雑になってくると分かりにくくなってきますので、基本の操作はデフォルトの(wAny, hAny)で作業していくことをお勧めします。

これですべての修正が終わりました。プレビューで確認してみます。上から順番に4インチiPhoneのPortrait、Landscape、5.5インチiPhoneのLandscape、そして最後がiPadのPortraitです。iPadでは、PortraitでもUITextFieldが横並びになっていること、ケーキが元々のデコレーションケーキであることが確認できます。

SizeClassLayoutPhone4-1.png

SizeClassLayoutPhone4-2.png

SizeClassLayoutPhone55.png

SizeClassLayoutPad.png

まとめ

Size Classは便利です。今回、ここに挙げたすべての処理をアセットカタログとIBだけで完結させています。これまでだと、回転時や起動時に画面サイズを検出し、それに応じた処理をする必要がありました。より複雑なレイアウトになると、デバイス回転時に画面遷移を行うことすらありました。

それが、Size Classの導入でコード量も一気に減り、とても分かりやすくなったと感じています。

Auto Layoutは好みではないとか、IBは嫌いだとか、そういう意見もときには聞きますが、個人的にはこれからの開発には全て必須だと思っています(あくまでも個人的な意見です)。

今後また新たなサイズのデバイスが発売されても平静を保てるよう、今のうちから対応しておきたいものです。

今回、サンプルで使った美味しそうなケーキのイラストはこちらからいただきました。どうもありがとうございました。
http://girlysozai.com

サンプルプロジェクトはGitHubにあります。Swiftですがコードは変更していません。
https://github.com/natsuko/Valentine

Auto LayoutをはじめUIKitに関してはこちらもどうぞ(iOS 7ベース、Objective-Cです)。