【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の余白調整について」を参照してください。

 

scrollDirection = .horizontal

上から collectionView.contentSize が

  1. (848.5714285714286, 50.0)
  2. (905.3333333333333, 50.0)
  3. (688.5714285714286, 50.0)

となっています。

本来、1と3は同じcontentSizeになるはずだと思うのに、3は最後までセルが表示できなくなってしまっています。

 

Scroll Direction が Vertical の時は正しく動作しています。

scrollDirection = .vertical

 

発生条件

こちらの発生条件は、

  1. collectionView の scrollDirection が horizontal である。
  2. minimumLineSpacing と minimumInteritemSpacing で違う値を設定している。
  3. Self-sizing を利用している。

の3点です。

 

解決方法

解決方法は、「minimumLineSpacing と minimumInteritemSpacing に同じ値を設定する」です。

例えばセルとセルの間を20px空けたい場合、

  • minimumLineSpacing = 20
  • minimumInteritemSpacing = 20

に設定します。

 

以下のように異なる値をいれてしまうと、うまくいきません。

  • minimumLineSpacing = 0
  • minimumInteritemSpacing = 20

 

xib で指定する場合も同様に Min Spacing For Cells と For Lines に同じ値をいれないと上手くいきません。

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 を見て頑張って原因を探ってようやく発見したものになります。

 

検索して見つからなかったと思うので、誰もこんなことにハマってないんかなぁ。仕様なんかなぁ。不思議。みたいなお気持ちです。

 

それではでは〜。ノシ

コメントは認証制です。詳しくは下記の注意をお読みください。お気軽にコメントお願いします!

Write a Comment

コメント時の注意

「Twitter」「Facebook」「Google+」「WordPress」のいずれかのアカウントをお持ちの方は各アカウントと連携することでコメントできます。 コメントしたことはSNSに流れませんので、アカウントをお持ちの方はこちらの方法でコメントを投稿して下さると嬉しいです。 アカウントをお持ちでない方はメールアドレスで投稿することができます。 初回コメント時は承認後に表示されます。

Related Memo...

UITableView.RowAnimation の .none はアニメーションするよ

UITableView.RowAnimation の .none はアニメーションがnoneなわけじゃなく、デフォルトの設定を使うよという意味らしい。

The inserted or deleted rows use the default animations.

なのでアニメーションしちゃう。今更の気づき。

 

iOS

UINavigationController + UIScrollView の組み合わせで使っている時に謎の余白ができる時

UINavigationController + UIScrollView の組み合わせで使っていて、UIScrollView 上に AutoLayout で上下左右0で View を設置しているのに、30px程度上にずれてしまうとき。

`navigationController.navigationBar.isTranslucent = false` にすると直るかもしれない。

ScrollView上のコンテンツとNavigationBarの重なっているところが透過していたら多分これで直せるはず。

通常のターゲットではちゃんと動いているのに、iOSSnapshotTestCase を用いたテストでだけこの対応が必要なのよくわからないけれど。。。

iOS

記事を書くほどでもないけれどメモっておきたいこと

テスト投稿。

例えばiphone7 の画面サイズ

750 × 1334
半分375 × 667

iOS
more