LoginSignup
4

More than 3 years have passed since last update.

posted at

updated at

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

いわゆる横画面対応など、画面回転時の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()
}

以上

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

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
What you can do with signing up
4