ストーリーボードを使わないTableViewの配置とカスタムCellの高さ自動調整

個人開発したアプリの宣伝
目的地が設定できる手帳のような使い心地のTODOアプリを公開しています。
Todo with Location

Todo with Location

  • Yoshiko Ichikawa
  • Productivity
  • Free

以前にもTableViewCellの高さ自動調整について書いたけど、今度はStoryboard、autolayoutを使わずにSwiftコードだけでやってみた。

www.letitride.jp


いくつかの手順に分けて紹介していきます。

カスタムセルクラスの作成

例として、UILabelを配置したカスタムセルを定義する。

import UIKit

class CustomListCell: UITableViewCell{
    static let nameLabelFrame = CGRect(x: 10, y: 10, width: 300, height: 0)
    let nameLabel = UILabel(frame: CustomListCell.nameLabelFrame)

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        nameLabel.numberOfLines = 0
        nameLabel.lineBreakMode = .byWordWrapping
        self.contentView.addSubview(nameLabel)
    }
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

後述するCellの高さ自動調節をするため、

static let nameLabelFrame = CGRect(x: 10, y: 10, width: 300, height: 0)
let nameLabel = UILabel(frame: CustomListCell.nameLabelFrame)
nameLabel.numberOfLines = 0
nameLabel.lineBreakMode = .byWordWrapping

とし、width:300で折り返すようなUILabelにしています。

また、UITableViewCellはself.addSubviewではなく、self.contentView.addSubviewなので地味に注意。

このクラスがcellForRowAtで返されるcellインスタンスとなる。

TableViewの配置
import UIKit
class ViewController: UIViewController {
    
    let contents = [  "short","midlemidlemidlemidlemidlemidlemidlemidle","longlonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglonglong"]

    override func viewDidLoad() {
        super.viewDidLoad()
        let tableView = UITableView(frame: CGRect(x: 0, y: 0, width: self.view.frame.width, height: self.view.frame.height))
        tableView.register(
             CustomListCell.self,
             forCellReuseIdentifier: "productCell"
        )
        tableView.delegate = self
        tableView.dataSource = self
        self.view.addSubview(tableView)
    }
}

tableView.registerで配置済みcellを再利用する際のdequeue、identifierを設定できる。

UITableViewDelegate、UITableViewDataSourceの実装
extension ViewController: UITableViewDelegate, UITableViewDataSource{
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 3
    }
    
    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: "productCell", for: indexPath) as! CustomListCell         
        cell.nameLabel.text = contents[indexPath.row]
        cell.nameLabel.sizeToFit()

        return cell
    }
}

で、ここまででカスタムCellのnameLabelにindexPathに応じたテキストがwidth:300で折り返し表示される。

但し、当然、セルの高さは調整されないので、セルに収まらず、Labelが突き抜けて表示される。

高さを返すメソッドを実装して可変にする必要がある。

セルの高さの自動調整

以前にも書いた通りAutoLayoutを使わないのであれば、heightForRowAtを実装してcellの高さを返す必要がある。

単純にカスタムcellのラベルの高さ + margin分を返せば良いのだけど、heightForRowAtcellForRowAtより先に実行されるようで、

  • この時点ではdequeueReusableCellでカスタムセルインスタンスを取得できない
  • let cell = tableView.cellForRow(at: indexPath) as! CustomListCellではCustomListCellにダウンキャストできない
  • 都度、CustomListCellのインスタンスを作成してLabelの高さを調べるとdequeueでcellを再利用する恩恵が薄れてしまう(メソッド内で破棄されるインスタンスなのでオーバーヘッドは少ないかも)
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    let cell = CustomListCell()
    cell.nameLabel.text = contents[indexPath.row]
    cell.nameLabel.sizeToFit()
    return cell.nameLabel.frame.height
}

などの問題があった。

で、自分が取った策は、CustomListCellクラス内に高さを図る為のモック的なUILabelを別にstaticで持てばいいじゃん。という方法。

前提としてモック的なUILabelは、cellに配置されるUILabelと同じRect定義でならなければならない。

import UIKit
class CustomListCell: UITableViewCell{
    static let nameLabelFrame = CGRect(x: 10, y: 10, width: 300, height: 0)
    let nameLabel = UILabel(frame: CustomListCell.nameLabelFrame)
    //高さ計算に使用するモックラベル
    static let mocLabel = UILabel(frame: nameLabelFrame)

    override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        nameLabel.numberOfLines = 0
        nameLabel.lineBreakMode = .byWordWrapping
        self.contentView.addSubview(nameLabel)
    }
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    //渡された文字列からLabelの高さを返す
    static func getLabelHeight(_ value:String)->CGFloat{
        mocLabel.frame = CustomListCell.nameLabelFrame
        mocLabel.numberOfLines = 0
        mocLabel.lineBreakMode = .byWordWrapping
        mocLabel.text = value
        mocLabel.sizeToFit()
        //maxYを返すのはposition.y分のマージンを含む為
        return mocLabel.frame.maxY
    }
}

これでcellインスタンスの生成なしでLabelの高さを返すメソッドが出来た。

あとは、heightForRowAtを実装すれば良い。

extension ViewController: UITableViewDelegate, UITableViewDataSource{
    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return CustomListCell.getLabelHeight(contents[indexPath.row])
    }

    ...略
}