クリスマスよりも、Appleの審査休みと年内の最終リリース日の方が気になる季節になってきました。iOS Advent Calendar 2019は8日目、@sussan0416が投稿します、よろしくお願いします。
1ヶ月ほど前、縦固定のiPadアプリの改修を担当しました。今日はその時に経験した話です。
至極当然な内容かと思いますが、経験の棚卸しさせてください
なお、今回の記事はUIViewController寄りの内容ですが、**UIView寄りの内容は別記事**に書いていますので、そちらも参照いただけますと嬉しいです。
Support Split Screen Multitaskingと横画面対応
はじめに、マルチタスクについておさらいしておきます。
iPad向けアプリは、2020年4月までに、マルチタスクへの対応が求められています。これに対応するには、アプリを4つの向き(Portrait, Landscape Left/Right, Upside Down)で実行できるように改修する必要があります。マルチタスクに対応するためには、必然的に、端末を回転したときのレイアウトにも対応することになります。
私が対応した案件では、横レイアウトに対応させることが目的だったので「横画面対応」と呼ばれていました。そのため、ここからは「横画面対応」と呼ばせていただきます。
担当した案件について
プロジェクトの状況は以下のとおりでした。
経過年数 | 言語 | アーキテクチャ | レイアウト |
---|---|---|---|
5年 | Objective-C | MVC | コードベースの実装、部分的にAuto Layoutのxib・storyboard(フルスクリーン固定) |
※この記事で出てくるコードはSwiftです
結論
「ライフサイクルをちゃんと理解しよう!」これが一番大切でした。
当然ではあるのですが、UIViewControllerとUIViewのライフサイクルを理解し「いつレイアウトされるのか」を理解して実装する必要があると、改めて感じました。
以下の記事はとても参考になりました!
- https://qiita.com/motokiee/items/0ca628b4cc74c8c5599d
- https://qiita.com/shtnkgm/items/f133f73baaa71172efb2
- https://qiita.com/kazuhiro4949/items/2d2f6f1636f0baed2341
ここから先は、棚卸しです。
非常に長くなったので、UIViewControllerで対応したことのみを棚卸しします……。
問題のあるUIViewController
私が対応した案件では、こんな実装をよく見かけました。
UIの初期化メソッドを、viewDidLoad
から呼び出す実装です。
ViewやControlのインスタンス生成から、レイアウトまでを一気にviewDidLoadで、コードでやっているようです。
コード
var redView: UIView!
var greenView: UIView!
var blueView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.white
prepareSubviews()
}
func prepareSubviews() {
let viewWidth = UIScreen.main.bounds.width
let margin: CGFloat = 16.0
let viewHeight: CGFloat = 100.0
var yPosition: CGFloat = 20.0
// Viewを横に2つ並べる
let segmentWidth = (viewWidth - margin * 3) / 2.0
redView = UIView(frame: CGRect(x: margin,
y: yPosition,
width: segmentWidth,
height: viewHeight))
redView.backgroundColor = .systemYellow
greenView = UIView(frame: CGRect(x: segmentWidth + margin * 2,
y: yPosition,
width: segmentWidth,
height: viewHeight))
greenView.backgroundColor = .systemGreen
// その下に、Viewを横いっぱいに一つ置く
yPosition += viewHeight + margin
blueView = UIView(frame: CGRect(x: margin,
y: yPosition,
width: viewWidth - margin * 2,
height: viewHeight))
blueView.backgroundColor = .systemBlue
view.addSubview(redView)
view.addSubview(greenView)
view.addSubview(blueView)
}
端末の種類が少なかった頃は、この実装でも問題にはならなかっただろうと思います。
しかし、このままでは「フルスクリーン」では良くても、マルチタスクにした際に問題が発生します。
問題点
レイアウト処理もviewDidLoadで一気に処理しているため、端末を回転したときにレイアウトが崩れます。
①Portraitで起動し、Landscapeへ回転。横幅が足りません……。
②Landscapeで起動し、Portraitへ回転。画面からはみ出してしまいました……。
③マルチタスクを実行すると……。レイアウトが固定なので、表示の一部は切れてしまいます。
これではマルチタスクもできません。実装を修正する必要があります。
修正内容
UIViewのインスタンス化とレイアウトの実装を分ける
横幅いっぱいのViewであれば、autoresizingMask
を.flexibleWidth
に設定するだけで、ひとまず対応は可能です。しかし、autoresizingMaskでは難しい場合もあります。
基本的にUIViewのインスタンス化とレイアウト処理は、分けるようにします。
viewDidLoad
ではインスタンス化とviewの追加のみを実行し、レイアウト処理はviewDidLayoutSubviews
に書きます。
コード
var redView: UIView!
var greenView: UIView!
var blueView: UIView!
override func viewDidLoad() {
super.viewDidLoad()
view.backgroundColor = UIColor.white
initializeViews()
}
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
layoutViews()
}
func initializeViews() {
redView = UIView()
greenView = UIView()
blueView = UIView()
view.addSubview(redView)
view.addSubview(greenView)
view.addSubview(blueView)
}
func layoutViews() {
let viewWidth = UIScreen.main.bounds.width
let margin: CGFloat = 16.0
let viewHeight: CGFloat = 250.0
var yPosition: CGFloat = 20.0
// Viewを横に2つ並べる
let segmentWidth = (viewWidth - margin * 3) / 2.0
redView.frame = CGRect(x: margin,
y: yPosition,
width: segmentWidth,
height: viewHeight)
redView.backgroundColor = .systemYellow
greenView.frame = CGRect(x: segmentWidth + margin * 2,
y: yPosition,
width: segmentWidth,
height: viewHeight)
greenView.backgroundColor = .systemGreen
// その下に、Viewを横いっぱいに一つ置く
yPosition += viewHeight + margin
blueView.frame = CGRect(x: margin,
y: yPosition,
width: viewWidth - margin * 2,
height: viewHeight)
blueView.backgroundColor = .systemBlue
}
これでひとまず、回転への対応ができました!
UIScreen.main.bounds
への依存を直す
マルチタスクに対応するとき、ディスプレイサイズに依存した実装はレイアウト崩れの原因になります。
ディスプレイサイズは極力使わないようにしたほうが良いです。たとえば端末の判断
// 画面サイズに依存しているため、マルチタスクのときにレイアウトが崩れる
let viewWidth = UIScreen.main.bounds.width
// ViewControllerのview幅を使う
let viewWidth = view.bounds.width
これで、マルチタスクも含めて、横画面対応ができました!ひとまず!
その他の修正
safeareaInset
を使う
viewのy方向のレイアウトが固定値になっていることがありました。iPhone X以降のノッチ付き端末だと表示が切れる可能性があります。
iPad向けのアプリとはいえ、なるべく、システムが提供するレイアウトのための定数を使うように心がけたいです。
var yPosition: CGFloat = view.safeAreaInsets.top
画面を回転して戻ってきたときに、CollectionViewのCellが崩れる
UICollectionViewや、UITableViewの画面から他の画面へ行き、回転してから戻る。すると、レイアウトが崩れているんですね……。
カスタムしているCellの実装が、UIViewのライフサイクルに即して実装してあればよいのですが、Viewの生成とレイアウトが引き剥がしにくくされているなど、難があることが多いです。
仕方がなくviewWillTransitionToSize(:withTransitionCoordinator:)
を使用して、画面サイズの変更を検知します。collectionView.collectionViewLayout.invalidateLayout()
を呼び出しました。
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator)
collectionView.collectionViewLayout.invalidateLayout()
}
UICollectionViewの回転時のレイアウト崩れ対応については、別の記事にもまとめましたので、参考にしていただけますと嬉しいです。
→ https://qiita.com/sussan0416/items/e1f76ef6bc340e3e2524
まとめ
iPadアプリを横画面に対応したときの経験をまとめました。UIViewControllerのみを書きましたが、他にも多くの経験がありました。UIView周りの経験を、またどこかでまとめようかな……。
まぁとにかく、UIViewにしてもUIViewControllerにしても、**「ライフサイクル意識して実装しよう」**これに尽きます……。
書ききれないので、今回はこのあたりで。
失礼いたします