はじめに
受託案件を受けていますと、iPadのレイアウトについて縦横異なるデザインにしたいという要望をよくお聞きします。
私も個人的には賛成です。せっかくiPadの大きさで自由にレイアウトできるなら縦横異なるレイアウトにしてもいいと思っています。縦と横でアプリの使われるUXも変わりますし、それに合わせてレイアウトするのはありと思っています。
さて、iOS8からSizeClassという概念がXcodeに追加されました。
これはiOS6から追加されたレイアウト機構Auto Layoutを補完するもので、SizeClassを使うことでiPhone、iPadを1つのStoryboadでレイアウトできるようになる仕組みです。
以前まではiPhone用、iPad用でStoryboadを分けて作っていたのでそれが1つにまとまることは有意義なことでした。
しかしこのSizeClass、なんとiPadのレイアウトは縦横同じものしか設定できないようになっています。
このままお客様に「縦横レイアウト?iPadじゃ無理っす!」で流してもいいでしょう。
でもiOSエンジニアとして、なんとかしたい。そう思います。
なんとかなりました。それを解説したいと思います。
実行環境
この記事でお話するコードは以下の環境で動作しています。
- Xcode8.1
- Swift3.0.1
- アプリターゲットiOS10
SizeClassのおさらい
SizeClassとは
iPhone,iPadの縦横サイズを抽象的なサイズ「Compact」「Regular」「Any(Compact,Regularどちらでも)」にわけて、それに対してAuto Layoutを指定できる仕組みです。
iPhone, iPad各サイズ対応表
縦(Portrait)の場合は
Device | Portrait Width | Portrait Height |
---|---|---|
iPhone4s/5/5s/6/6s/SE/7 | Compact | Regular |
iPhone6Plus/6sPlus/7Plus | Compact | Regular |
iPad | Regular | Regular |
iPhoneは幅がCompactで高さがRegular、iPadは幅も高さもRegularなのが特徴です。
横(Landscape)の場合は
Device | Landscape Width | Landscape Height |
---|---|---|
iPhone4s/5/5s/6/6s/SE/7 | Compact | Compact |
iPhone6Plus/6sPlus/7Plus | Regular | Compact |
iPad | Regular | Regular |
iPhone4s-7は幅がCompacteで高さもCompact,iPhonePlus系は幅がRegularで高さがCompact、iPadは幅も高さもRegularで同じなのが特徴です。
SizeClassを上書きする!
このままではiPadは縦も横も同じRegular、Regularなので、レイアウトを分けることができません。結果としてiPadでは縦横のレイアウトを作ることができなくなっています。
これを打破するメソッドがUIViewControllerにあります。
overrideTraitCollection(forChildViewController:)
です。これを実行することでSizeClassを任意に上書きすることができます。
このメソッドはContainer View Controllerに対して、子のViewControllerのSizeClassを上書きすることができます。
完成版
まず完成レイアウトをご覧ください。
iPad Proのシュミレーターでビルドをしていますが、縦と横で異なるレイアウトが表示されているのがわかると思います。
Stroyboardの設定
プロジェクトをDeviceをiPadまたはUniversalで作成し、デフォルトで作成されているViewControllerに対してContainer Viewを設置します。
Container Viewの制約は親Viewに対して上下左右0ポイントの制約を追加します。
次にContainer ViewのEmbed segue されている子ViewControllerにたいして、縦(Width:Compact, Height:Regular)、横(Witdh:Regular, Height:Regular)のレイアウトを作ります。
縦のレイアウト
Width:Compact,Height:Regularでレイアウトをしていきます。
真ん中にLabelを配置します。テキストを「width:C height:R」にします。
Attributes Inspectorを表示して一番下installed横のプラスボタンから「wC hR」のinstalledをオンにします。これでWidth:Compact,Height:Regularのみに表示されるラベルを作成できます。
ラベルの制約は親ビューに対して縦横中心に整列させます。Center Horizontally in ContainerとCenter Vertically in Containerを設定します。
さらにわかりやすいように下に緑のViewを配置します。Viewの制約は下部に高さ200、VFLで表すと下記のように設定しています。
H:|-view-|
V:[view(==200)]-|
横のレイアウト
Width:Regular, Height:Regularでレイアウトしていきます。
Labelを配置して、縦の場合と同じようにAttributes Inspectorを表示して一番下installed横のプラスボタンから「wR hR」のinstalledをオンにします。
ラベルのテキストは「Width:R Height:R」にします。
制約をWidth:R Height:Rに対して追加します。親ビューに対してLeading Margin、Center Vertically in Containerを追加します。
viewを配置して背景をオレンジに変更して、右側に表示するように幅200で制約を追加します。VFLで表すと下記のように設定しています。
H:|[view(==200]-|
V:|-[view]-|
以上でレイアウトは終了です。
コード
続いてコードです。ViewControllerクラスに対して下記を実装すればiPadで縦横レイアウトが分かれる様になります。
import UIKit
class ViewController: UIViewController {
//for hack sizeClass
var isPortrait = false
var traitCollectionCompactRegular = UITraitCollection()
var traitCollectionAnyAny = UITraitCollection()
override func viewDidLoad() {
super.viewDidLoad()
self.setUpReferenceSizeClasses()
}
}
extension ViewController {
//Mark: - sizeClass
func setUpReferenceSizeClasses() {
let traitCollectionHcompact = UITraitCollection.init(horizontalSizeClass: UIUserInterfaceSizeClass.compact)
let traitCollectionVRegular = UITraitCollection.init(verticalSizeClass: UIUserInterfaceSizeClass.regular)
self.traitCollectionCompactRegular = UITraitCollection.init(traitsFrom: [traitCollectionHcompact, traitCollectionVRegular])
let traitCollectionHAny = UITraitCollection.init(horizontalSizeClass: UIUserInterfaceSizeClass.unspecified)
let traitCollectionVAny = UITraitCollection.init(verticalSizeClass: UIUserInterfaceSizeClass.unspecified)
self.traitCollectionAnyAny = UITraitCollection.init(traitsFrom: [traitCollectionHAny, traitCollectionVAny])
self.isPortrait = self.view.frame.height > self.view.frame.width
}
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
self.isPortrait = size.height > size.width
}
override func overrideTraitCollection(forChildViewController childViewController: UIViewController) -> UITraitCollection? {
let traitCollectionForOverride = self.isPortrait ? self.traitCollectionCompactRegular : self.traitCollectionAnyAny
return traitCollectionForOverride
}
}
解説
何をしているかといいますと、
setUpReferenceSizeClasses()
メソッドでプロパティを初期化しています。traitCollectionCompactRegular
はUITraitCollectionのインスタンを保持するプロパティですが、水平方向にCompact,垂直方向にRegularのSizeClass情報を保持させます。
traitCollectionAnyAny
は水平方向、垂直方向ともにAnyのSizeClass情報を保持させます。
isPortrait
は端末が縦か横かを判別させるプロパティです。
viewWillTransition(to:with:)
メソッドはコンテナビューのサイズが変更されたときに実行されるUIViewControllerのメソッドです。
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
self.isPortrait = size.height > size.width
}
プロパティisPortrait
をデバイスが縦なのか横なのかを判別します。
overrideTraitCollection(forChildViewController:)
は子のViewControllerのSizeClassを上書きするメソッドです。レイアウトに変更がかかると呼ばれるようです。(呼ばれるタイミングについてすこし不確かなので知っている方がいればご連絡いただければ幸いです)
override func overrideTraitCollection(forChildViewController childViewController: UIViewController) -> UITraitCollection? {
let traitCollectionForOverride = self.isPortrait ? self.traitCollectionCompactRegular : self.traitCollectionAnyAny
return traitCollectionForOverride
}
isPortrait
プロパティによってtraitCollectionCompactRegular
かtraitCollectionAnyAny
の値を返すようにしています。これをすることによってデバイスが縦の場合はSizeClassをWidth:Compact,Height:Regularに上書きすることができます。
お断り
今回のやり方では、iPadの縦をCompact,RegularにしたことでiPhoneの縦レイアウトと共通になっています。
つまりiPhoneとiPadのレイアウトを同じにしなければいけなくなります。
これを分けたいという場合には今回の記事内容は適しませんのでご了承ください。
おわりに
iPadのレイアウトを縦と横を分けるTipsをご紹介しました。
けっこう使う機会は多い気がします。
参考になりましたら幸いです。
参考サイト
- [Sizing class for iPad portrait and Landscape Modes]
(http://stackoverflow.com/questions/26633172/sizing-class-for-ipad-portrait-and-landscape-modes) - overrideTraitCollection(forChildViewController:)
- viewWillTransition(to:with:)