【Swift】横スクロールの可変UICollectionViewCellを実装するときの注意点【Self-sizing】
どうも。Reoです。2020年初記事です。あけましておめでとうございます。
今回は、可変セルを利用した横スクロールの UICollectionView を実装した際に、contentSize が思った通りにならなかった問題について書いていきます。
GitHubにサンプルプロジェクトを用意しました。
uruly/SelfSizingCollectionViewDemo
環境
- Xcode 11.3
- Swift 5.1
- iOS 12.0 ~ 13.3
Development target を iOS12.0 からに設定してあります。
何がおかしい?
可変セルを利用した横スクロールのUICollectionViewで、minimumLineSpacing と minimumInteritemSpacing で異なる値を指定すると contentSize がおかしくなります。
minimumInteritemSpacing は行間、minimuLineSpacing は列間の余白になります。詳しくは、以前書いた「【Swift】 UICollectionViewFlowLayoutの余白調整について」を参照してください。
上から collectionView.contentSize が
- (848.5714285714286, 50.0)
- (905.3333333333333, 50.0)
- (688.5714285714286, 50.0)
となっています。
本来、1と3は同じcontentSizeになるはずだと思うのに、3は最後までセルが表示できなくなってしまっています。
Scroll Direction が Vertical の時は正しく動作しています。
発生条件
こちらの発生条件は、
- collectionView の scrollDirection が horizontal である。
- minimumLineSpacing と minimumInteritemSpacing で違う値を設定している。
- Self-sizing を利用している。
の3点です。
解決方法
解決方法は、「
例えばセルとセルの間を20px空けたい場合、
- minimumLineSpacing = 20
- minimumInteritemSpacing = 20
に設定します。
以下のように異なる値をいれてしまうと、うまくいきません。
- minimumLineSpacing = 0
- minimumInteritemSpacing = 20
xib で指定する場合も同様に Min Spacing For Cells と For Lines に同じ値をいれないと上手くいきません。
これだけといえばこれだけなんですけど、自分はかなりハマってしまいました。行間の余白を適当に異なる値に設定してたことが原因なんて、こんなのわからんよ...
本題は以上です。残りはおまけ。
実装方法
せっかくサンプルプロジェクトを作ったので、実装方法を紹介していこうと思います。
カスタムセルを用意
まずは、適当にHorizontalCollectionViewCellというカスタムセルを用意しました。
作ったセルにUIViewとUILabelを以下のように配置します。
UILabel の numberOfLines は 1 になっています。なので高さは固定されます。
width は可変して欲しいので、固定の値は設定していません。
UILabel はコードの方と繋げておきます。
import UIKit
final class HorizontalCollectionViewCell: UICollectionViewCell {
@IBOutlet private weak var textLabel: UILabel!
func configure(text: String) {
textLabel.text = text
}
}
このSelf-sizing には、iOS12 の場合にのみ発生するバグがあります。(参照 UICollectionViewFlowLayoutのestimatedItemSizeを指定するとiOS12で表示がおかしい)
iOS12.1とかでは直っているんですが、iOS12 をサポートする場合には対応しておきましょう。
import UIKit
final class HorizontalCollectionViewCell: UICollectionViewCell {
@IBOutlet private weak var textLabel: UILabel!
override func awakeFromNib() {
super.awakeFromNib()
activationSelfSizing()
}
func configure(text: String) {
textLabel.text = text
}
private func activationSelfSizing() {
// iOS12 のときに Self-sizing が有効にならない対策
contentView.translatesAutoresizingMaskIntoConstraints = false
let leftConstraint = contentView.leftAnchor.constraint(equalTo: leftAnchor)
let rightConstraint = contentView.rightAnchor.constraint(equalTo: rightAnchor)
let topConstraint = contentView.topAnchor.constraint(equalTo: topAnchor)
let bottomConstraint = contentView.bottomAnchor.constraint(equalTo: bottomAnchor)
NSLayoutConstraint.activate([leftConstraint, rightConstraint, topConstraint, bottomConstraint])
}
}
地味に厄介。
ちなみにこのバグは、scrollDirection が vertical の時にも発生します。
UIViewController に UICollectionView を設置
適当に UICollectionView を設置した UIViewController を設置します。
適当に高さ50に指定したUICollectionViewを設置しました。
Scroll Direction を Horizontal にする
冒頭のバグにも対応するために、Min Spacing For Cells と For Lines には同じ値を入れておきます。
可変セルにするために、Estimate Size を Automatic に設定します。
あとは、UIViewControllerに繋げて、dataSourceやセルの設定をしていきます。
import UIKit
final class HorizontalCollectionViewController: UIViewController {
@IBOutlet private var collectionView: UICollectionView! {
didSet {
collectionView.delegate = self
collectionView.dataSource = self
collectionView.register(UINib(nibName: "HorizontalCollectionViewCell", bundle: nil), forCellWithReuseIdentifier: reuseIdentifier)
// iOS12ではxibでの設定だけで動くが、iOS13ではコードで設定しないと可変にならない
if let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
layout.invalidateLayout()
}
}
}
private let reuseIdentifier = "cell"
private let animals: [String] = ["cat", "kangaroo", "seal", "dog", "panda", "giraffe", "penguin", "zebra", "hippopotamus"]
}
// MARK: - UICollectionViewDataSource
extension HorizontalCollectionViewController: UICollectionViewDataSource {
func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
return animals.count
}
func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
let cell = collectionView.dequeueReusableCell(withReuseIdentifier: reuseIdentifier, for: indexPath) as! HorizontalCollectionViewCell
cell.configure(text: animals[indexPath.row])
return cell
}
}
// MARK: - UICollectionViewDelegate
extension HorizontalCollectionViewController: UICollectionViewDelegate {}
iOS12 では以下のコードがなくても可変セルにできますが、iOS13だと以下のコードがないと可変セルになってくれません...
if let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout {
layout.estimatedItemSize = UICollectionViewFlowLayout.automaticSize
layout.invalidateLayout()
}
こちらを書けば xib 上の Estimate size の設定はしてなくても大丈夫です。OS特有のバグ(?)が多くてツライ...
これでできているはず!!!!!以上です!!!
おわりに
サンプルプロジェクトには、scrollDirection が vertical の時のレイアウトを確認するために、vertical バージョンの可変セルも実装してあります。でもセルの軸が真ん中寄せになっちゃてるので実用的ではないかなぁとは思います。
uruly/SelfSizingCollectionViewDemo
実はこれ自分が勘違いしてるだけで仕様とかだったりしないよね...
お仕事でハマってた時は RxSwift や RxDataSources を使ってるせいか?とかも考えましたが、最終的に色々いじってたらいつの間にか直ってた... になりました。
diff を見て頑張って原因を探ってようやく発見したものになります。
検索して見つからなかったと思うので、誰もこんなことにハマってないんかなぁ。仕様なんかなぁ。不思議。みたいなお気持ちです。
それではでは〜。ノシ
コメントはありません。
現在コメントフォームは工事中です。