Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

The best way to calculate UITableViewCell height without contentView frame

SUMMARY

Given that we don't always know what the frame of a cell or its content view is going to be (due to editing, rotation, accessory views etc.), what is the best way to calculate the height in tableView:heightForRowAtIndexPath: when the cell contains a variable height text field or label?

One of my UITableViewController's contains the following presentation: UITableViewCell with UITextView.

UITextView should be the same width and height as UITableViewCell.

enter image description here

I created the UITableViewCell subclass, and then and initialized it with UITextView (UITextView is a private field of my UITableViewController)

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{ 
     static NSString *CellIdentifier = @"TextViewCell";
     UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
     if (cell == nil) {
         cell = [[[BTExpandableTextViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier textView:_notesTextView] autorelease];
     }    
     return cell;
}

I implemented the following method in my UITableViewCell subclass:

- (void)layoutSubviews{
    [super layoutSubviews];
    CGFloat height = [textView.text sizeWithFont:textView.font constrainedToSize:CGSizeMake(textView.frame.size.width, MAXFLOAT)].height + textView.font.lineHeight;
    textView.frame = CGRectMake(0, 0, self.contentView.frame.size.width, (height < textView.font.lineHeight * 4) ? textView.font.lineHeight * 4 : height);    
    [self.contentView addSubview:textView];
}

and of course i implemented the following UITableViewDataSource method (look! I am using self.view.frame.size.width (but really i need UITableViewCell contentView frame width):

- (CGFloat)tableView:(UITableView*)tableView heightForRowAtIndexPath:(NSIndexPath*)indexPath{

    CGFloat height  = [_notesTextView.text sizeWithFont:_notesTextView.font 
                                      constrainedToSize:CGSizeMake(self.view.frame.size.width, MAXFLOAT)].height;        
    CGFloat groupedCellCap = 20.0;     
    height          += groupedCellCap; 

    if(height < [BTExpandableTextViewCell minimumTextViewHeightWithFont:_notesTextView.font]){
        height = [BTExpandableTextViewCell minimumTextViewHeightWithFont:_notesTextView.font];
    }        
    return height;
}    

also I implemented the following method (thats not so important but ill post it anyway, just to explain that cell's height is dynamical, it will shrink or expand after changing text in UITextView)

 - (void)textViewDidChange:(UITextView *)textView{
    CGFloat height = [_notesTextView.text sizeWithFont:_notesTextView.font 
                                 constrainedToSize:CGSizeMake(_notesTextView.frame.size.width, MAXFLOAT)].height;
    if(height > _notesTextView.frame.size.height){
        [self.tableView beginUpdates];
        [self.tableView endUpdates];
    }
}

And now, my question is: After loading view, UITableViewController is calling methods in the following order: (ill remove some, like titleForHeaderInSection and etc for simplification)

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
- (CGFloat)tableView:(UITableView*)tableView heightForRowAtIndexPath:(NSIndexPath*)indexPath{ 

and only then

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath

Look! I should return the correct UITableViewCell height before cellForRowAtIndexPath! That means: I don't know UITableViewCell contentView frame. And i can't get it programmatically.

This width can be one of:

  1. iPhone plain table, portrait orientation
  2. iPhone plain table, landscape orientation
  3. iPhone grouped table, portrait orientation
  4. iPhone grouped table, landscape orientation
  5. and the same for the iPad ( another 4 values )

And don't forget that contentView frame can be smaller because of UITableViewCell accessoryType, or because of UITableView editing state. (for example if we have UITableViewCell with multiline UILabel of any height in any editing state and with any accessoryView)

So this problem is fundamental: I just can't get cell contentView frame width for constraining, because I should return this height before cell layouts contentView. (And this is pretty logical, by the way) But this contentView frame really matters.

Of course sometimes I can know this width exactly and "hardcode" it (for example: UITableViewCellAccessoryDisclosureIndicator has 20 px width, and tableView cannot be in editing state, then I can write self.view.frame.size.width - 20 and the task is done)! Or sometimes contentView is equal to UITableViewController's view frame!

Sometimes I'm using self.view.frame.width in -tableView:heightForRowAtIndexPath: method.. (like now, and it works pretty well, but not perfectly because of grouped UITableView, should subtract some constant values, and they are different for 2 devices * 2 orientations)

Sometimes I have some #defined constants in UITableViewCell (if I know width exactly)...

Sometimes I'm using some dummy pre-allocated UITableViewCell (what is just stupid, but sometimes is pretty elegant and easy for use)...

But I don't like anything of that.

What's the best decision? Maybe i should create some helper class, that will be initialized with such parameters: accessory views, device orientation, device type, table view editing state, table view style (plain, grouped), controller view frame, and some other, that will include some constants (like grouped tableView offset, etc) and use it to find the expected UITableViewCell contentView width? ;)

Thanks

like image 556
Valentyn Kuznietsov Avatar asked Jun 19 '12 14:06

Valentyn Kuznietsov


2 Answers

Table view uses the tableView:heightForRowAtIndexPath: method to determine its contentSize before creating any UITableViewCellcells. If you stop and think about it, this makes sense, as the very first thing you would do with a UIScrollView is set its contentSize. I have run into a similar problem before, and what I've found is that it is best to have a helper function that can take the content going into the UITableViewCell and predict the height of that UITableViewCell. So I think you will want to create some sort of data structure that stores the text in each UITableViewCell, an NSDictionary with NSIndexPaths as keys and the text as values would do nicely. That way, you can find the height of the text needed without referencing the UITableViewCell.

like image 69
Sean Lynch Avatar answered Oct 01 '22 11:10

Sean Lynch


Although you can calculate heights for labels contained in table view cells, truly dynamically, in '- layoutSubviews' of a UITableViewCell subclass, there's no similar way of doing this (that I know of) for cell heights in '- tableView:heightForRowAtIndexPath:' of a table view delegate.

Consider this:

- (void)layoutSubviews
{
    [super layoutSubviews];

    CGSize size = [self.textLabel.text sizeWithFont:self.textLabel.font
                                  constrainedToSize:CGSizeMake(self.textLabel.$width, CGFLOAT_MAX)
                                      lineBreakMode:self.textLabel.lineBreakMode];
    self.textLabel.$height = size.height;
}

Unfortunately though, by the time '- tableView:heightForRowAtIndexPath:' is called, that is too early, because cell.textLabel.frame is yet set to CGRectZero = {{0, 0}, {0, 0}}.

AFAIK you won't be able to do this neither with content view's frame, nor summing up individual labels' frames...

The only way I can think of is to come up with a convenience class, methods, constants, or such that will try to cover up all possible width in any device orientation, on any device:

@interface UITableView (Additions)

@property (nonatomic, readonly) CGFloat padding;

@end

@implementation UITableView (Additions)

- (CGFloat)padding
{
    if (self.formStyle == PTFormViewStylePlain) {
        return 0;
    }

    if (self.$width < 20.0) {
        return self.$width - 10.0;
    }

    if (self.$width < 400.0 || [[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
        return 10.0;
    }

    return MAX(31.0, MIN(45.0, self.$width * 0.06));
}

@end

Also note that, recently we also have new iPhone 5's 4-inch width (568 instead of 480) in landscape orientation.

This whole thing is pretty disturbing, I know... Cheers.

like image 36
Ali Avatar answered Oct 01 '22 10:10

Ali