1

I am following the answer from this thread about using a UITableViewCell with a dynamic height. Here is the link to the great GitHub solution for iOS7.

The example works in for me in iOS7, but when I run it in iOS6 the first cell(s) do not wrap correctly.

It's only after I scroll the cell of the screen and back on does the wrapping look correct.

Here is my code, with the following comment by any changes I made so it could run in iOS6

// *** Added/Removed for iOS6

The table view cell

#import "RJTableViewCell.h"

#define kLabelHorizontalInsets 20.0f

@interface RJTableViewCell ()

@property (nonatomic, assign) BOOL didSetupConstraints;

@end

@implementation RJTableViewCell

- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
    if (self) {
        self.titleLabel = [[UILabel alloc] initWithFrame:CGRectZero];
        [self.titleLabel setTranslatesAutoresizingMaskIntoConstraints:NO];
        [self.titleLabel setLineBreakMode:NSLineBreakByTruncatingTail];
        [self.titleLabel setNumberOfLines:1];
        [self.titleLabel setTextAlignment:NSTextAlignmentLeft];
        [self.titleLabel setTextColor:[UIColor blackColor]];
        [self.titleLabel setBackgroundColor:[UIColor clearColor]];

        self.bodyLabel = [[UILabel alloc] initWithFrame:CGRectZero];
        [self.bodyLabel setTranslatesAutoresizingMaskIntoConstraints:NO];
        [self.bodyLabel setContentCompressionResistancePriority:UILayoutPriorityRequired forAxis:UILayoutConstraintAxisVertical];
        [self.bodyLabel setLineBreakMode:NSLineBreakByTruncatingTail];
        [self.bodyLabel setNumberOfLines:0];
        [self.bodyLabel setTextAlignment:NSTextAlignmentLeft];
        [self.bodyLabel setTextColor:[UIColor darkGrayColor]];
        [self.bodyLabel setBackgroundColor:[UIColor clearColor]];

        [self.contentView addSubview:self.titleLabel];
        [self.contentView addSubview:self.bodyLabel];

        [self updateFonts];
    }

    return self;
}

- (void)updateConstraints
{
    [super updateConstraints];

    if (self.didSetupConstraints) return;

    [self.contentView addConstraint:[NSLayoutConstraint
                                     constraintWithItem:self.titleLabel
                                     attribute:NSLayoutAttributeLeading
                                     relatedBy:NSLayoutRelationEqual
                                     toItem:self.contentView
                                     attribute:NSLayoutAttributeLeading
                                     multiplier:1.0f
                                     constant:kLabelHorizontalInsets]];

    [self.contentView addConstraint:[NSLayoutConstraint
                                     constraintWithItem:self.titleLabel
                                     attribute:NSLayoutAttributeTop
                                     relatedBy:NSLayoutRelationEqual
                                     toItem:self.contentView
                                     attribute:NSLayoutAttributeTop
                                     multiplier:1.0f
                                     constant:(kLabelHorizontalInsets / 2)]];

    [self.contentView addConstraint:[NSLayoutConstraint
                                     constraintWithItem:self.titleLabel
                                     attribute:NSLayoutAttributeTrailing
                                     relatedBy:NSLayoutRelationEqual
                                     toItem:self.contentView
                                     attribute:NSLayoutAttributeTrailing
                                     multiplier:1.0f
                                     constant:-kLabelHorizontalInsets]];

    //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

    [self.contentView  addConstraint:[NSLayoutConstraint
                                      constraintWithItem:self.bodyLabel
                                      attribute:NSLayoutAttributeLeading
                                      relatedBy:NSLayoutRelationEqual
                                      toItem:self.contentView
                                      attribute:NSLayoutAttributeLeading
                                      multiplier:1.0f
                                      constant:kLabelHorizontalInsets]];

    [self.contentView  addConstraint:[NSLayoutConstraint
                                      constraintWithItem:self.bodyLabel
                                      attribute:NSLayoutAttributeTop
                                      relatedBy:NSLayoutRelationEqual
                                      toItem:self.titleLabel
                                      attribute:NSLayoutAttributeBottom
                                      multiplier:1.0f
                                      constant:(kLabelHorizontalInsets / 4)]];

    [self.contentView  addConstraint:[NSLayoutConstraint
                                      constraintWithItem:self.bodyLabel
                                      attribute:NSLayoutAttributeTrailing
                                      relatedBy:NSLayoutRelationEqual
                                      toItem:self.contentView
                                      attribute:NSLayoutAttributeTrailing
                                      multiplier:1.0f
                                      constant:-kLabelHorizontalInsets]];

    [self.contentView  addConstraint:[NSLayoutConstraint
                                      constraintWithItem:self.bodyLabel
                                      attribute:NSLayoutAttributeBottom
                                      relatedBy:NSLayoutRelationEqual
                                      toItem:self.contentView
                                      attribute:NSLayoutAttributeBottom
                                      multiplier:1.0f
                                      constant:-(kLabelHorizontalInsets / 2)]];

    self.didSetupConstraints = YES;
}

