62

At the moment, I'm using a UITableView along with other views that are contained in a UIScrollView. I want the UITableView to have its height to be the same as its content height.

To complicate things, I'm also inserting / deleting rows to provide an accordion effect so that when the user taps on a row, it will show more detail for that row.

I've got the insert / deletion done, though at the moment it doesn't update the UIScrollView which is its superview so that the content size of the UIScrollView is recalculated and the UITableView along with other views in the UIScrollView are displayed correctly.

How can I go about implementing this so that UIScrollView's size is adjusted and its contents laid out correctly when I change the content of the UITableView? I'm currently using auto layout.

Wez Sie Tato
  • 1,186
  • 12
  • 33
Matt Delves
  • 1,585
  • 2
  • 13
  • 19
  • you add UITableView In UIScrollView ????? – iPatel Jun 27 '13 at 04:33
  • yes, the UITableView doesn't take up the entire visible area. I'm well aware that UITableView has a UIScrollView. To disable that scrolling, I'm setting the height of the tableView to the contentSize. – Matt Delves Jun 27 '13 at 04:49
  • 2
    UITableView itself has a scrollview. Then why adding to another scrollView? – Meera Jun 27 '13 at 04:49
  • The UITableView is a subview of another view (a UIScrollView) along with other views such as labels and images. – Matt Delves Jun 27 '13 at 04:51
  • 1
    @MattDelves: If thats the case its okay – Meera Jun 27 '13 at 05:02
  • are you able to expand a row on tap? You might be using the didSelectRowAtIndexPath: , then try recalculating your superview scrollview there. It should work. – Meera Jun 27 '13 at 05:07
  • That works to an extent, but I'm having a lot of drawing quirks (the removed cell is drawn on top of the tableView) when running on the iPad under iOS 6.1 (simulator and device). – Matt Delves Jun 27 '13 at 05:22
  • @Meera UITablewView doesn't have any scrollview, because the table is a scrollview (specialization or subclassing). The relationship is different. – Ricardo Nov 05 '15 at 12:00

5 Answers5

102

First of all, are those other views (siblings of the table view) strictly above and below the table view? If so, have you considered letting the table view scroll normally, and putting those outside views in the table view's header and footer views? Then you don't need the scroll view.

Second, you may want to read Technical Note TN2154: UIScrollView And Autolayout if you haven't already.

Third, given the information in that tech note, I can think of a few ways to do what you want. The cleanest is probably to create a subclass of UITableView that implements the intrinsicContentSize method. The implementation is trivial:

@implementation MyTableView

- (CGSize)intrinsicContentSize {
    [self layoutIfNeeded]; // force my contentSize to be updated immediately
    return CGSizeMake(UIViewNoIntrinsicMetric, self.contentSize.height);
}

@end

Then just let auto layout use the table view's intrinsic content size. Create the constraints between the subviews of the scroll view (including the table view) to lay them out, and make sure there are constraints to all four edges of the scroll view.

You probably need to send invalidateIntrinsicContentSize to the table view at appropriate times (when you add or remove rows or change the heights of rows). You could probably just override the appropriate methods in MyTableView to do that. E.g. do [self invalidateIntrinsicContentSize] in -endUpdates, -reloadData, - insertRowsAtIndexPaths:withRowAnimation:, etc.

Here's the result of my testing:

table view with intrinsic content size in scroll view

The scroll view has the light blue background. The red top label and the blue bottom label are siblings of the table view inside the scroll view.

Here's the complete source code for the view controller in my test. There's no xib file.

#import "ViewController.h"
#import "MyTableView.h"

@interface ViewController () <UITableViewDataSource, UITableViewDelegate>

@end

@implementation ViewController

