Dynamic UITableView Cell Height Based on Contents - ios

I have a UITableView that is populated with custom cells (inherited from UITableViewCell), each cell contains a UIWebView that is automatically resize based on it's contents. Here's the thing, how can I change the height of the UITableView cells based on their content (variable webView).
The solution must be dynamic since the HTML used to populate the UIWebViews is parsed from an ever changing feed.
I have a feeling I need to use the UITableView delegate method heightForRowAtIndexPath but from it's definition:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
;//This needs to be variable
}
I can't access the cell or it's contents. Can I change the height of the cell in cellForRowAtIndexPath?
Any help would be grand. Thanks.
Note
I asked this question over 2 years ago. With the intro of auto layout the best solution for iOS7 can be found:
Using Auto Layout in UITableView for dynamic cell layouts & variable row heights
and on iOS8 this functionality is built in the SDK

This usually works pretty well:
Objective-C:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return UITableViewAutomaticDimension;
}
Swift:
override func tableView(tableView: UITableView!, heightForRowAtIndexPath indexPath: NSIndexPath!) -> CGFloat {
return UITableViewAutomaticDimension;
}

The best way that I've found for dynamic height is to calculate the height beforehand and store it in a collection of some sort (probably an array.) Assuming the cell contains mostly text, you can use -[NSString sizeWithFont:constrainedToSize:lineBreakMode:] to calculate the height, and then return the corresponding value in heightForRowAtIndexPath:
If the content is constantly changing, you could implement a method that updated the array of heights when new data was provided.

self.tblVIew.estimatedRowHeight = 500.0; // put max you expect here.
self.tblVIew.rowHeight = UITableViewAutomaticDimension;

I tried many solutions, but the one that worked was this, suggested by a friend:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
int height = [StringUtils findHeightForText:yourLabel havingWidth:yourWidth andFont:[UIFont systemFontOfSize:17.0f]];
height += [StringUtils findHeightForText:yourOtherLabel havingWidth:yourWidth andFont:[UIFont systemFontOfSize:14.0f]];
return height + CELL_SIZE_WITHOUT_LABELS; //important to know the size of your custom cell without the height of the variable labels
}
The StringUtils.h class:
#import <Foundation/Foundation.h>
#interface StringUtils : NSObject
+ (CGFloat)findHeightForText:(NSString *)text havingWidth:(CGFloat)widthValue andFont:(UIFont *)font;
#end
StringUtils.m class:
#import "StringUtils.h"
#implementation StringUtils
+ (CGFloat)findHeightForText:(NSString *)text havingWidth:(CGFloat)widthValue andFont:(UIFont *)font {
CGFloat result = font.pointSize+4;
if (text) {
CGSize size;
CGRect frame = [text boundingRectWithSize:CGSizeMake(widthValue, CGFLOAT_MAX)
options:NSStringDrawingUsesLineFragmentOrigin
attributes:#{NSFontAttributeName:font}
context:nil];
size = CGSizeMake(frame.size.width, frame.size.height+1);
result = MAX(size.height, result); //At least one row
}
return result;
}
#end
It worked perfectly for me. I had a Custom Cell with 3 images with fixed sizes, 2 labels with fixed sizes and 2 variable labels.

The big problem with cells with dynamic height in iOS is that the table vc must calculate and return a height of each cell before the cells are drawn. Before a cell is drawn, though, it doesn't have a frame and thus no width. This causes a problem if your cell is to change its height based on, say, the amount of text in the textLabel, since you do not know its width.
A common solution that I've seen is that people define a numeric value for the cell width. This is a bad approach, since tables can be plain or grouped, use iOS 7 or iOS 6 styling, be displayed on an iPhone or iPad, in landscape or portrait mode etc.
I struggled with these issues in an iOS app of mine, which supports iOS5+ and both iPhone and iPad with multiple orientations. I needed a convenient way to automate this and leave the logic out of the view controller. The result became a UITableViewController sub class (so that it can hold state) that supports default cells (Default and Subtitle style) as well as custom cells.
You can grab it at GitHub (https://github.com/danielsaidi/AutoSizeTableView). I hope it helps those of you who still struggle with this problem. If you do check it out, I'd love to hear what you think and if it worked out for you.

Here is code that I used for dynamic cell height when fetching tweets from twitter and then storing them in CoreData for offline reading.
Not only does this show how to get the cell and data content, but also how to dynamically size a UILabel to the content with padding
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
Tweet *tweet = [self.fetchedResultsController objectAtIndexPath:indexPath];
NSString* text = tweet.Text;
TweetTableViewCell *cell = (TweetTableViewCell*)[self tableView:tableView cellForRowAtIndexPath:indexPath];
//Set the maximum size
CGSize maximumLabelSize = cell.tweetLabel.frame.size;
CGPoint originalLocation = cell.tweetLabel.frame.origin;
//Calculate the new size based on the text
CGSize expectedLabelSize = [text sizeWithFont:cell.tweetLabel.font constrainedToSize:maximumLabelSize lineBreakMode:cell.tweetLabel.lineBreakMode];
//Dynamically figure out the padding for the cell
CGFloat topPadding = cell.tweetLabel.frame.origin.y - cell.frame.origin.y;
CGFloat bottomOfLabel = cell.tweetLabel.frame.origin.y + cell.tweetLabel.frame.size.height;
CGFloat bottomPadding = cell.frame.size.height - bottomOfLabel;
CGFloat padding = topPadding + bottomPadding;
CGFloat topPaddingForImage = cell.profileImage.frame.origin.y - cell.frame.origin.y;
CGFloat minimumHeight = cell.profileImage.frame.size.height + topPaddingForImage + bottomPadding;
//adjust to the new size
cell.tweetLabel.frame = CGRectMake(originalLocation.x, originalLocation.y, cell.tweetLabel.frame.size.width, expectedLabelSize.height);
CGFloat cellHeight = expectedLabelSize.height + padding;
if (cellHeight < minimumHeight) {
cellHeight = minimumHeight;
}
return cellHeight;
}

Also i think such an algorithm will suit you:
1) in cellForrowAtIndexPath you activate your webviews for loading and give them tags equal to indexPath.row
2) in webViewDidFinishLoading you calculate the height of the content in the cell, and compose a dictionary with keys and values like this: key= indexPath.row value = height
3)call [tableview reloadData]
4) in [tableview cellForRowAtIndexPath:indexPath] set proper heights for corresponding cells

This is one of my nice solution. it's worked for me.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
cell.textLabel.text = [_nameArray objectAtIndex:indexPath.row];
cell.textLabel.numberOfLines = 0;
cell.textLabel.lineBreakMode = NSLineBreakByWordWrapping;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return UITableViewAutomaticDimension;
}
We need to apply these 2 changes.
1)cell.textLabel.numberOfLines = 0;
cell.textLabel.lineBreakMode = NSLineBreakByWordWrapping;
2)return UITableViewAutomaticDimension;

In Swift 4+ you can set it dinamic
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension
}

I always implement this in all my cells in a super cell class because for some reason UITableViewAutomaticDimension doesn't work so well.
-(CGFloat)cellHeightWithData:(id)data{
CGFloat height = [[self contentView] systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
[self fillCellWithData:data]; //set the label's text or anything that may affect the size of the cell
[self layoutIfNeeded];
height = [[self contentView] systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
return height+1; //must add one because of the cell separator
}
just call this method on your -(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPathusing a dummy cell.
note: this works only with autolayout, but it also works with ios 7 and later.
pd: don't forget to check the checkbox on the xib or storyboard for "preferred width explicit" and set the static width (on the cmd + alt + 5 menu)

Swift
Use custom cell and labels. Set up the constrains for the UILabel. (top, left, bottom, right) Set lines of the UILabel to 0
Add the following code in the viewDidLoad method of the ViewController:
tableView.estimatedRowHeight = 68.0
tableView.rowHeight = UITableViewAutomaticDimension
// Delegate & data source
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return UITableViewAutomaticDimension;
}

I had very large test in UILabel. Above all fail to work, then i create category for string as below and got the exact height
- (CGFloat)heightStringWithEmojifontType:(UIFont *)uiFont ForWidth:(CGFloat)width {
// Get text
CFMutableAttributedStringRef attrString = CFAttributedStringCreateMutable(kCFAllocatorDefault, 0);
CFAttributedStringReplaceString (attrString, CFRangeMake(0, 0), (CFStringRef) self );
CFIndex stringLength = CFStringGetLength((CFStringRef) attrString);
// Change font
CTFontRef ctFont = CTFontCreateWithName((__bridge CFStringRef) uiFont.fontName, uiFont.pointSize, NULL);
CFAttributedStringSetAttribute(attrString, CFRangeMake(0, stringLength), kCTFontAttributeName, ctFont);
// Calc the size
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(attrString);
CFRange fitRange;
CGSize frameSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRangeMake(0, 0), NULL, CGSizeMake(width, CGFLOAT_MAX), &fitRange);
CFRelease(ctFont);
CFRelease(framesetter);
CFRelease(attrString);
return frameSize.height + 10;}

Related

UITextView With "See More" Button if it doesn't fit inside UITableViewCell IOS

It sounds so simple but it's not, what I want is to make a UITableViewCell with a UITextView inside. I am using auto layout so when the tableview first loads all cell they will all be in the same maximum static size and when I press the cell the cell expand according to the UITextView's text.
The hard part is to add a "See More" button in the end of the text view if it doesn't fit the static size.(before the cell was pressed)
I also saved to array if the cell was pressed to know if to expand it or not.
another tricky part is if the textview is below the static size than use the UITableViewAutomaticDimension so if its 40 points of height I don't want the cell to be 102 point because of a blank space the will be created
for that I need to know if the textview is below 102 point.
I thought about implementing heightForRow something like that
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if([self isCellWithIndexNeedsToExpand:[NSNumber numberWithInteger:indexPath.row]])
{
return UITableViewAutomaticDimension;
}
else
{
int height = [self someFunctionThatGivesTheTextViewHeight];
if(height>102)
return 102;
else
return UITableViewAutomaticDimension;
}
}
In autolayout it does not matter at all.. but through code you can use this below code to find estimated string size,
CGSize labelSize = [myLabel.text sizeWithFont:myLabel.font
constrainedToSize:myLabel.frame.size
lineBreakMode:NSLineBreakByWordWrapping];
CGFloat labelHeight = labelSize.height;
then you can increase the height of row

estimatedHeightForRowAtIndexPath best practice

I have a UITableCell that displays dynamic content of various type.
There are labels that can be 1 or 2 lines, textViews with various heights, images with various heights.
What is the best way to estimate height for these rows? I can make functions that calculates height for labels and textviews. But for the constraints, can i make outlet and sum upp all the height constants?
If you set up your auto layout correctly. You don't have to calculate the constant yourself. You can simply set two properties (or do it in table view delegates):
tableView.estimatedRowHeight = 80;
tableView.rowHeight = UITableViewAutomaticDimension
and OS will check the correct cell height for you.
If you have a few different cells the best way to make it works smooth is implement a delegate method instead of do it via property tableView.estimatedRowHeight, for example:
func tableView(tableView: UITableView, estimatedHeightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
// 1. get table view cell for indexPath
// 2. check which cell type have you got
// 3. return as closed estimated value for the cell type as you possible can.
// Example
//if cell type is my cell A return 80
// else if cell is B return 120, etc
}
maybe it can help
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSAttributedString *attrStr = [[NSAttributedString alloc] init];
height = [self calculateHeightForAttributeString:attrStr];
return height;
}
-(CGFloat)calculateHeightForAttributeString:(NSAttributedString *)attrStr {
CGRect frame;
frame = [attrStr boundingRectWithSize:CGSizeMake(labelMaxWidth, MAXFLOAT)
options:(NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading)
context:nil];
return frame.size.height;
}

UILabel Height is not updating in UITableView at launch

I am trying to set height of UILabel dynamically in the UITableView. During the launch height update is not reflected but as soon as I scroll down and scroll up back, update can be seen.
At Launch
After Scroll down and Scrolling back up again - This what I need. See the change in text in front of player icon. I need the complete text at launch itself.
Here is the code that I am trying to use:
- (void) updateMessageTextForRow:(long)row ofCell:(ESGameStreamCellView *)cell
{
NSString *item = _gameFeedItems[row];
NSString *title = item ?: NSLocalizedString(#"[No Title]", nil);
cell.message.preferredMaxLayoutWidth = cell.message.bounds.size.width;
// Update message label height
CGSize maximumLabelSize = CGSizeMake(296, FLT_MAX);
CGSize expectedLabelSize = [title sizeWithFont:cell.message.font
constrainedToSize:maximumLabelSize
lineBreakMode:cell.message.lineBreakMode];
//adjust the label the the new height.
CGRect newFrame = cell.message.frame;
newFrame.size.height = expectedLabelSize.height;
cell.message.frame = newFrame;
NSLog(#"Message = %#, Height: %f", title, cell.message.frame.size.height);
}
During Custom TableCellView Initialization
- (void)awakeFromNib {
// Initialization code
self.backgroundColor = [UIColor clearColor];
_message.lineBreakMode = NSLineBreakByWordWrapping;
_message.numberOfLines = 0;
}
Code for the row height
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
// Currently fixed height. Will be calculating dynamic height after adjusting the views.
return 300;
}
Are you using autoLayout ? I highly suspect that your constraints aren't set up properly: the UIImageView top should by tied with the titleLabel's bottom.
Also, you should use the new property for dynamic row : rowHeight and estimatedRowHeight.
You are getting the correct height after the cell's reuse : set the preferredMaxLayoutWidth property in the viewDidLayoutSubviews inside your custom cell class.
firstly, you should understand of the working flow of tableview delegate in objective c. Your cell height & position will be fixed after init. that's why you have to define each row's height in
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
// Currently fixed height. Will be calculating dynamic height after adjusting the views.
return 300;
}
From this point forward, your cell's height will be fixed, even if you re-config the frame.
The best practice is some article called "Dynamic height tableview cell" and you can easily find it here
http://www.raywenderlich.com/87975/dynamic-table-view-cell-height-ios-8-swift
I learn on above article (thanks for Joshua Greene) and re-write it to another library allow you to make a dynamic tableview easily. You can find it here
https://github.com/EugeneNguyen/XBMobile
it's not too perfect, but hope that if can help.

Dynamic table cell height

