51

I have a non scrolling UITableView in a UIScrollView. I set the frame size of the UITableView to its content size.

When I add a row to the UITableView, I call insertRowsAtIndexPaths:withRowAnimation: on the UITableView. Then I call a method to resize the frame of the UITableView:

- (void)resizeTableViewFrameHeight
{
    // Table view does not scroll, so its frame height should be equal to its contentSize height
    CGRect frame = self.tableView.frame;
    frame.size = self.tableView.contentSize;
    self.tableView.frame = frame;
}

It seems though that the contentSize hasn't been updated at this point. If I manually calculate the frame in the above method based on the number of rows and sections, then the method works properly.

My question is, how can I get the UITableView to update its contentSize? I suppose I could call reloadData and that would probably do it, but it seems inefficient to reload the entire table when I'm just inserting one cell.

Dharmesh Dhorajiya
  • 3,976
  • 9
  • 30
  • 39
Darren
  • 10,091
  • 18
  • 65
  • 108

7 Answers7

78

You can make the UITableView calculate the size of the content immediately by calling layoutIfNeeded on the UITableView. This will run all the necessary calculations to layout the UITableView.

Example for a UITableViewController subclass that you want to put in a container view with variable size:

Objective-C

- (CGSize)preferredContentSize
{
    // Force the table view to calculate its height
    [self.tableView layoutIfNeeded];
    return self.tableView.contentSize;
}

Swift

override var preferredContentSize: CGSize {
    get {
        // Force the table view to calculate its height
        self.tableView.layoutIfNeeded()
        return self.tableView.contentSize
    }
    set {}
}
felinira
  • 1,644
  • 15
  • 21
  • I tried this, but it didn't work for me. But, I was able to come up with another solution that I posted over here: http://stackoverflow.com/questions/17634617/when-does-uitableview-content-size-update-with-row-insert-delete-animation/18665064#18665064 – Daren Sep 06 '13 at 19:21
  • 1
    Oh, this is only for the initial contentSize. Your mileage may vary for the other cases when the size is being changed afterwards. – felinira Sep 07 '13 at 06:09
  • 9
    Note for those for whom this isn't working -- tableView:estimatedHeightForRowAtIndexPath messes with contentSize, so you might have luck just by removing your implementation of it. – weienw Dec 31 '13 at 23:09
  • 13
    Not work! After calling insertRowAtIndexPath, I call layoutIfNeeded, but the contentSize does not get updated. – onmyway133 Jan 16 '14 at 03:16
  • how to write this in swift? – Himali Shah Jun 24 '16 at 06:10
  • It is everything I needed. Thanks!! – Nupur Sharma Oct 26 '17 at 03:53
29

While I don't love KVO (Key-Value Observing), it's a fairly good way to know exactly when your table's contentSize has changed (rather than just calling your update methods in a bunch of random places). It's rather simple to use (though somewhat cryptic and implicit). To observe changes on your table's contentSize, do the following:

1) Become an observer of your table's contentSize property like so:

[self.tableView addObserver:self forKeyPath:@"contentSize" options:NSKeyValueObservingOptionNew context:NULL];

This is usually done in the view controller that holds the tableView (like in viewDidLoad:, for example).

2) Implement the KVO observing method and make the changes you need:

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
    if(object == self.tableView && [keyPath isEqualToString:@"contentSize"]) {
        // perform your updates here
    }
}

3) Remove your view controller as an observer at some logical point (I call it in dealloc). You do this like so:

- (void)dealloc {
    [self.tableView removeObserver:self forKeyPath:@"contentSize"];
}
Brian Sachetta
  • 3,319
  • 2
  • 34
  • 45
  • 2
    Thanks for reminding me of KVO man. I found this the only solution that works on my fairly complex layout. – preams Jun 11 '16 at 19:25
  • Let me make a note here. I used to use KVO to the scrolling as you mentioned. However, it will not work properly after iOS 11 if you are using beginUpdates and endUpdates. In iOS 11, the contentSize changes during the updates, i.e., if you scroll as soon as contentSize changes, you will be changing the positioning of the table view during the updates, which will take you to very weird bugs. Check my question [here](https://stackoverflow.com/questions/46587878/uitableview-header-not-disappearing/46854828#46854828), using the KVO was the reason for my bug. – Gabriel Gava Nov 01 '17 at 13:12
  • In here needs notice: using Runloop to avoid recursive run leading throw bad exception. – 0xxxD Dec 31 '17 at 11:43
  • 1
    Be careful when using this, you can easily crash the app: a common crash is KVO_IS_RETAINING_ALL_OBSERVERS_OF_THIS_OBJECT_IF_IT_CRASHES_AN_OBSERVER_WAS_OVERRELEASED_OR_SMASHED – Fernando Martínez Dec 03 '18 at 16:40
14

Try this:

- (void)resizeTableViewFrameHeight
{
    UITableView *tableView = self.tableView;
    CGRect frame = tableView.frame;
    frame.size.height = [tableView sizeThatFits:CGSizeMake(frame.size.width, HUGE_VALF)].height;
    tableView.frame = frame;
}
rob mayoff
  • 375,296
  • 67
  • 796
  • 848
  • 1
    I tried this and it did work, thanks! But although it does solve my problem, it doesn't really answer my question of when the content size gets set. – Darren Apr 01 '12 at 22:58
  • 1
    I suspect it gets set in `layoutSubviews`, but I don't feel like verifying that at the moment. – rob mayoff Apr 03 '12 at 05:18
10
  1. Add observer (in my sample in viewDidLoad

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

     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 {
                     //do stuff here
                 }
             }
         }
     }
    
  3. Remove observer when not needed

     deinit {
         self.tableView.removeObserver(self, forKeyPath: "contentSize")
     }
    

---UPDATE---

with combine everything much easier:

      tableView
        .publisher(for: \.contentSize, options: .new)
        .sink(receiveValue: updateConstraintForTableViewHeight)
        .store(in: &tokens)

      // and then somewhere in u'r class

      private func updateConstraintForTableViewHeight(_ size: CGSize) {
        // do stuff with table size here
      }
hbk
  • 10,908
  • 11
  • 91
  • 124
  • 1
    Be careful when using this, you can easily crash the app: a common crash is KVO_IS_RETAINING_ALL_OBSERVERS_OF_THIS_OBJECT_IF_IT_CRASHES_AN_OBSERVER_WAS_OVERRELEASED_OR_SMASHED – Fernando Martínez Dec 03 '18 at 16:38
  • for sure, you need to fully control this, in other case - it's just one more "black box" – hbk Dec 03 '18 at 16:42
7

as @Farthen suggested you should always call yourTableView.layoutIfNeeded() to recalculate content size first.

By calling layoutIfNeeded UITableView will calculate contentSize again and then you can update the frame or constant whatever it is.

Simple two line of code will save much more efforts.

tableView.layoutIfNeeded()
tableViewHeight.constant = tableView.contentSize.height
Anil Kukadeja
  • 1,348
  • 1
  • 14
  • 27
-1

This worked for me, when addressing the issues of the tableview getting the right size after adding / removing rows

Using

    tableView.layoutIfNeeded()

and setting

    tableView.estimatedRowHeight = UITableViewAutomaticDimension
RiceAndBytes
  • 1,286
  • 10
  • 21
-1

You can change the frame of footer. I call before animated appear of footer.

if self.newTableView.contentSize.height < self.newTableView.frame.height {
        let additionalHeight: CGFloat = self.newTableView.frame.height - self.newTableView.contentSize.height

        let tableFooterView = self.newTableView.tableFooterView!

        let x = tableFooterView.frame.origin.x
        let y = tableFooterView.frame.origin.y + additionalHeight
        let width = tableFooterView.frame.width
        let height = tableFooterView.frame.height

        tableFooterView.frame = CGRect(x: x, y: y, width: width, height: height)
    }