Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

UITableView inside UITableViewCell with dynamic height

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.

like image 307
Michal Avatar asked Sep 27 '17 07:09

Michal


Video Answer


3 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

like image 120
Michal Avatar answered Oct 18 '22 18:10

Michal


In your first table view, add this to your cellForRowAt after you dequeue your cell (might need to be slightly tweaked to fit your implementation):

cell.tableView.reloadData()
DispatchQueue.main.async {
    cell.tableView.scrollToRow(at: IndexPath(row: cell.numberOfRowsInInnerTableView.count.count - 1, section: 0), at: .bottom, animated: false)
}
DispatchQueue.main.async {
    cell.tableView.invalidateIntrinsicContentSize()
    cell.tableView.layoutIfNeeded()
    self.updateHeight()
}

Then define a function updateHeight in the same class (outer table view):

func updateHeight() {
    UIView.setAnimationsEnabled(false)
    tableView.beginUpdates()
    tableView.endUpdates()
    UIView.setAnimationsEnabled(true)
}

Obviously, this is kind of hacky, but essentially it allows the cell's InnerTableView to know the actual height of all of the inner cells and resize the outer tableview appropriately. This method worked for me.

like image 41
MichaelScaria Avatar answered Oct 18 '22 20:10

MichaelScaria


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 ))
}
}
like image 31
Shahul Hasan Avatar answered Oct 18 '22 19:10

Shahul Hasan