Reputation: 311
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
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
What can I do to show full second table without empty space at bottom of cell.
Upvotes: 8
Views: 17935
Reputation: 16639
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).
Upvotes: 0
Reputation: 300
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 ))
}
}
Upvotes: 3
Reputation: 2454
All you need is invalidate content size in side ParentTableView's cellForRowAt indexPath
parentCell.InsideTableview.invalidateIntrinsicContentSize()
Upvotes: 0
Reputation: 311
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
Upvotes: 22
Reputation: 1590
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.
Upvotes: 16