Support Split Screen Multitasking 〜 回転時のレイアウト崩れ修正でやったこと

クリスマスよりも、Appleの審査休みと年内の最終リリース日の方が気になる季節になってきました。[iOS Advent Calendar 2019](https://qiita.com/advent-calendar/2019/ios)は8日目、@sussan0416が投稿します、よろしくお願いします。

1ヶ月ほど前、縦固定のiPadアプリの改修を担当しました。今日はその時に経験した話です。
至極当然な内容かと思いますが、経験の棚卸しさせてください :pray:

なお、今回の記事はUIViewController寄りの内容ですが、**UIView寄りの内容は[別記事](https://qiita.com/sussan0416/items/e1f76ef6bc340e3e2524)**に書いていますので、そちらも参照いただけますと嬉しいです。

# 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で、コードでやっているようです。

<details><summary>コード</summary><div>

```swift:
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)
}
```

</div></details>

端末の種類が少なかった頃は、この実装でも問題にはならなかっただろうと思います。
しかし、このままでは「フルスクリーン」では良くても、マルチタスクにした際に問題が発生します。

## 問題点

レイアウト処理もviewDidLoadで一気に処理しているため、端末を回転したときにレイアウトが崩れます。

①Portraitで起動し、Landscapeへ回転。横幅が足りません……。
![images.001.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75329/f9e0ed74-5cbc-ef9f-73bd-822b1b6b9cda.jpeg)

②Landscapeで起動し、Portraitへ回転。画面からはみ出してしまいました……。
![images.002.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75329/e034363b-64e2-7727-dca1-98e34bddb0c8.jpeg)

③マルチタスクを実行すると……。レイアウトが固定なので、表示の一部は切れてしまいます。
![images.003.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75329/9659798b-8368-7e5f-3038-774de040d3aa.jpeg)

これではマルチタスクもできません。実装を修正する必要があります。

# 修正内容

## UIViewのインスタンス化とレイアウトの実装を分ける

横幅いっぱいのViewであれば、`autoresizingMask`を`.flexibleWidth`に設定するだけで、ひとまず対応は可能です。しかし、autoresizingMaskでは難しい場合もあります。

基本的にUIViewのインスタンス化とレイアウト処理は、分けるようにします。
`viewDidLoad`ではインスタンス化とviewの追加のみを実行し、レイアウト処理は`viewDidLayoutSubviews`に書きます。

<details><summary>コード</summary><div>

```swift
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
}
```

</div></details>

これでひとまず、回転への対応ができました！

![images.004.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75329/9c68e3f2-476c-a69f-d82a-15a934586ec3.jpeg)

## `UIScreen.main.bounds`への依存を直す

マルチタスクに対応するとき、ディスプレイサイズに依存した実装はレイアウト崩れの原因になります。
ディスプレイサイズは極力使わないようにしたほうが良いです。たとえば端末の判断

```swift
// 画面サイズに依存しているため、マルチタスクのときにレイアウトが崩れる
let viewWidth = UIScreen.main.bounds.width

// ViewControllerのview幅を使う
let viewWidth = view.bounds.width
```

![images.005.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75329/a7ed60dd-9ccf-5002-5916-bc270156b787.jpeg)

これで、マルチタスクも含めて、横画面対応ができました！ひとまず！

---

# その他の修正

## `safeareaInset`を使う

viewのy方向のレイアウトが固定値になっていることがありました。iPhone X以降のノッチ付き端末だと表示が切れる可能性があります。
iPad向けのアプリとはいえ、なるべく、システムが提供するレイアウトのための定数を使うように心がけたいです。

```swift
var yPosition: CGFloat = view.safeAreaInsets.top
```

![images.006.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/75329/88c0544a-53c9-357d-d94b-f3b11a9186fd.jpeg)

## 画面を回転して戻ってきたときに、CollectionViewのCellが崩れる

UICollectionViewや、UITableViewの画面から他の画面へ行き、回転してから戻る。すると、レイアウトが崩れているんですね……。
カスタムしているCellの実装が、UIViewのライフサイクルに即して実装してあればよいのですが、Viewの生成とレイアウトが引き剥がしにくくされているなど、難があることが多いです。

仕方がなく`viewWillTransitionToSize(:withTransitionCoordinator:)`を使用して、画面サイズの変更を検知します。`collectionView.collectionViewLayout.invalidateLayout()`を呼び出しました。

```swift
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にしても、**「ライフサイクル意識して実装しよう」**これに尽きます……。

書ききれないので、今回はこのあたりで。
失礼いたします :bow: