LoginSignup
6
4

More than 3 years have passed since last update.

UICollectionViewの横画面対応・回転対応 〜 レイアウト崩れに対応する

Last updated at Posted at 2019-12-07

いわゆる横画面対応など、画面回転時のUICollectionViewのレイアウト崩れについて。
こんな感じで対応した、というのをまとめておく。

よくある4カラムの実装(修正前)

端末の向き固定で使う分には問題はないが……。

class CollectionViewController: UIViewController {

    private var collectionView: UICollectionView!
    private var layout: UICollectionViewFlowLayout!

    override func viewDidLoad() {
        super.viewDidLoad()

        // レイアウトを決める
        layout = UICollectionViewFlowLayout()
        layout.minimumInteritemSpacing = 16
        layout.minimumLineSpacing = 16
        let itemCount: CGFloat = 4
        let itemWidth: CGFloat = (view.bounds.width - (itemCount - 1) * 16) / itemCount
        layout.itemSize = CGSize(width: itemWidth, height: itemWidth)

        // UICollectionViewを初期化
        collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
        collectionView.register(Cell.self, forCellWithReuseIdentifier: "Cell")
        collectionView.dataSource = self
        collectionView.backgroundColor = .white

        // CollectionViewしか使わないので、viewを交換しちゃう
        view = collectionView
    }
}

extension CollectionViewController: UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 30
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "Cell", for: indexPath) as! Cell
        cell.label.text = String(indexPath.row)
        return cell
    }
}

class Cell: UICollectionViewCell {
    var label: UILabel!

    override init(frame: CGRect) {
        super.init(frame: frame)

        // サブビューをインスタンス化して配置
        label = UILabel(frame: contentView.bounds)
        label.backgroundColor = .systemBlue
        contentView.backgroundColor = .yellow
        contentView.addSubview(label)
    }

    required init?(coder: NSCoder) {
        fatalError()
    }
}

回転してみる

Portraitで起動し、Landscapeへ。4カラムだったのが、5カラムに。しかも、アイテムのスペースが変わってしまった。

名称未設定.001.jpeg

Landscapeで起動して、Portraitに回転させると……。4カラムだったのが3カラムになってしまった……。

名称未設定.002.jpeg

実装を直す

とりあえずレイアウト関連の処理は、viewWillLayoutSubviews()に移動する。これだけで、カラム数やアイテムサイズは意図したとおりになる……!

override func viewDidLoad() {
    super.viewDidLoad()

    // UICollectionViewFlowLayoutをインスタンス化
    layout = UICollectionViewFlowLayout()

    // UICollectionViewを初期化
    collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
    collectionView.register(Cell.self, forCellWithReuseIdentifier: "Cell")
    collectionView.dataSource = self
    collectionView.backgroundColor = .white

    // CollectionViewしか使わないので、viewを交換しちゃう
    view = collectionView
}

override func viewWillLayoutSubviews() {
    super.viewWillLayoutSubviews()

    // レイアウト関連は、ここでやる
    layout.minimumInteritemSpacing = 16
    layout.minimumLineSpacing = 16
    let itemCount: CGFloat = 4
    let itemWidth: CGFloat = (view.bounds.width - (itemCount - 1) * 16) / itemCount
    layout.itemSize = CGSize(width: itemWidth, height: itemWidth)
}

これがベストな方法かはいまいちはっきりしません……。
きっと、独自でUICollectionViewFlowLayoutを継承したクラスを作るのがいいんでしょうね……。

とはいえこれでOK……と思いきや!!!
回転したとき、CellのcontentViewは4カラムを維持するようにリサイズされたが、Labelがリサイズされていない……!

Portraitで起動し、Landscapeへ。contentViewの黄色が見えてしまった。

名称未設定.003.jpeg

Landscapeで起動し、Portraitへ。UILabelにつけた青色で埋め尽くされてしまった……。

名称未設定.004.jpeg

斜め左上から見ると、こんな状況。

スクリーンショット 2019-12-07 21.24.35.png

青のlabelは、リサイズされずに隣り合ったセルと重なり合っている。
黄色のcontentViewだけはリサイズされて4カラムサイズの正方形に。

つまり、Cellクラスでaddしたviewが、リサイズされていない

UICollectionViewCellの修正

subViewをレイアウトするときに、場所を決めてあげれば良い。
レイアウトの処理は、layoutSubviewsに移動する。UIViewControllerのときと考え方は一緒。

class Cell: UICollectionViewCell {
    var label: UILabel!

    override init(frame: CGRect) {
        super.init(frame: frame)

        // サブビューはインスタンス化するだけ、場所は決めない
        label = UILabel()
        label.backgroundColor = .systemBlue
        contentView.backgroundColor = .yellow
        contentView.addSubview(label)
    }

    required init?(coder: NSCoder) {
        fatalError()
    }

    override func layoutSubviews() {
        super.layoutSubviews()

        // レイアウトの処理を移動してきた
        label.frame = contentView.bounds
    }
}

もしくは、autoresizingMaskを適切に設定すればOK。

override init(frame: CGRect) {
    super.init(frame: frame)

    // サブビューをインスタンス化して配置
    label = UILabel(frame: contentView.bounds)
    label.backgroundColor = .systemBlue
    label.autoresizingMask = [.flexibleWidth, .flexibleHeight]
    contentView.backgroundColor = .yellow
    contentView.addSubview(label)
}

これでCellサイズぴったり。

名称未設定.005.jpeg

他の画面で端末を回転して戻ってきた場合も大丈夫

CollectionViewの画面に戻ってくるタイミングで、CellのlayoutSubviews()が呼ばれるので、全く問題ない。

UINavigationControllerが親のとき

プッシュ → 回転 → バックしても、ちゃんとレイアウトが更新されている!

名称未設定2.001.jpeg

UITabBarControllerが親のとき

他のタブへ行く → 回転 → 元のタブに戻る。問題ない、レイアウトが更新されている!

名称未設定2.002.jpeg

↑見えにくいが、タブが追加されている

それでも大丈夫じゃない場合

大抵の場合、UIViewのライフサイクルに沿った実装になっていないことが多い。
でも、改修が困難なこともあるので。

対応策

あまりやりたくはないが、以下の方法がある。

  1. collectionView.reloadData()を呼ぶ
  2. collectionView.collectionViewLayout.invalidateLayout()を呼ぶ

呼ぶタイミングは、

  1. viewWillAppear(_:)
  2. viewWillTransition(to:with:)

のいずれか。

reloadData()

override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    collectionView.reloadData()
}

invalidateLayout()

override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
    super.viewWillTransition(to: size, with: coordinator)
    collectionView.collectionViewLayout.invalidateLayout()
}

以上

こんな感じで、レイアウト崩れにはとりあえず対応できるんじゃないかと!

6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4