UITableView content size while using auto-layout

Viewed 35602

I want to calculate Content size of my UITableView. I am using autolayout with height UITableViewAutomaticDimension.

Tried to get [UITableView contentsize] after reloading tableview, in that case height get calculated based on estimatedRowHeight.

self.tableView.estimatedRowHeight = 50;
self.tableView.rowHeight = UITableViewAutomaticDimension;

Delegate -

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
    return UITableViewAutomaticDimension;
}

Trying to get content size and assign it to height constrain.

[self.tableView reloadData];
[self.tableView layoutIfNeeded];
self.tableHeightConstraint.constant = self.tableView.contentSize.height;

Any suggestions? Thanks in advance.

Edit:

Finally I am solved my problem with some tweak. I changed tableview height to max (In my case 300 is max) before reloading data on table, so tableview has space to resize all cells.

self.tableHeightConstraint.constant = 300;
[self.tableView reloadData];
[self.tableView layoutIfNeeded];
self.tableHeightConstraint.constant = self.tableView.contentSize.height;

Thanks for help.

12 Answers

All the solutions above weren't working for my case. I ended up using an observer like this:

self.tableView.addObserver(self, forKeyPath: "contentSize", options: .new, context: nil)

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    if let obj = object as? UITableView {
        if obj == self.tableView && keyPath == "contentSize" {
            if let newSize = change?[NSKeyValueChangeKey.newKey] as? CGSize {
                self.tableView.frame.size.height = newSize.height
            }
        }
    }
}

for swift just use

tableView.invalidateIntrinsicContentSize()
tableView.layoutIfNeeded()

after reloadData() and

tableView.contentSize.height

will works perfect. Example on medium

Using KVO is the best practice to solve this contentSize problem, you don't need to use layoutIfNeeded to force the TableView to update its layout. Using @OnkarK answer (the accepted answer) will cause lag if the max_height (CGFloat.greatestFiniteMagnitude) is too big and setting the height manually is not a good idea.

Using Swift KVO:

override  func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    self.tableView.addObserver(self, forKeyPath: "contentSize", options: .new, context: nil)

}
override func viewWillDisappear(_ animated: Bool) {
    self.tableView.removeObserver(self, forKeyPath: "contentSize")
    super.viewWillDisappear(true)
}

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?){
    if keyPath == "contentSize" {
        if let value = change?[.newKey]{
            let size  = value as! CGSize
            self.tableHeightConstraint.constant = size.height
        }
    }
} 

Using Combine:

override func viewDidLoad() {
   super.viewDidLoad()

   tableView.publisher(for: \.contentSize)
      .receive(on: RunLoop.main)
      .sink { size in
         self.tableHeightConstraint.constant = size.height
      }.store(in: &cancellables)
}

Using RxSwift:

override func viewDidLoad() {
   super.viewDidLoad()

   tableView.rx.observe(CGSize.self, "contentSize")
      .filter({$0 != nil})
      .subscribe(onNext: { size in
          self.tableHeightConstraint.constant = size!.height
      }).disposed(by: bag)
}

For me the solution was to just set the estimatedRowHeight to a value larger than anything I deemed possible for the data I had to display.

For some reason this ensured that the estimatedRowHeight property wasn't used to determine the contentSize. I don't display the scroll indicator so it didn't really matter to me that the estimatedRowHeight was way off.

In my case, I don't use tableView's height constraint - using auto layout, top and bottom constraint is set and only bottom constraint changes as it goes into/out of edit mode.

Tzegenos's answer is modified in this way: 1. add tableView.estimatedRowHeight = 0 in viewDidLoad() 2. add the following:

override func viewDidLayoutSubviews() {

    // 60 is row's height as set by tableView( cellRowAt )
    // places is of type [String] to be used by tableView
    tableView.contentSize.height = 60 * CGFloat(places.count)
}

You may as well subclass the UITableView and call whomever you want (i.e. it's container view) from it's layoutSubviews() where you definitely have a proper contentSize.

Block based KVO approach

var contentSizeObserver: NSKeyValueObservation?
@IBOutlet weak var myTableView: UITableView!
@IBOutlet weak var tableHeightConstraint: NSLayoutConstraint!

override func viewDidLoad() {
        contentSizeObserver = myTableView.observe(\UITableView.contentSize, options: [NSKeyValueObservingOptions.new], changeHandler: { _, change in
            if let contentSize = change.newValue {
                self.tableHeightConstraint.constant = contentSize.height
            }
        })
    }

override func viewWillDisappear(_ animated: Bool) {
        contentSizeObserver = nil
        super.viewWillDisappear(true)
    }

Use this function inside your viewWillLayoutSubviews and call the second function after every reload. Then this problem will be solved.

 override func viewWillLayoutSubviews() {
    super.viewWillLayoutSubviews()
    dynamicHegihtChange()
}


 func dynamicHegihtChange(){
    yourTableName.invalidateIntrinsicContentSize()
    yourTableName.layoutIfNeeded()
    
    let addtionalPadding = 10 
    let height = yourTableName.contentSize.height
    self.sevicesTableViewHeightConstraint?.constant = 
     height+addtionalPadding 
  }

//for reloading the table

func serviceTableReload(){
    DispatchQueue.main.async {
        self.servicesTableView.reloadData()
        self.dynamicHegihtChange()
    }
}
Related