📲

セルを生成する際にsizeForItemAtをCGSize.zeroにするとcellForItemAtの挙動が変わる

6 min read

はじめに

UICollectionViewのセルを生成する際,サイズにCGSize.zeroを指定するとセルのインスタンスを生成するcollectionView(_:cellForItemAt:)の挙動が変わるのでまとめてみました.

この記事は2019/12/23に執筆したものを転載したものなります。

また、以下の内容はiOS13.0での検証になります。

検証

検証のために画面中央にCollectionViewを配置した簡単なViewControllerを用います.

今回は,

  • CollectionViewCellというUICollectionViewCellのカスタムクラスを使用
  • UICollectionViewDelegateFlowLayoutのcollectionView(_:sizeForItemAt:)でサイズを返却する

という状況を想定します.

import UIKit

class ViewController: UIViewController {
    
    @IBOutlet weak var collectionView: UICollectionView! {
        didSet {
            self.collectionView.dataSource = self
            self.collectionView.delegate = self
        }
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }
}

extension ViewController: UICollectionViewDataSource {
    func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return 10
    }
    
    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "CollectionViewCell", for: indexPath) as! CollectionViewCell
        cell.label.text = indexPath.debugDescription
        print("cellForItemAt: \(indexPath)")
        return cell
    }
}

extension ViewController: UICollectionViewDelegateFlowLayout {
    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        
        print("sizeForItemAt: \(indexPath)")
        return CGSize.zero //ここでindexPathに対応するセルのサイズを返却
    }
}

CGSize(width: 70.0, height: 70.0)を返す場合

通常の固定サイズを返却する場合です.collectionView(_:sizeForItemAt:)が常にCGSize(width: 70.0, height: 70.0)を返すようにします.実際に実行すると想定されているように10個のセルがきちんと表示されています.

コンソールは出力は以下のようになります.

sizeForItemAt: [0, 0]
sizeForItemAt: [0, 1]
sizeForItemAt: [0, 2]
sizeForItemAt: [0, 3]
sizeForItemAt: [0, 4]
sizeForItemAt: [0, 5]
sizeForItemAt: [0, 6]
sizeForItemAt: [0, 7]
sizeForItemAt: [0, 8]
sizeForItemAt: [0, 9]
cellForItemAt: [0, 0]
cellForItemAt: [0, 1]
cellForItemAt: [0, 2]
cellForItemAt: [0, 3]
cellForItemAt: [0, 4]
cellForItemAt: [0, 5]
cellForItemAt: [0, 6]
cellForItemAt: [0, 7]
cellForItemAt: [0, 8]
cellForItemAt: [0, 9]

この出力を見てわかるように

  1. 全てのindexPathについてsizeForItemAt (サイズ計算)を実行
  2. 全てのindexPathに対してcellForItemAt (インスタンス生成)を実行

の順に処理が行われます.

sizeForItemAt→cellForItemAtがindexPath毎に逐一実行されるというわけではないんですね.

セルサイズの一部がCGSize.zeroを含む場合

次に特定のindexPathに対して,sizeForItemがCGSize.zeroを返却する場合を考えます.collectionView(_:sizeForItemAt:)を以下のように書き換えました.

func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {

    print("sizeForItemAt: \(indexPath)")
    if indexPath.item % 2 == 0 {
      return CGSize.zero
    } else {
      return CGSize(width: 70.0, height: 70.0)
    }
}

この場合で実行すると以下のように画面は表示されます.先ほどの条件と比べて,CGSize.zeroを当ててられたセルは表示されず,表示されたセルの配置も先ほどとズレてしまいます.

コンソールの出力を確認するとWarningが出力されています.


sizeForItemAt: [0, 0]
sizeForItemAt: [0, 1]
sizeForItemAt: [0, 2]
sizeForItemAt: [0, 3]
sizeForItemAt: [0, 4]
sizeForItemAt: [0, 5]
sizeForItemAt: [0, 6]
sizeForItemAt: [0, 7]
sizeForItemAt: [0, 8]
sizeForItemAt: [0, 9]
cellForItemAt: [0, 0]
XXXX-XX-XX XX:XX:XX CollectionViewTest[62020:545496] [Warning] Warning once only: Detected a case where constraints ambiguously suggest a size of zero for a collection view cell's content view. We're considering the collapse unintentional and using standard size instead. Cell: <CollectionViewTest.CollectionViewCell: 0x7fa5edd18890; baseClass = UICollectionViewCell; frame = (0 35; 0 0); clipsToBounds = YES; opaque = NO; layer = <CALayer: 0x600003c01460>>
cellForItemAt: [0, 1]
cellForItemAt: [0, 2]
cellForItemAt: [0, 3]
cellForItemAt: [0, 4]
cellForItemAt: [0, 5]
cellForItemAt: [0, 6]
cellForItemAt: [0, 7]
cellForItemAt: [0, 8]
cellForItemAt: [0, 9]

これはCGSize.zeroをCollectionViewCellに対して割り当てたので,意図しない表示崩れを防ぐためにframe = (0 35; 0 0)を当てたというものです.

この処理が入ったため,[0,1]のセルを生成する際,画面上には[0,0]のセルが表示されていないものの,実在しているとみなされ,セルの間のスペースが取られた状態で描画処理が行われているものと考えられます.(要検証)

いずれにせよ,sizeForItemAt→cellForItemAtのライフサイクルは通常の場合と変わりません.

セルサイズが全てCGSize.zeroの場合

最後にcollectionView(_:sizeForItemAt:)が常にCGSize.zeroを返す場合です.全てのセルが表示されていません.

セルサイズが全てCGSize.zeroの場合,上記の2つとは異なり,cellForItemAtが実行されません.

sizeForItemAt: [0, 0]
sizeForItemAt: [0, 1]
sizeForItemAt: [0, 2]
sizeForItemAt: [0, 3]
sizeForItemAt: [0, 4]
sizeForItemAt: [0, 5]
sizeForItemAt: [0, 6]
sizeForItemAt: [0, 7]
sizeForItemAt: [0, 8]
sizeForItemAt: [0, 9]

すなわち,CollectionViewが含むセルのサイズが全てCGSize.zeroだった場合はセルのインスタンスの生成を行わないということになります.同じcollectionView(_:sizeForItemAt:)がCGSize.zeroを返す場合であっても,インスタンス自体は存在するが描画はされない「セルサイズの一部がCGSize.zeroを含む場合」とは挙動が異なります.

参考

Discussion

ログインするとコメントできます