いわゆる横画面対応など、画面回転時の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カラムに。しかも、アイテムのスペースが変わってしまった。
Landscapeで起動して、Portraitに回転させると……。4カラムだったのが3カラムになってしまった……。
実装を直す
とりあえずレイアウト関連の処理は、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の黄色が見えてしまった。
Landscapeで起動し、Portraitへ。UILabelにつけた青色で埋め尽くされてしまった……。
斜め左上から見ると、こんな状況。
青の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サイズぴったり。
他の画面で端末を回転して戻ってきた場合も大丈夫
CollectionViewの画面に戻ってくるタイミングで、CellのlayoutSubviews()
が呼ばれるので、全く問題ない。
UINavigationControllerが親のとき
プッシュ → 回転 → バックしても、ちゃんとレイアウトが更新されている!
UITabBarControllerが親のとき
他のタブへ行く → 回転 → 元のタブに戻る。問題ない、レイアウトが更新されている!
↑見えにくいが、タブが追加されている
それでも大丈夫じゃない場合
大抵の場合、UIViewのライフサイクルに沿った実装になっていないことが多い。
でも、改修が困難なこともあるので。
対応策
あまりやりたくはないが、以下の方法がある。
-
collectionView.reloadData()
を呼ぶ -
collectionView.collectionViewLayout.invalidateLayout()
を呼ぶ
呼ぶタイミングは、
viewWillAppear(_:)
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()
}
以上
こんな感じで、レイアウト崩れにはとりあえず対応できるんじゃないかと!