UITableView inside UITableViewCell with dynamic height

Viewed 14879

I need to put UITableView inside UITableViewCell with auto-layout because second table has different number of rows and one row can have different height.
This is my ViewController

class ViewController: UIViewController {

    let tableView = UITableView()
    let cellId = "firstTableCellId"

    override func viewDidLoad() {
        super.viewDidLoad()
        setupView()
        tableView.reloadData()
        view.backgroundColor = UIColor.gray
    }

    func setupView() {
        tableView.delegate = self
        tableView.dataSource = self
        tableView.separatorStyle = .none
        tableView.register(NextTable.self, forCellReuseIdentifier: cellId)
        tableView.backgroundColor = UIColor.green
        tableView.separatorStyle = .singleLine

        view.addSubview(tableView)
        view.addConstraintsWithFormat("V:|-60-[v0]-5-|", views: tableView)
        view.addConstraintsWithFormat("H:|-8-[v0]-8-|", views: tableView)
    }
}

extension ViewController: UITableViewDelegate {

}

extension ViewController: UITableViewDataSource {
    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 2
    }

    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return UITableViewAutomaticDimension
    }

    func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
        return 80
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath) as! NextTable
        cell.layoutIfNeeded()
        return cell
    }
}

And NextTable which is the cell in first table

class NextTable: UITableViewCell {

    var myTableView: UITableView!
    let cellId = "nextTableCellId"

    override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        backgroundColor = UIColor.brown
        setupView()
        myTableView.reloadData()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }

    let label: UILabel = {
        let label = UILabel()
        label.numberOfLines =  0
        label.lineBreakMode = .byWordWrapping
        label.text = "Next table:"
        label.textColor = UIColor.black
        label.sizeToFit()
        label.backgroundColor = UIColor.cyan
        return label
    }()

    func setupView() {
        myTableView = UITableView()
        myTableView.delegate = self
        myTableView.dataSource = self
        myTableView.separatorStyle = .singleLineEtched
        myTableView.backgroundColor = UIColor.blue
        myTableView.register(TableCell.self, forCellReuseIdentifier: cellId)
        myTableView.isScrollEnabled = false

        addSubview(myTableView)
        addSubview(label)
        addConstraintsWithFormat("H:|-30-[v0]-30-|", views: myTableView)
        addConstraintsWithFormat("H:|-30-[v0]-30-|", views: label)

        addConstraint(NSLayoutConstraint(item: label, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1.0, constant: 15))
        addConstraint(NSLayoutConstraint(item: myTableView, attribute: .top, relatedBy: .equal, toItem: label, attribute: .bottom, multiplier: 1.0, constant: 0))
        addConstraint(NSLayoutConstraint(item: label, attribute: .bottom, relatedBy: .equal, toItem: myTableView, attribute: .top, multiplier: 1.0, constant: 0))
        addConstraint(NSLayoutConstraint(item: myTableView, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1.0, constant: -15))
    }
}

extension NextTable: UITableViewDelegate {

}

extension NextTable: UITableViewDataSource {

    func numberOfSections(in tableView: UITableView) -> Int {
        return 1
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return 5
    }

    func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return UITableViewAutomaticDimension
    }

    func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat {
        return 60
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        let cell = tableView.dequeueReusableCell(withIdentifier: cellId, for: indexPath) as! TableCell
        cell.layoutIfNeeded()
        return cell
    }
}

And cell in second table

class TableCell: UITableViewCell {
    let label: UILabel = {
        let label = UILabel()
        label.numberOfLines =  0
        label.lineBreakMode = .byWordWrapping
        label.text = "Some text"
        label.textColor = UIColor.black
        label.sizeToFit()
        label.backgroundColor = UIColor.red
        return label
    }()

    override init(style: UITableViewCellStyle, reuseIdentifier: String?) {
        super.init(style: style, reuseIdentifier: reuseIdentifier)
        backgroundColor = UIColor.yellow
        setupView()
    }

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

    func setupView(){
        addSubview(label)
        addConstraintsWithFormat("V:|-8-[v0]-8-|", views: label)
        addConstraintsWithFormat("H:|-5-[v0]-5-|", views: label)
    }
}

This is the effect of my code Program result
There is no second table so I create new class and use it in cell as new table view

class InnerTableView: UITableView {
    override var intrinsicContentSize: CGSize {
        return self.contentSize
    }
}

And now table is show but size is too large Result of InnerTableView
What can I do to show full second table without empty space at bottom of cell.

5 Answers

I added override for property in my table view:

override var intrinsicContentSize: CGSize {
    self.layoutIfNeeded()
    return self.contentSize
}

Full code for my table in table can you find on my GitHub

I tried the above solution but it haven't worked for me. I have done the small change and then it worked fine for me.

A) In my ViewController:

//NOTE: TestQuestionAnsOptionCell is the outer cell