- (void)updateFonts
{
    self.titleLabel2.font = [UIFont systemFontOfSize:16.0f]; // *** Added for iO6
    self.bodyLabel.font = [UIFont systemFontOfSize:12.0f]; // *** Added for iO6
    //    self.titleLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline]; // *** Removed for iO6
    //    self.bodyLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleCaption2]; // *** Removed for iO6
}

The table view controller

#import "RJTableViewController.h"
#import "RJModel.h"
#import "RJTableViewCell.h"

static NSString *CellIdentifier = @"CellIdentifier";

@interface RJTableViewController ()

@property (strong, nonatomic) RJModel *model;

// This property is used to work around the constraint exception that is thrown if the
// estimated row height for an inserted row is greater than the actual height for that row.
// See: https://github.com/caoimghgin/TableViewCellWithAutoLayout/issues/6
@property (assign, nonatomic) BOOL isInsertingRow;

@end

@implementation RJTableViewController

- (id)initWithStyle:(UITableViewStyle)style
{
    self = [super initWithStyle:style];
    if (self) {
        // Custom initialization
        self.title = @"Table View Controller";
        self.model = [[RJModel alloc] init];
        [self.model populateDataSource];
    }
    return self;
}

- (void)viewDidLoad
{
    [super viewDidLoad];

    [self.tableView registerClass:[RJTableViewCell class] forCellReuseIdentifier:CellIdentifier];

    self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemTrash target:self action:@selector(clear:)];

    self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(addRow:)];
}


- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}


- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];

    [self.tableView reloadData]; // *** Added for iOS6

    // *** Removed for iOS6
    //    [[NSNotificationCenter defaultCenter] addObserver:self
    //                                             selector:@selector(contentSizeCategoryChanged:)
    //                                                 name:UIContentSizeCategoryDidChangeNotification
    //                                               object:nil];
}

- (void)viewDidDisappear:(BOOL)animated
{
    [super viewDidDisappear:animated];

    [self.tableView reloadData]; // *** Added for iOS6

    // *** Removed for iOS6
    //    [[NSNotificationCenter defaultCenter] removeObserver:self
    //                                                    name:UIContentSizeCategoryDidChangeNotification
    //                                                  object:nil];
}


- (void)contentSizeCategoryChanged:(NSNotification *)notification
{
    [self.tableView reloadData];
}

- (void)clear:(id)sender
{
    NSMutableArray *rowsToDelete = [NSMutableArray new];
    for (NSUInteger i = 0; i < [self.model.dataSource count]; i++) {
        [rowsToDelete addObject:[NSIndexPath indexPathForRow:i inSection:0]];
    }

    self.model = [[RJModel alloc] init];

    [self.tableView deleteRowsAtIndexPaths:rowsToDelete withRowAnimation:UITableViewRowAnimationAutomatic];

    [self.tableView reloadData];
}

- (void)addRow:(id)sender
{
    [self.model addSingleItemToDataSource];

    self.isInsertingRow = YES;

    NSIndexPath *lastIndexPath = [NSIndexPath indexPathForRow:[self.model.dataSource count] - 1 inSection:0];
    [self.tableView insertRowsAtIndexPaths:@[lastIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];

    self.isInsertingRow = NO;
}

#pragma mark - Table view data source

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return 1;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    return [self.model.dataSource count];
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    RJTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];

    [cell updateFonts];

    NSDictionary *dataSourceItem = [self.model.dataSource objectAtIndex:indexPath.row];

    cell.titleLabel.text =  [dataSourceItem valueForKey:@"title"];
    cell.bodyLabel.text = [dataSourceItem valueForKey:@"body"];

    // Make sure the constraints have been added to this cell, since it may have just been created from scratch
    [cell setNeedsUpdateConstraints];

    return cell;
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    RJTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];

    [cell updateFonts];

    NSDictionary *dataSourceItem = [self.model.dataSource objectAtIndex:indexPath.row];
    cell.titleLabel.text =  [dataSourceItem valueForKey:@"title"];
    cell.bodyLabel.text = [dataSourceItem valueForKey:@"body"];

    [cell setNeedsUpdateConstraints];
    [cell updateConstraintsIfNeeded];

    // Do the initial layout pass of the cell's contentView & subviews
    [cell.contentView setNeedsLayout];
    [cell.contentView layoutIfNeeded];

    // Since we have multi-line labels, set the preferredMaxLayoutWidth now that their width has been determined,
    // and then do a second layout pass so they can take on the correct height
    cell.bodyLabel.preferredMaxLayoutWidth = CGRectGetWidth(cell.bodyLabel.frame);
    [cell.contentView layoutIfNeeded];

    CGFloat height = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;

    return height;
}

- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    if (self.isInsertingRow) {
        // A constraint exception will be thrown if the estimated row height for an inserted row is greater
        // than the actual height for that row. In order to work around this, we return the actual height
        // for the the row when inserting into the table view.
        // See: https://github.com/caoimghgin/TableViewCellWithAutoLayout/issues/6
        return [self tableView:tableView heightForRowAtIndexPath:indexPath];
    } else {
        return 500.0f;
    }
}

@end

Here is a before and after of what I'm seeing if that is useful.

Any ideas what is going on? I appreciate any insight.

Community
  • 1
  • 1
ansible
  • 3,569
  • 2
  • 18
  • 29
  • Show the code you're using. – Wain Dec 16 '13 at 16:24
  • @Wain - Sorry about that, added some files from the project along with my changes. You can see some examples of what I'm seeing before https://dl.dropboxusercontent.com/u/11748/UITableViewCellBefore.png and after https://dl.dropboxusercontent.com/u/11748/UTTableViewCell%20After.png – ansible Dec 16 '13 at 17:51
  • 1
    Have you tried calling `[cell.contentView setNeedsLayout]` when the cell is created / displayed? The cell used for calculating the height is probably not used for display... – Wain Dec 16 '13 at 18:19
  • Thanks - I tried adding it to the end of tableView: cellForRowAtIndexPath: right before returning cell, but I'm seeing the same issues. – ansible Dec 16 '13 at 20:05
  • 1
    I should really say, everything that you have in `heightForRowAtIndexPath` that is related to configuration or layout should really be in `cellForRowAtIndexPath`. – Wain Dec 16 '13 at 20:34
  • @Wain- Yeah, the example from the other problem had it this way, I'm not sure why. That said, moving all the code from 'heightForRowAtIndexPath' to 'cellForRowAtIndexPath' does seem to work, I think I needed to set 'cell.bodyLabel.preferredMaxLayoutWidth = CGRectGetWidth(cell.bodyLabel.frame)'. Per your suggestion, I moved all the code into 'cellForRowAtIndexPath' and have 'heightForRowAtIndexPath' calling it to get the height now. Now I'm not sure why it's fine in iOS7 and not iOS6,but I'm still figuring out some of this auto layout stuff. Thanks for the help. – ansible Dec 16 '13 at 21:08

2 Answers2

1

Per the advice from @Wain I tried moving all the code from heightForRowAtIndexPath to cellForRowAtIndexPath and that worked. I think I needed to set cell.bodyLabel.preferredMaxLayoutWidth = CGRectGetWidth(cell.bodyLabel.frame).

I moved all the code into 'cellForRowAtIndexPath' and have 'heightForRowAtIndexPath' calling it to get the height now. Now I'm not sure why it's fine in iOS7 and not iOS6,but I'm still figuring out some of this auto layout stuff.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    RJTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];

    [cell updateFonts];

    NSDictionary *dataSourceItem = [self.model.dataSource objectAtIndex:indexPath.row];
    cell.titleLabel2.text =  [dataSourceItem valueForKey:@"title"];
    cell.bodyLabel.text = [dataSourceItem valueForKey:@"body"];

    [cell setNeedsUpdateConstraints];
    [cell updateConstraintsIfNeeded];

    // Do the initial layout pass of the cell's contentView & subviews
    [cell.contentView setNeedsLayout];
    [cell.contentView layoutIfNeeded];

    // Since we have multi-line labels, set the preferredMaxLayoutWidth now that their width has been determined,
    // and then do a second layout pass so they can take on the correct height
    cell.bodyLabel.preferredMaxLayoutWidth = CGRectGetWidth(cell.bodyLabel.frame);
    [cell.contentView layoutIfNeeded];

    return cell;
}

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
    RJTableViewCell *cell = [self tableView:tableView cellForRowAtIndexPath:indexPath];

    CGFloat height = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;

    return height;
}
ansible
  • 3,569
  • 2
  • 18
  • 29
  • You're correct here - you stumbled across an omission in the code of that sample project. It was already fixed in this [pull request](https://github.com/caoimghgin/TableViewCellWithAutoLayout/pull/9). Good catch! – smileyborg Dec 18 '13 at 17:55
  • Thanks for following up. Do you understand why it works okay in iOS7 and not iOS6? I'm just trying to understand. The best I can come up with it's dumb lock that iOS7 calls heightForRowAtIndexPath before calling cellForRowAtIndexPath. – ansible Dec 18 '13 at 21:01
  • Yes, I think it's something related to that -- on iOS 7 the cell reuse pool is getting filled from the heightForRow method, so that when the cellForRow method gets called, the cells are all there with the preferredMaxLayoutWidth set correctly. Would have to do a little debugging to double check though. There are quite a few differences between iOS 6 and 7 around table views, and auto layout, so I'm not surprised to see things like this. – smileyborg Dec 18 '13 at 21:58
0

For others coming by this page, Use Your Loaf has a couple articles about autolayout + UITableViewCells.

With iOS 7 and with iOS 8.

His code shows how to use a prototype cell to figure out height instead of dequing one every time. This will result in faster code.

By overwriting layoutSubviews to set the preferredMaxLayoutWidth it means you don't have to do it in your controller.

- (void)layoutSubviews
{
  [super layoutSubviews];
  [self.contentView layoutIfNeeded];
  self.lineLabel.preferredMaxLayoutWidth = CGRectGetWidth(self.lineLabel.frame);
}

[self.contentView layoutIfNeeded] is also important to get the right layout when it first loads.

RyanJM
  • 7,028
  • 8
  • 60
  • 90