Help us understand the problem. What is going on with this article?

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

クリスマスよりも、Appleの審査休みと年内の最終リリース日の方が気になる季節になってきました。iOS Advent Calendar 2019は8日目、@sussan0416が投稿します、よろしくお願いします。

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

なお、今回の記事は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のライフサイクルを理解し「いつレイアウトされるのか」を理解して実装する必要があると、改めて感じました。

以下の記事はとても参考になりました!


ここから先は、棚卸しです。
非常に長くなったので、UIViewControllerで対応したことのみを棚卸しします……。

問題のあるUIViewController

私が対応した案件では、こんな実装をよく見かけました。
UIの初期化メソッドを、viewDidLoadから呼び出す実装です。
インスタンス生成から、レイアウトまでを一気に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へ回転。横幅が足りません……。
images.001.jpeg

②Landscapeで起動し、Portraitへ回転。画面からはみ出してしまいました……。
images.002.jpeg

③マルチタスクを実行すると……。レイアウトが固定なので、表示の一部は切れてしまいます。
images.003.jpeg

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

修正: 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
}

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

images.004.jpeg

修正: UIScreen.main.boundsへの依存を直す

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

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

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

images.005.jpeg

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


その他の修正: safeareaInsetを使う

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

var yPosition: CGFloat = view.safeAreaInsets.top

images.006.jpeg

その他の修正: 画面を回転して戻ってきたときに、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にしても、「ライフサイクル意識して実装しよう」これに尽きます……。

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

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした