I have a UITableViewCell
that is populated dynamically with data retrieved from a remote source. It has a variable number of subviews based on the type of item being represented in the cell. I am dynamically calculating the size of the cell's contents by using a NSMutableAttributedString
and the boundingRectWithSize:
method. I take the returned values and calculate a combinedHeight
variable that is stored in a NSMutableDictionary
and associated with the indexPath
of the cell. That all works fine, but when I attempt to use the values from the dictionary in the heightForRowAtIndexPath:
method, it only uses the first set of visible cells, and after that everything is 0.
My question is how I can dynamically size the cell to contain all of the subviews. I am also adding a custom view to the cell's contentView that is smaller width than the cell so that I can achieve a unique style we are going for.
Here is my code:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (areFeedItemsLoaded && feedItems.count > 0 && indexPath.row < feedItems.count)
{
CGFloat height = [[_cellHeights objectForKey:[self keyForIndexPath:indexPath]] floatValue];
return MAX(90, height);
}
return 44;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell;
if (areFeedItemsLoaded)
{
if (feedItems.count == 0)
{
if (indexPath.row == 0)
{
cell = [EmptyTableViewCell getBlankCell:tableView];
}
else
{
EmptyTableViewCell *eCell = [EmptyTableViewCell getEmptyCell:tableView withHeaderText:@"No Recent Activity"];
[eCell setLinkText:@"Your team's activity on Hudl will appear here." withUrl:nil];
cell = eCell;
}
}
else if (indexPath.row < feedItems.count)
{
FeedItem *item = [feedItems objectAtIndex:indexPath.row];
cell = [self.tableView dequeueReusableCellWithIdentifier:FeedItemCellIdentifier forIndexPath:indexPath];
if (cell == nil)
{
cell = [[FeedItemTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:FeedItemCellIdentifier];
}
cell.clipsToBounds = YES;
cell.backgroundColor = [UIColor clearColor];
CGFloat combinedHeight = 8;
UIView *customContentView = [cell viewWithTag:customContentTagId];
if (customContentView == nil)
{
customContentView = [[UIView alloc] init];
customContentView.tag = customContentTagId;
customContentView.backgroundColor = [UIColor colorWithR:100 g:100 b:100 a:1];
[cell.contentView addSubview:customContentView];
}
customContentView.frame = CGRectMake(50, 0, 270, 70);
UIImageView *userImage = (UIImageView *)[cell viewWithTag:userImageTagId];
if (userImage == nil)
{
userImage = [[UIImageView alloc] init];
userImage.tag = userImageTagId;
[customContentView addSubview:userImage];
}
userImage.frame = CGRectMake(8, 8, 45, 45);
userImage.layer.cornerRadius = CGRectGetWidth(userImage.frame) / 2;
userImage.layer.masksToBounds = YES;
NSURL *imageUrl = item.imageSourceUrl;
[userImage setImageWithURL:imageUrl placeholderImage:[UIImage imageNamed:@"default-user.png"]];
CGFloat minX = CGRectGetMaxX(userImage.frame) + 8;
UILabel *titleLabel = (UILabel *)[cell viewWithTag:titleTagId];
if (titleLabel == nil)
{
titleLabel = [[UILabel alloc] init];
titleLabel.numberOfLines = 0;
titleLabel.textColor = [UIColor colorWithR:204 g:204 b:204 a:1];
titleLabel.font = [UIFont fontWithName:@"Helvetica" size:14];
titleLabel.lineBreakMode = NSLineBreakByWordWrapping;
titleLabel.tag = titleTagId;
[customContentView addSubview:titleLabel];
}
NSString *title = [NSString stringWithFormat:@"%@ %@", item.creatorName, item.title];
NSMutableAttributedString *titleText = [[NSMutableAttributedString alloc] initWithString:title];
[titleText addAttribute:NSForegroundColorAttributeName value:[UIColor colorWithR:204 g:204 b:204 a:1] range:NSMakeRange(0, title.length - 1)];
[titleText addAttribute:NSFontAttributeName value:[UIFont fontWithName:@"Helvetica-Bold" size:14] range:NSMakeRange(0, item.creatorName.length)];
[titleText addAttribute:NSFontAttributeName value:[UIFont fontWithName:@"Helvetica" size:15] range:NSMakeRange(item.creatorName.length + 1, title.length - item.creatorName.length - 1)];
titleLabel.attributedText = titleText;
CGFloat width = CGRectGetWidth(customContentView.frame) - (CGRectGetMaxX(userImage.frame) + 8) - 8;
width = MIN(width, 200);
CGSize titleSize = [titleText boundingRectWithSize:CGSizeMake(width, 10000) options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading context:nil].size;
titleLabel.frame = CGRectMake(minX, CGRectGetMinY(userImage.frame), ceil(titleSize.width), ceil(titleSize.height));
combinedHeight += ceil(titleSize.height) + 8;
UIView *thumbnailsView = [cell viewWithTag:thumbnailTagId];
UILabel *descriptionLabel = (UILabel *)[cell viewWithTag:descriptionTagId];
if ([item.feedItemType intValue] == PlaylistFI || [item.feedItemType intValue] == PlaylistAndNotesFI)
{
if ([item.uris count] > 0)
{
if (thumbnailsView == nil)
{
thumbnailsView = [[UIView alloc] init];
thumbnailsView.tag = thumbnailTagId;
[customContentView addSubview:thumbnailsView];
}
else
{
for (UIView *view in thumbnailsView.subviews)
{
[view removeFromSuperview];
}
}
thumbnailsView.frame = CGRectMake(minX, CGRectGetMaxY(titleLabel.frame) + 8, width, 40);
combinedHeight += 48;
for (int i = 0; i < 3 && i < item.uris.count; i++)
{
NSURL *url = [item.uris objectAtIndex:i];
UIImageView *imageView = [[UIImageView alloc] init];
int x = i * 60 + i * 8;
imageView.frame = CGRectMake(x, 0, 60, 40);
[imageView setImageWithURL:url placeholderImage:[UIImage imageNamed:@"soft-dark-grad.png"]];
[thumbnailsView addSubview:imageView];
}
}
if (descriptionLabel == nil)
{
descriptionLabel = [[UILabel alloc] init];
descriptionLabel.tag = descriptionTagId;
descriptionLabel.font = [UIFont systemFontOfSize:14];
descriptionLabel.textColor = [UIColor colorWithR:204 g:204 b:204 a:1];
descriptionLabel.numberOfLines = 0;
descriptionLabel.lineBreakMode = NSLineBreakByWordWrapping;
[customContentView addSubview:descriptionLabel];
}
NSMutableAttributedString *descriptionText = [[NSMutableAttributedString alloc] initWithString:item.descriptionText];
[descriptionText addAttribute:NSFontAttributeName value:[UIFont fontWithName:@"Helvetica" size:14] range:NSMakeRange(0, item.descriptionText.length)];
descriptionLabel.attributedText = descriptionText;
CGSize descriptionSize = [descriptionText boundingRectWithSize:CGSizeMake(width, 10000) options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading context:nil].size;
descriptionLabel.frame = CGRectMake(minX, CGRectGetMaxY(thumbnailsView.frame) + 8, ceil(descriptionSize.width), ceil(descriptionSize.height));
combinedHeight += ceil(descriptionSize.height) + 8;
}
else
{
if (thumbnailsView != nil)
{
[thumbnailsView removeFromSuperview];
}
if (descriptionLabel != nil)
{
[descriptionLabel removeFromSuperview];
}
}
UILabel *messageLabel = (UILabel *)[cell viewWithTag:messageTagId];
if ([item.feedItemType intValue] == MessageFI)
{
if (messageLabel == nil)
{
messageLabel = [[UILabel alloc] init];
messageLabel.tag = messageTagId;
messageLabel.font = [UIFont systemFontOfSize:14];
messageLabel.textColor = [UIColor colorWithR:204 g:204 b:204 a:1];
messageLabel.numberOfLines = 0;
messageLabel.lineBreakMode = NSLineBreakByWordWrapping;
[customContentView addSubview:messageLabel];
}
NSMutableAttributedString *messageText = [[NSMutableAttributedString alloc] initWithString:item.message];
[messageText addAttribute:NSFontAttributeName value:[UIFont fontWithName:@"Helvetica" size:14] range:NSMakeRange(0, item.message.length)];
CGSize messageSize = [messageText boundingRectWithSize:CGSizeMake(width, 10000) options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading context:nil].size;
messageLabel.frame = CGRectMake(minX, CGRectGetMaxY(titleLabel.frame) + 8, ceil(messageSize.width), ceil(messageSize.height));
messageLabel.attributedText = messageText;
combinedHeight += ceil(messageSize.height) + 8;
}
else
{
if (messageLabel != nil)
{
[messageLabel removeFromSuperview];
}
}
if ([item.feedItemType intValue] == PlaylistFI || [item.feedItemType intValue] == PlaylistAndNotesFI ||
[item.feedItemType intValue] == ArticleFI)
{
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
cell.selectionStyle = UITableViewCellSelectionStyleBlue;
}
else
{
cell.accessoryType = UITableViewCellAccessoryNone;
cell.selectionStyle = UITableViewCellSelectionStyleNone;
}
[_cellHeights setObject:@(combinedHeight) forKey:[self keyForIndexPath:indexPath]];
CGRect frame = customContentView.frame;
frame.size.height = MAX(70, combinedHeight);
customContentView.frame = frame;
}
else if (!requestFailed)
{
cell = [LoadingTableViewCell getLoadingCell:tableView];
}
else
{
cell = [ErrorTableViewCell getErrorCell:tableView];
}
}
else
{
cell = (indexPath.row == 0 || indexPath.row < feedItems.count) ? [EmptyTableViewCell getBlankCell:tableView] : [LoadingTableViewCell getLoadingCell:tableView];
}
return cell;
}
- (NSIndexPath *)keyForIndexPath:(NSIndexPath *)indexPath
{
if ([indexPath class] == [NSIndexPath class]) {
return indexPath;
}
return [NSIndexPath indexPathForRow:indexPath.row inSection:indexPath.section];
}
The issue with your code is the order in which the height is calculated. If you set a breakpoint in both methods you will see that
tableView:heightForRowAtIndexPath:
is actually called BEFOREtableView:cellForRowAtIndexPath:
. This means that doing the calculations for the height when setting up the cell will not work (as the height will return 0 before it has been set).Here is my suggestions:
Create a
UITableViewCell
subclass and move all of the initialization and setup logic you have piled intableView:cellForRowAtIndexPath:
in that subclass.once a cell is setup with whatever data properties may change its height, set the frame height of the cell internally because you will use it in the next step.
Then in order to calculate the height independently, use a method in the cell itself that looks something like this:
Then to get the height you would just do: