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などで遷移すると高さがずれる
セルの選択で別画面を表示したりすると,戻ってきた時にスクロール位置がずれます.
厳密にいうと各セルの高さ計算がずれており,これによってスクロール位置がずれるのです.
じゃぁどうするの?
セルの要素をクリアするメソッドを追加して,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
でも同様の呼び出し方でちゃんと高さ計算をしてくれます.
このあたりは,ちゃんと考えないと,どうしてもセルの高さがずれたり,スクロールするとずれたりってことが起こってしまうので大変ですよね.