Posted at

cellForRowAtで高さの確定するUITableViewCellの高さをUITableViewAutomaticDimensionで確定する

More than 1 year has passed since last update.

UITableViewCellの高さをAutolayoutで確定させる場合、UITableViewの高さ系のdataSourceにUITableViewAutomaticDimensionを指定します。

この方法の問題は、

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell

の中で高さの変わりうる値を渡した場合にその値を考慮しない高さになることです。

例えば複数行を想定したUILabelのあるUITableViewを作った場合

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

let cell = tableView.dequeueReusableCell(indexPath: indexPath)!
cell.label = multiLineText
return cell
}

のように指定しても、セルの高さは1行の時の高さが返ってしまいます。

このようになるのは、cell.labelの代入タイミングから高さの計算タイミングまでにセルのレイアウトが更新されないことにあります。

多くの場合は

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

let cell = tableView.dequeueReusableCell(indexPath: indexPath)!
cell.label = multiLineText
cell.layoutIfNeeded()
return cell
}

のようにcellの代入タイミングで更新をかけていると思います。

しかし、この方法にも問題があります。

それは一回目の横幅が想定するよりも小さい値になることです。

実際にprintを仕込むと分かりやすいのですが、

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

let cell = tableView.dequeueReusableCell(indexPath: indexPath)!
cell.label = multiLineText
print(cell.bounds)
print(cell.contentView.bounds)
cell.layoutIfNeeded()
print(cell.bounds)
print(cell.contentView.bounds)
return cell
}

のログを見ると

//1回目

(0.0, 0.0, 320.0, 48.0)
(0.0, 0.0, 320.0, 47.6666666666667)
//2回目
(0.0, 0.0, 414.0, 45.0)
(0.0, 0.0, 414.0, 44.6666666666667)

と表示されています。(iPhone7p)

つまり一回目は横幅320px、2回目は414pxの横幅として計算されているのです。

これが一見問題にならないのは、高さは3px程しか変わらないためです。

より高いセルやiPadでの検証時には気がつくことが多いと思います。

これはdequeueReusableCellが初回は生成、2回目からはリサイクルをする挙動のためで生成直後はTableViewに貼られていないのでlayoutIfNeededを呼んでも意図したサイズにならないことが原因です。


解決策

要するにはセルの生成から高さの計算までの間にセルを正しいサイズにすれば良いのですが、それを行えるのがUItableViewCell側の

systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize

になります。

このメソッドはautolayout計算後のサイズを返すメソッドで、これをorverrideしサイズを計算する前に横幅を合わせてあげれば良いわけです。

具体的には以下のようになります。

  override func systemLayoutSizeFitting(_ targetSize: CGSize, withHorizontalFittingPriority horizontalFittingPriority: UILayoutPriority, verticalFittingPriority: UILayoutPriority) -> CGSize {

contentView.bounds.size = targetSize
contentView.layoutIfNeeded()
return super.systemLayoutSizeFitting(targetSize,
withHorizontalFittingPriority: horizontalFittingPriority,
verticalFittingPriority: verticalFittingPriority)
}

targetSizeの横幅はセルの乗るUITableViewの横幅を含んでいるので一度このサイズにcontentViewをセットしたあと、layoutIfNeededを呼びます。

この時点で正しいセルのサイズになっているので、ここでsuperのsystemLayoutSizeFittingを呼べば正しく計算されたサイズが返されます。

UITableViewAutomaticDimensionはこの値を参照して高さを計算するので、これで意図した動作になるはずです。