48

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.

OnkarK
  • 3,745
  • 2
  • 26
  • 33

12 Answers12

96

Finally I am solved my problem with some tweak. I changed tableview height to max (Objective-C: CGFLOAT_MAX, Swift: CGFloat.greatestFiniteMagnitude) before reloading data on table, so tableview has space to resize all cells.

Objective-C:

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

Swift:

tableHeightConstraint.constant = CGFloat.greatestFiniteMagnitude
tableView.reloadData()
tableView.layoutIfNeeded()
tableHeightConstraint.constant = contentTableView.contentSize.height

Posting this so it will be helpful for others.

Viktor
  • 1,020
  • 1
  • 9
  • 22
OnkarK
  • 3,745
  • 2
  • 26
  • 33
  • 1
    How do you know 300 is max(or enough). I have a tableview in scrollview, and the tableview should has a height of its content. And I find that I should give tableview a initial height greater than `([self.tableView numberOfRowInSection:0] - 1) * estimatedHeight`. Also the height must be set in `viewWillLayoutSubviews` method, that's just working, I can't tell why. – ooops Jul 27 '16 at 10:29
  • 1
    This actually works. Do you know why do we have to set constraint.constant to max first? Without it this doesn't work. – Balázs Papp Jul 31 '17 at 12:21
  • it is enough to just set frame height of table view to Big_Enough_Value, then remove row and after remove row set frame height back and set tableView contentOffset to initial value – poGUIst Apr 03 '18 at 13:18
  • This *does* work, but since I need to animate the changes in the height constraint - it looks disgusting. It pops to really tall, and then animates it shrinking to the correct size. Though if I remove this first line, it animates nicely but into the incorrect size.. Guess I'll have to find a better solution. – Sti May 06 '18 at 11:59
  • 1
    I found that (when using autolayouts, and not setting a height constraint), I didnt need to add the first line – mylogon May 09 '19 at 14:17
  • Magic, Its Worked. Thank you for saving my Time. – Harshad Patel Jul 11 '19 at 07:49
  • Setting the height to a high amount does work but if too high the view render time increases significantly. I found setting the estimatedRowHeight worked instead. – danfordham Aug 08 '19 at 10:42
  • This will cause lag on my table view, but this is the only answer that works for me :( I still looking for the best practice. – Farras Doko May 17 '21 at 10:51
  • 2
    Using `CGFLOAT_MAX` gives me warning `This NSLayoutConstraint is being configured with a constant that exceeds internal limits. A smaller value will be substituted, but this problem should be fixed. Break on BOOL _NSLayoutConstraintNumberExceedsLimit() to debug. This will be logged only once. This may break in the future.`, maybe it's better to use a smaller value – schmidt9 Feb 15 '22 at 14:39
21

Finally, I understood you problem and here is the solution of it.

I hope you have already done this.

  1. First take put some fix height of UITableView.
  2. Then take the constraint IBOutlet of UITableView Height.
  3. Then under viewDidLayoutSubviews method you can get the original UITableView height after populating the data.

Update the below code:

override func viewDidLayoutSubviews() {

   constTableViewHeight.constant = tableView.contentSize.height
}

Update:

self.tableView.estimatedRowHeight = UITableViewAutomaticDimension;

Please check this.

juanjo
  • 3,737
  • 3
  • 39
  • 44
Sohil R. Memon
  • 9,404
  • 1
  • 31
  • 57
  • 6
    I did same. Not working. tableView.contentSize get calculated based on estimatedRowHeight. I need height after resizing all cells. – OnkarK Mar 15 '16 at 05:19
  • Thanks for help. Edited question with solution I found. – OnkarK Mar 15 '16 at 05:32
16

By setting the estimatedRowHeight to zero during table view initialisation and by adding the following code to viewDidLayoutSubviews worked for me!

tableView.reloadData()
tableView.layoutIfNeeded()
tableView.constant = tableView.contentSize.height
Amit
  • 4,837
  • 5
  • 31
  • 46
Tzegenos
  • 837
  • 12
  • 16
15

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
            }
        }
    }
}
flo bee
  • 830
  • 2
  • 11
  • 20
9

for swift just use

tableView.invalidateIntrinsicContentSize()
tableView.layoutIfNeeded()

after reloadData() and

tableView.contentSize.height

will works perfect. Example on medium

Daniil Chuiko
  • 866
  • 8
  • 7
9

Using UICollectionView for custom cell size is the best solution for me but if you somehow prefer UITableView, you can use KVO instead.

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)
}
Farras Doko
  • 307
  • 4
  • 11
2

You can try two things,

  1. Call below method just after some seconds of delay after tableView reloads completely.

    - (void)adjustHeightOfTableview
    {
      dispatch_async(dispatch_get_main_queue(), ^{
    
      [self.tableView reloadData];
    
      //In my case i had to call this method after some delay, because (i think) it will allow tableView to reload completely and then calculate the height required for itself. (This might be a workaround, but it worked for me)
      [self performSelector:@selector(adjustHeightOfTableview) withObject:nil afterDelay:0.3];
      });
    }
    
  2. When you call above method do not return estimatedHeightForRow,

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

Just give it a try, it could help you get the problem.

Bharat Modi
  • 4,158
  • 2
  • 16
  • 27
2

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)
}
Brian Hong
  • 930
  • 11
  • 15
1

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.

Stefan S
  • 721
  • 10
  • 13
0

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.

nikans
  • 2,468
  • 1
  • 28
  • 35
0

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)
    }
Francis F
  • 3,157
  • 3
  • 41
  • 79
0

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()
    }
}