- (void)loadView {
    UIView *view = [[UIView alloc] init];
    self.view = view;

    UIScrollView *scrollView = [[UIScrollView alloc] init];
    scrollView.translatesAutoresizingMaskIntoConstraints = NO;
    scrollView.backgroundColor = [UIColor cyanColor];
    [view addSubview:scrollView];

    UILabel *topLabel = [[UILabel alloc] init];
    topLabel.translatesAutoresizingMaskIntoConstraints = NO;
    topLabel.text = @"Top Label";
    topLabel.backgroundColor = [UIColor redColor];
    [scrollView addSubview:topLabel];

    UILabel *bottomLabel = [[UILabel alloc] init];
    bottomLabel.translatesAutoresizingMaskIntoConstraints = NO;
    bottomLabel.text = @"Bottom Label";
    bottomLabel.backgroundColor = [UIColor blueColor];
    [scrollView addSubview:bottomLabel];

    UITableView *tableView = [[MyTableView alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
    tableView.translatesAutoresizingMaskIntoConstraints = NO;
    tableView.dataSource = self;
    tableView.delegate = self;
    [scrollView addSubview:tableView];

    UILabel *footer = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 200, 30)];
    footer.backgroundColor = [UIColor greenColor];
    footer.text = @"Footer";
    tableView.tableFooterView = footer;

    NSDictionary *views = NSDictionaryOfVariableBindings(
        scrollView, topLabel, bottomLabel, tableView);
    [view addConstraints:[NSLayoutConstraint
        constraintsWithVisualFormat:@"V:|[scrollView]|"
        options:0 metrics:nil views:views]];
    [view addConstraints:[NSLayoutConstraint
        constraintsWithVisualFormat:@"H:|[scrollView]|"
        options:0 metrics:nil views:views]];
    [view addConstraints:[NSLayoutConstraint
        constraintsWithVisualFormat:@"V:|[topLabel][tableView][bottomLabel]|"
        options:0 metrics:nil views:views]];
    [view addConstraints:[NSLayoutConstraint
        constraintsWithVisualFormat:@"H:|[topLabel]|"
        options:0 metrics:nil views:views]];
    [view addConstraints:[NSLayoutConstraint
        constraintsWithVisualFormat:@"H:|-8-[tableView]-8-|"
        options:0 metrics:nil views:views]];
    [view addConstraint:[NSLayoutConstraint
        constraintWithItem:tableView attribute:NSLayoutAttributeWidth
        relatedBy:NSLayoutRelationEqual
        toItem:view attribute:NSLayoutAttributeWidth
        multiplier:1 constant:-16]];
    [view addConstraints:[NSLayoutConstraint
        constraintsWithVisualFormat:@"H:|[bottomLabel]|"
        options:0 metrics:nil views:views]];
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return 20;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"Cell"];
    if (!cell) {
        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"Cell"];
    }
    cell.textLabel.text = [NSString stringWithFormat:@"Row %d", indexPath.row];
    return cell;
}