if let cell = tableView.dequeueReusableCell(withIdentifier: String(describing:TestQuestionAnsOptionCell.self), for: indexPath) as? TestQuestionAnsOptionCell {
            cell.reloadData()
            setUpInnerCellListner(cell: cell)
            cell.selectionStyle = .none
            return cell
  }


private func setUpInnerCellListner(cell:TestQuestionAnsOptionCell) {
    cell.reloadTable = { // Closure called from TestQuestionAnsOptionCell
        DispatchQueue.main.async {
            UIView.setAnimationsEnabled(false)
            self.tbQuestion.beginUpdates()
            self.tbQuestion.endUpdates()
            UIView.setAnimationsEnabled(true)
        }
    }
}

Extention used here:

extension NSObject {
class var className: String {
    return String(describing: self)
}
}

B) In TestQuestionAnsOptionCell:

class TestQuestionAnsOptionCell: UITableViewCell {

var tblOptions: OwnTableView = OwnTableView()
var reloadTable:(()->(Void))?

override func awakeFromNib() {
    super.awakeFromNib()
    setupView()
}

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
}

private func setupView() {
    tblOptions.estimatedRowHeight = 60.0
    tblOptions.rowHeight = UITableView.automaticDimension
    tblOptions.delegate = self
    tblOptions.dataSource = self
    tblOptions.separatorStyle = .none
    tblOptions.register(UINib(nibName: TestQuestionOptionCell.className, bundle: nil), forCellReuseIdentifier: TestQuestionOptionCell.className)
    tblOptions.isScrollEnabled = false
    
    addSubview(tblOptions)
    addConstraintsWithFormat("H:|-30-[v0]-30-|", views: tblOptions)
    
    addConstraint(NSLayoutConstraint(item: tblOptions, attribute: .top, relatedBy: .equal, toItem: self, attribute: .top, multiplier: 1.0, constant: 15))
    addConstraint(NSLayoutConstraint(item: tblOptions, attribute: .bottom, relatedBy: .equal, toItem: self, attribute: .bottom, multiplier: 1.0, constant: -15))
}

func reloadData()  {
    tblOptions.reloadData()
}

}

With TableView Delegate and datasource, I used the following:

extension TestQuestionAnsOptionCell:UITableViewDelegate, UITableViewDataSource {

func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    //NOTE: TestQuestionOptionCell is the inner cell

    if let cell = tableView.dequeueReusableCell(withIdentifier: TestQuestionOptionCell.className, for: indexPath) as? TestQuestionOptionCell {
        cell.updateCell()
        cell.selectionStyle = .none
        return cell
    }
    return UITableViewCell()
}

func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
    return UITableView.automaticDimension
}
 
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
    tblOptions.invalidateIntrinsicContentSize()
    tblOptions.layoutIfNeeded()
    reloadTable?()//Closure to update outer tableview
}

}

C) Inner tableview is used as:

class OwnTableView: UITableView {
override var intrinsicContentSize: CGSize {
    self.layoutIfNeeded()
    return self.contentSize
}

override var contentSize: CGSize {
    didSet{
        self.invalidateIntrinsicContentSize()
    }
}
}

D) UIView Extention is as:

extension UIView {

func addConstraintsWithFormat(_ format: String, views: UIView...) {
    var viewsDictionary = [String:UIView]()
    for(index, view) in views.enumerated() {
        let key = "v\(index)"
        view.translatesAutoresizingMaskIntoConstraints = false
        viewsDictionary[key] = view
    }
    addConstraints(NSLayoutConstraint.constraints(withVisualFormat: format, options: NSLayoutConstraint.FormatOptions(), metrics: nil, views: viewsDictionary ))
}
}

All you need is invalidate content size in side ParentTableView's cellForRowAt indexPath

parentCell.InsideTableview.invalidateIntrinsicContentSize()

I am posting another answer and I wish to make your life easier this time.

I have searched and tried everything I found on google. What worked for me was combination of what I found here and there + something weird I tried and worked: I cannot believe that this worked but it did guys.

1- Override the UITableView as mentioned in the answers to this question + override reloadData():

class InnerAutoTableView: UITableView {
    override var intrinsicContentSize: CGSize {
        self.layoutIfNeeded()
        return self.contentSize
    }
    
    override var contentSize: CGSize {
        didSet{
            self.invalidateIntrinsicContentSize()
        }
    }
    override func reloadData() {
      super.reloadData()
      self.invalidateIntrinsicContentSize()
    }
}

2- Set rowHeight to be automaticDimension and estimatedRowHeight to be 0:

private lazy var tableView: UITableView = {
    let tv = InnerAutoTableView()
    tv.register(ItemTableCell.self, forCellReuseIdentifier: "itemId")
    tv.delegate = self
    tv.dataSource = self
    tv.isScrollEnabled = false
    tv.rowHeight = UITableView.automaticDimension
    tv.estimatedRowHeight = 0
    return tv
}()

I tried this on iOS 15.4 simulator and real devices (iPhone 12 Pro Max + iPhone 12).

Related