UITableViewCellをカスタマイズしたときのheight計算

iOSでオリジナルのUITableViewCellを使っていると,やがてセルごとに高さを変えたくなる事案が発生します.
UITableViewControllerにはheightForRowAtIndexというメソッドが用意されており,これをオーバーライドすることでセルごとの高さを調節できます.


では,UITableViewCellを使ってカスタムセルを作っていた場合は?

Cellの高さだけを計算するメソッドを用意する必要があります.

最初のころは,これをインスタンス変数を用いて行っていました.

つまり,

class OriginalCell: UItableViewCell {
    var totalHeight: CGFloat!

    /* -- initなどは省略 -- */

    func configureCell() {

	/*-- ここでセルのデザイン設定 -- */
	/*-- 要素からtotalHeightの計算 -- */
    }

    func cellHeight() -> CGFloat {
        height = CGFloat(60.0)
	if (self.totalHeight > 60) {
	    height = self.totalHeight
	}
	return height
    }
}

としておいて,heightForRowAtIndex側からは

override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    var cell: OriginalCell
    /*-- cellのインスタンス取り出し --*/

    return cell.cellHeight()
}


としていました.


この手法,確かにcellの高さはカスタムになるのですが,重大な問題があります.

didSelectなどで遷移すると高さがずれる


セルの選択で別画面を表示したりすると,戻ってきた時にスクロール位置がずれます.
厳密にいうと各セルの高さ計算がずれており,これによってスクロール位置がずれるのです.

なぜ?

ひとえに,UITableViewCellは使いまわされるというのが一番の要因です.


このようなカスタムセルを作る場合,ながーいテーブルを作ってみればわかりますが,普通に作ると高さがうまく計算されるか否か,ということ以前に,セルに正しい情報が表示されません.


これはTableViewがセルを使いまわすため,インスタンス複数個生成しても,そのインスタンスごとに管理はしてくれないのです.

つまり,100個のセルを持つテーブルを生成したとしても,OriginalCellインスタンスは100個生成されないのです.


なぜかといえば簡単で,それをやるとテーブルがぬるぬる動かないからです.

じゃぁどうするの?

セルの要素をクリアするメソッドを追加して,cellForRowAtIndexで呼び出されるときに,セルの中身をクリアして再描画してやります.

再描画といっても,configureCell()を呼ぶだけですが.
問題は,他のインスタンスを使いまわしているので,そっちで描画されていたものをクリアしないといけないってことです.

class OriginalCell: UITableViewCell {
    var profileImage: UIImageView!

    /*-- 省略 --*/

    func cleanCell() {
        if (self.profileImage != nil) {
	    self.profileImage.removeFromSuperview()
        }
	self.profileImage = nil
    }

    func configureCell() {
	self.profileImage = UIImageView()
	/*-- profileImageの描画処理 --*/
    }
}

このcleanCell()というメソッドcellForRowAtIndexで,cellを描画させる前に呼び出します.


インスタンスが保持されていないなら,totalHeightは意味をなさない


というわけなので,インスタンス変数に高さを格納しておいても,インスタンスが生きている分に関しては正確な計算がされるわけですが…….


実はテーブルがスクロールされたり,遷移されたりして,「スクロール位置の計算」をしなければならないときは,heightForRowAtIndexは呼ばれていません.

これはあくまで表示されているセルの高さを返すメソッドであり,表示されていない部分のセルの高さに関しては関与してくれません.

じゃぁ何が呼ばれているの?

estimatedHeightForRowAtIndexというメソッドがあります.

これが非表示部分の高さを計算していて,スクロール位置を算出するときに使われているメソッドの正体です.


ここで,正確な高さを計算してやれば良いわけです.

でも,ちょっとまって,さっき実装したOriginalCellクラスはインスタンス変数で高さを保持したよね?

これって表示されてない部分のインスタンスは,使いまわせれて表示されている部分のインスタンスに成り代わっているので……役に立たないよ!


かといってこの数だけインスタンスを生成していたら,「じゃぁなんのためにUITableViewはこんなに頑張ってセルを使いまわしてるんだよ」っていう話になります.


じゃぁクラスメソッドに積もう


要は高さだけ計算してくれればいいわけです.
というわけで,totalHeightを廃止して,クラスメソッドで高さ計算をしてやりましょう.

class OriginalCell: UITableViewCell {
    class func estimateCellHeight() -> CGFloat {
        /*-- dummyでラベルを作って高さ計算 --*/
	var dummyLabel = UIImageView(frame CGRectMake(0, 0, 50, 50))

	return dummyLabel.frame.origin.y + dummyLabel.frame.size.height
    }
}

で,これをestimatedHeightForRowAtIndexで呼び出してやれば,正確に高さが計算されるので,遷移してもずれないTableViewを作ることができます.

override func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
    var height: CGFloat!
    height = OriginalCell.estimateCellHeight(self.cellData[indexPath.row] as NSDictionary)
    return height
}


もちろん,同じメソッドを使いまわせるならheightForRowAtIndexでも同様の呼び出し方でちゃんと高さ計算をしてくれます.


このあたりは,ちゃんと考えないと,どうしてもセルの高さがずれたり,スクロールするとずれたりってことが起こってしまうので大変ですよね.