@end
rob mayoff
  • 375,296
  • 67
  • 796
  • 848
  • 7
    **Important note :** the table footer is required for the trick to work ! (lost one hour with it --') – Tancrede Chazallet Oct 11 '13 at 15:26
  • 5
    Thanks for this great answer! Saved me hours! I've made a gist of the subclass (it works without headers and footers, too): https://gist.github.com/booiiing/7941890 – patric.schenke Dec 13 '13 at 09:29
  • 1
    This works pretty well, except for animated changes of the content size, where the tableViews size will only change after the animation inside the tableView is complete – Ahti Mar 22 '14 at 18:52
  • 3
    I got an infinite loop because layoutIfNeeded calls intrinsicContentSize that calls layoutIfNeeded. Did it happen with you Rob? – Raphael Oliveira May 14 '14 at 14:27
  • 1
    @patric.schenke YOU/r LEGEND \D/ – Zeeshan Jun 22 '14 at 19:35
  • @RaphaelOliveira, same here. The call to layoutIfNeeded isn't required in intrinsicContentSize. Works fine without it. – user3099609 Jul 11 '14 at 10:52
  • @user3099609 Not always. I'm using View Controller Containment and it doesn't work without laying out first. – Rudolf Adamkovič Aug 01 '14 at 17:05
  • @patric.schenke This link is not working. Please update link. – Varsha Vijayvargiya Jan 01 '15 at 14:54
  • @VarshaVijayvargiya the link works fine for me. The implementation may have some issues by now, though. iOS 7 and 8 changed a few things in autolayout and view-hierarchy. – patric.schenke Jan 01 '15 at 18:49
  • Worked for me! I have an implementation with two UITableViews I needed to scroll at the same time. Had to also set my scroll view's contentSize.height to the height of all my subviews afterwards in my viewWillAppear so if anybody is having issues give that a shot. – Max Stein Jun 22 '15 at 15:50
  • thanks so much. when I remove table footer , the whole table isnot appeared. why tableFooter is Required? – stackFish Jun 27 '15 at 07:42
  • Infinite loop here too. Works on 9.2 simulator but ends up in a loop on device. Any ideas why this behaves different? – Christoph Jan 27 '16 at 09:13
  • 5
    Note that this does not work when using an estimated row height with automatic table view row dimensions. The content size is not correct, it doesn't know how tall the table actually is. – Jordan H Mar 30 '16 at 21:49
  • tableview height is not increasing if i want to increase the height dynamically..? i tried to add few more rows to the tableview but the scrollview content is not expanding ,only tableview content size in expanding. please give any suggestion to increase the height of tableview inside scroll view dynamically – Dhanunjay Kumar Apr 26 '16 at 14:05
  • i am able to increase the height of the tableivew by updating its height constraint. – Dhanunjay Kumar Apr 26 '16 at 14:17
  • The selection of cells does not working :( Who has the same problem? – chudin26 Mar 27 '17 at 08:43
66

In addition to rob's answer there is swift example of self-resizable subclass of UITableView:

Swift 2.x

class IntrinsicTableView: UITableView {

    override var contentSize:CGSize {
        didSet {
            self.invalidateIntrinsicContentSize()
        }
    }


    override func intrinsicContentSize() -> CGSize {
        self.layoutIfNeeded()
        return CGSizeMake(UIViewNoIntrinsicMetric, contentSize.height)
    }

}

Swift 3.x or Swift 4.x

class IntrinsicTableView: UITableView {

    override var contentSize:CGSize {
        didSet {
            self.invalidateIntrinsicContentSize()
        }
    }

    override var intrinsicContentSize: CGSize {
        self.layoutIfNeeded()
        return CGSize(width: UIViewNoIntrinsicMetric, height: contentSize.height)
    }

}

I have used it to put a table view into another auto-resizable table view's cell.

SPatel
  • 4,768
  • 4
  • 32
  • 51
MuHAOS
  • 1,108
  • 9
  • 8
  • i try to write this method in swift 3 override func intrinsicContentSize() but an error displayed --> method don't override any in super class – Amr Angry Feb 09 '17 at 07:15
  • 6
    Swift 3 has replaced the function in favor for: override var intrinsicContentSize: CGSize – Daniel Christopher Feb 25 '17 at 20:13
  • 1
    Swift 4.2 UIViewNoIntrinsicMetric -> UIView.noIntrinsicMetric – Robert Veringa Dec 12 '18 at 12:47
  • I tried this solution, and one problem seems to be that all items in the table view are loaded at once, rather than just only those that are visible, which will cause the UI to hang. – mliu Jul 07 '20 at 17:15
12

Here is the obj-C version. It's based on a solution from user @MuHAOS

@implementation SizedTableView

- (void)setContentSize:(CGSize)contentSize {
  [super setContentSize:contentSize];
  [self invalidateIntrinsicContentSize];
}

- (CGSize)intrinsicContentSize {
  [self layoutIfNeeded]; // force my contentSize to be updated immediately
  return CGSizeMake(UIViewNoIntrinsicMetric, self.contentSize.height);
}


@end
Klemen
  • 2,144
  • 2
  • 23
  • 31
2

@MuHAOS's and @klemen-zagar's code helped me a lot but actually causes a performance issue by triggering an endless layout loop when the tableview is contained within a stack view which itself is contained in a scroll view. See my solution below.

@interface AutoSizingTableView ()
@property (nonatomic, assign) BOOL needsIntrinsicContentSizeUpdate;
@end

@implementation AutoSizingTableView

- (void)setContentSize:(CGSize)contentSize
{
    [super setContentSize:contentSize];

    self.needsIntrinsicContentSizeUpdate = YES;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        if (!self.needsIntrinsicContentSizeUpdate) {
            return;
        }

        self.needsIntrinsicContentSizeUpdate = NO;
        [self layoutIfNeeded];
        [self invalidateIntrinsicContentSize];
    });
}

- (CGSize)intrinsicContentSize
{
    return CGSizeMake(UIViewNoIntrinsicMetric, self.contentSize.height);
}

@end
mhoeller
  • 530
  • 2
  • 9
0

you can add view as headerview and footerview of tableview. because the tableview is the subview of scrollview. follow below example.

UILabel *topLabel = [[UILabel alloc] init];
topLabel.translatesAutoresizingMaskIntoConstraints = NO;
topLabel.text = @"Top Label";
topLabel.backgroundColor = [UIColor redColor];
tableView.tableFooterView = topLabel;

UILabel *footer = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 200, 30)];
     footer.backgroundColor = [UIColor greenColor];
     footer.text = @"Footer";
     tableView.tableFooterView = footer;

and also you can add headerview and footerview of tableview using simple drag and drop view to the tableview in storyboard and take IBOutlet of that views.

KKRocks
  • 8,222
  • 1
  • 18
  • 84