Using the storyboard, I've created a custom cell for my table view, I've also created a custom class for it with all my properties.
Now, what would be the best way in making the cells height dynamic, where is the best way to do this?
Should I do this in the custom class for the cell? Or in my table view controller?
I imagine it would make more sense to do this in the custom class, but then how should I do this?
Once a specific label is filled in, it should change the height of the cell
You cannot change the height of the cell from your custom drawing class.
You can do this in the viewController that has the UITableView only.
Either by specifying a hardcoded row height for all cells, or by using the
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
and specifying a height for the cells here.
If you want to have different heights for the cells, you should check the indexpath.row property and return the desired height value.
In case you want to change the height of an already drawn in screen cell, you will have to reload that cell to reflect the change using this:
- (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation
Use the following code
-(CGFloat)heightForText:(NSString *)str width:(int)width font:(UIFont *)font lineBreakMode:(NSLineBreakMode) lineBreakMode
{
CGSize textSize;
textSize = [str boundingRectWithSize:CGSizeMake(width, FLT_MAX) options:NSStringDrawingUsesLineFragmentOrigin attributes:#{NSFontAttributeName : font} context:nil].size;
return textSize.height;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if(tableView == self.yourTable)
{
NSMutableDictionary *dict = [self.yourArray objectAtIndex:indexPath.row] ;
return [self heightForText:[dict valueForKey:#"reviewDescription"] width:300 font:[UIFont fontWithName:kAppRegularFont size:15.0] lineBreakMode:0]+75;
}
return 70;
}
Set your view controller to be the delegate for the table view and then implement the following UITableViewDelegate method in the view controller:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
Add these line into your "viewDidLoad" method
self.tableView.estimatedRowHeight = your height here
self.tableView.rowHeight = UITableViewAutomaticDimension;
Don't set default hight into delegate
func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
}

How do I create a multiline table cell in iOS?

How can I get the second cell to expand to fit the text rather than scaling the text? Is there a built in way of doing this in iOS or will I have to come up with some home-cooked solution? If you look in the iOS Contacts application, there's a box like this for Address. I can't find how to implement this though.
For anyone looking to achieve this in future, here's the code for my solution:
HEADER file:
#define FONT_SIZE 22.0f
#define CELL_CONTENT_WIDTH 320.0f
#define CELL_CONTENT_MARGIN 5.0f
IMPLEMENTATION file:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.section == 0 && indexPath.row == 1) {
NSString *text = [atmAnnotation address];
CGSize constraint = CGSizeMake(CELL_CONTENT_WIDTH - (CELL_CONTENT_MARGIN * 2), 20000.0f);
CGSize size = [text sizeWithFont:[UIFont systemFontOfSize:FONT_SIZE] constrainedToSize:constraint lineBreakMode:UILineBreakModeWordWrap];
NSLog(#"Size for address is Height:%f Width:%f",size.height,size.width);
CGFloat height = MAX(size.height, 44.0f);
return height + (CELL_CONTENT_MARGIN * 2);
}
return 44.0f;
}
Here's a screenshot of the result:
Unfortunately, you are going to have to implement this feature yourself. There are a variety of methods and callbacks you need to make use of to calculate the height of the rows and labels. If you need some help getting started, I can amend this answer to include some sample code. Otherwise, I'm sure there are some related questions here on SO or Google that can get you started. In summary, however:
Determine the height of the row in the UITableViewDelegate method -tableView:heightForRowAtIndexPath:. You'll probably need to use the NSString UIKit Addition methods to calculate how tall your string will be with a given font size, etc. Return the proper height from this delegate method.
Make sure your label (either in IB or configured programmatically) is set to use multiple lines. If you set numberOfLines to 0, the label will use as many lines as necessary.
In your UITableViewDataSource implementation of -tableView:cellForRowAtIndexPath:, you'll need to use the same logic as before to determine the frame of your labels. Set the text you want, then change the frame so that the label is tall enough to just barely fit all the text.
Where you have the address as the detailTextLabel, use a UILabel thats supports as many lines as you need, for an address, 3 should be enough. Together with Paul Bently's answer, you should be good to go.
Implement:
- (float)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
in your table delegate. For the different row and section in the passed indexPath, return a different height to fit your label.
Ensure 'adjust to fit' for the label in IB is not ticked (property adjustsFontSizeToFitWidth if done using code).
I just ran into this same problem. I found an alternative solution that doesn't require quite as much hardcoding, and allows you to make modifications to the label in Interface Builder that will be reflected in the height calculation.
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.section == 0 && indexPath.row == 1) {
NSString *text = [atmAnnotation address];
UITableViewCell* cell = [tableView dequeueReusableCellWithIdentifier:#"MyIdentifier"];
cell.textLabel.text = text;
[cell.textLabel sizeToFit];
CGFloat height = MAX( 44, cell.textLabel.bounds.size.height );
return height + CELL_CONTENT_MARGIN * 2;
}
return 44.0f;
}
The only downside is that this will cause some additional temporary UITableViewCell allocations, but the table view will immediately reclaim them, so I don't think it should be a big problem.

Resources