Irohabook
0
1316

SwiftのUICollectionViewにおけるcellForItemAtとsizeForItemAtの順番:セルの高さを動的に変えるための予備知識

UICollectionViewを理解するには、cellForItemAtとsizeForItemAtがどの順番で実行されるかを把握しないといけない。

多くのコードではviewDidLoadで

collectionView.dataSource = self
collectionView.delegate = self
view.addSubview(collectionView)

とする。その後、データをどこからか取得して、セルに入れていく。そしてお決まりの「reloadData」を行う。

多くの参考書にあるように、UICollectionViewを扱うときはreloadDataを必ず実行しなければいけない。これらの手順は次のようにまとめられる。

  1. とりあえずセルを描画する
  2. データをセルに入れる
  3. セルを再描画する

もう少し詳しく流れをおさえる

どこかのウェブサイトから検索したアイテムを表示するようなプログラムを想定する。ヘッダーセルは検索結果に関係なく、固定されている。通常セルは検索結果にあるそれぞれのアイテムを表す。

今、ヘッダーセルを3個、通常セルを7個用意する。このとき、次の順番でプログラムが実行される。

dataSource
delegate

sizeForItemAt 1
sizeForItemAt 2
sizeForItemAt 3

cellForItemAt 1
cellForItemAt 2
cellForItemAt 3

reloadData

sizeForItemAt 1
sizeForItemAt 2
sizeForItemAt 3
...
sizeForItemAt 9
sizeForItemAt 10

cellForItemAt 1
cellForItemAt 2
cellForItemAt 3
...
cellForItemAt 9
cellForItemAt 10

ポイントはsizeForItemAtのあとにcellForItemAtがくること。このポイントをわかっているかわかっていないかで、セルの高さを調節するという最もポピュラーで最も難しい難所をほんの少しだけ解決できる。

reloadData前は、プログラムはセルは用意するものの、そこにどんなデータが入ってくるかわからない。1000行もあるテキストかもしれないし、画像かもしれない。とりあえずセルだけは準備しないといけないので、幅や高さはズボラに設定してもあまり問題ない。そして、カスタムセルの内部で高さを設定していれば、その高さがこの時点で反映される。

reloadData後が問題である。

reloadDataすると、いよいよセルにデータが入ってきて、画面が一気に更新される。

挙動不審なセルの高さ

2番目のセルにUITextViewが入っているとしよう。中身が変われば高さが変わるという厄介なviewである。これはあるカスタムセルに入っているとする。

カスタムセル:

var descriptionView: UITextView!

override init(frame: CGRect) {
    super.init(frame: frame)
    self.backgroundColor = UIColor.white
    print(frame.size)
    descriptionView = UITextView(frame: CGRect(x: 0, y: 0, width: frame.width, height: frame.height))
    descriptionView.font = UIFont.systemFont(ofSize: 16)
    descriptionView.isEditable = false
    descriptionView.isScrollEnabled = false

    addSubview(descriptionView)
}

required init?(coder aDecoder: NSCoder) {
    fatalError("init(coder:) has not been implemented")
}

呼び出しもと(viewDidLoad):

let width: CGFloat = self.view.frame.width - 2 * padding
let size = CGSize(width: width, height: 1000)
let attributes = [NSAttributedStringKey.font: UIFont.systemFont(ofSize: 16)]
let boundingRect = NSString(string: groupJSON["description"].stringValue).boundingRect(with: size, options: .usesLineFragmentOrigin, attributes: attributes, context: nil)
print("boundingRect")
print(boundingRect.height)
let height: CGFloat = boundingRect.height + 48
return CGSize(width: width, height: height)

呼び出しもとでは、そのviewに入ってきた文章量に応じて高さを変えている。上のコードはスタックオーバーフローなどでよく知られている有名なコードだ。boundingRectを使うことがポイントになる。

boundingRectを使って、中身のテキスト量に依存する高さを取得する。

reloadDataする前はデータが入っていないため、高さは0に近い(実際は0でない。この辺りの原因もよくわからない)。

reloadDataするとデータが入ってきて、上のコードが動的な高さを計算し、セルの高さを自動的に変えてくれる。しかしboundingRectは、テキストが多かったり少なかったりすると高さが合わないという問題がある。

reloadDataして高さを自動的に変えるというプログラムは、いまだに多くの開発者を苦しめているようだ。スタックオーバーフローやYouTubeの解説動画を含めて、情報が非常に少ない。この辺りは開発者が知識を積極的に発信するしかない。

次の記事

UICollectionView