iOS 8 - autoresizing cells don't always show all label text - uitableview

basically I'm using autoresizing custom table view cells to display data on a table view and they resize perfectly, usually. The cells have a UILabel on them to display the data and the cells autoresize if the devices text size is changed. I'm sometimes having troubles with the cells autoresizing to show all of the UILabel's text. For example if the text size is medium sized, it sometimes doesn't fully display all the text of a longer label, it will show most of it then show "..." but if I increase or decrease the text size it will show it all but it might do the same thing for a different cell.
Any suggestions? Here's my code that calls the autoresize:
- (void)viewDidAppear:(BOOL)animated
{
[self.tableView reloadData];
[self retrieveFromParse];
self.tableView.estimatedRowHeight = 100;
self.tableView.rowHeight = UITableViewAutomaticDimension;
}
and to autoresize depending on text size:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"CellIdentifier";
_cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier forIndexPath:indexPath];
if (_cell == nil)
{
_cell = [[TableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
}
PFObject *object = [_postsArray objectAtIndex:indexPath.row];
NSString *nameString = [object objectForKey:#"Name"];
_cell.cellLabel.text = [NSString stringWithFormat:#"Posted by %#", nameString];
_cell.cellPostLabel.text = [NSString stringWithFormat:#" %#", [object objectForKey:#"Post"]];
//The following lines are to auto resize when the text size is changed
_cell.cellLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleFootnote];
_cell.cellPostLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleSubheadline];
return _cell;
}

Try using autoresizingMask if that helps
_cell.cellLabel.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleRightMargin;

Auto Layout has trouble figuring out how to draw multi-line labels if you don't set preferredMaxLayoutWidth on each one. The trouble is finding out what value to use since tables can change widths across the various iPhone & iPad devices, orientations, etc. To fix this, I usually subclass UILabel to override layoutSubviews:
- (void)layoutSubviews {
[super layoutSubviews]; // 1
self.preferredMaxLayoutWidth = CGRectGetWidth(self.bounds); // 2
[super layoutSubviews]; // 3
}
The first call to super correctly determines how wide the label should be.
Using the correct width, set preferredMaxLayoutWidth to the correct value.
Do another layout pass to render the label with its final and correct properties.

If you use autolayout, I have solution for you. I'm not sure that this is the perfect one, but here it is (Swift):
func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell
{
// configure your cell
cell.layoutIfNeeded()
}
P.S. if I doesn't use layoutIfNeeded and if I rotate my device and turn it back, to portrait, label layouts as it must e.g. 2 or more strings in one cell.
Didn't find reason why this occur, because (I use more that one prototype cell in my project) other type cells is okay, but their labels have simular constraints and settings.

override func viewDidLoad() {
super.viewDidLoad()
tableView.rowHeight = UITableViewAutomaticDimension
tableView.estimatedRowHeight = 66
}
works just fine on 8.4.1 for UITableViewCellStyle.Default cells

Related

Resize UITableViewCell containing UITextView upon typing

I have an UITableViewController that contains a custom cell. Each cell was created using a nib and contains a single non-scrollable UITextView. I have added constraints inside each cell so that the cell adapts its height to the content of the UITextView. So initially my controller looks like this :
Now I want that when the user types something in a cell its content automatically adapts. This question has been asked many times, see in particular this or the second answer here. I have thus written the following delegate in my code :
- (BOOL) textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString*)text {
[self.tableView beginUpdates];
[self.tableView endUpdates];
return YES;
}
However it leads to the following strange behavior : all constraints are ignored and all cells height collapse to the minimal value. See the picture below:
If I scroll down and up the tableView in order to force for a new call of cellForRowAtIndexPath, I recover the correct heights for the cells:
Note that I did not implement heightForRowAtIndexPath as I expect autoLayout to take care of this.
Could someone tell me what I did wrong or help me out here ? Thank you very much !
Here is a swift solution that is working fine for me. Provided you are using auto layout, you need assign a value to estimatedRowHeight and then return UITableViewAutomaticDimension for the row height. Finally do something similar to below in the text view delegate.
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.estimatedRowHeight = 44.0
}
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
// MARK: UITextViewDelegate
func textViewDidChange(textView: UITextView) {
// Calculate if the text view will change height, then only force
// the table to update if it does. Also disable animations to
// prevent "jankiness".
let startHeight = textView.frame.size.height
let calcHeight = textView.sizeThatFits(textView.frame.size).height //iOS 8+ only
if startHeight != calcHeight {
UIView.setAnimationsEnabled(false) // Disable animations
self.tableView.beginUpdates()
self.tableView.endUpdates()
// Might need to insert additional stuff here if scrolls
// table in an unexpected way. This scrolls to the bottom
// of the table. (Though you might need something more
// complicated if editing in the middle.)
let scrollTo = self.tableView.contentSize.height - self.tableView.frame.size.height
self.tableView.setContentOffset(CGPoint(x: 0, y: scrollTo), animated: false)
UIView.setAnimationsEnabled(true) // Re-enable animations.
}
My solution is similar to #atlwx but a bit shorter. Tested with static table. UIView.setAnimationsEnabled(false) is needed to prevent cell's contents "jumping" while table updates that cell's height
override func viewDidLoad() {
super.viewDidLoad()
self.tableView.estimatedRowHeight = 44.0
}
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
return UITableViewAutomaticDimension
}
func textViewDidChange(_ textView: UITextView) {
UIView.setAnimationsEnabled(false)
textView.sizeToFit()
self.tableView.beginUpdates()
self.tableView.endUpdates()
UIView.setAnimationsEnabled(true)
}
Tested on iOS 12
I really tried a lot of solutions and finally found a good one here
This works with animation and looks beautiful. The trick was the DispatchQueue.async block.
I also used TPKeyboardAvoidingTableView to make sure the keyboard doesn't overlap anything.
func textViewDidChange(_ textView: UITextView) {
// Animated height update
DispatchQueue.main.async {
self.tableView?.beginUpdates()
self.tableView?.endUpdates()
}
}
UPDATE
I got strange jumping issues because of TPKeyboardAvoidingTableView. Especially when I scrolled to the bottom and then a UITextView got active.
So I replaced TPKeyboardAvoidingTableView by native UITableView and handle the insets myself. The table view is does the scrolling natively.
The following example works for dynamic row height as the user types text into the cell. Even if you use auto layout you still have to implement the heightForRowAtIndexPath method. For this example to work constraints must be set to textView in such a way that if cell height increases textView will also grow in height. This can be achieved by adding a top constraint and bottom constraint from textView to cell content view. But do not set height constraint for textView itself. Also enable scrolling for the textView so that textView's content size will be updated as the user enters text. Then we use this content size to calculate the new row height. As long as the row height is long enough to vertically stretch the textView to equal to or greater than its content size the text view will not scroll even if scroll is enabled and that is what you need I believe.
In this example I have only a single row and I use only a single variable to keep track of the row height. But when we have multiple rows we need a variable for each row otherwise all the rows will have the same height. An array of rowHeight that corresponds to the tableView data source array may be used in that case.
#interface ViewController ()
#property (nonatomic, assign)CGFloat rowHeight;;
#property (weak, nonatomic) IBOutlet UITableView *tableView;
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.rowHeight = 60;
}
#pragma mark - UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return 1;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell1"];
return cell;
}
#pragma mark - UITableViewDelegate
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return self.rowHeight;
}
#pragma mark - UITextViewDelegate
- (void)textViewDidChange:(UITextView *)textView {
[self.tableView beginUpdates];
CGFloat paddingForTextView = 40; //Padding varies depending on your cell design
self.rowHeight = textView.contentSize.height + paddingForTextView;
[self.tableView endUpdates];
}
#end
Using Swift 2.2 (earlier versions would likely work too), if you set the TableView to use auto dimensions (assuming you're working in a subclassed UITableViewController, like so:
self.tableView.rowHeight = UITableViewAutomaticDimension
self.tableView.estimatedRowHeight = 50 // or something
You just need to implement the delegate in this file, UITextViewDelegate, and add the below function, and it should work. Just remember to set your textView's delegate to self (so, perhaps after you've dequeued the cell, cell.myTextView.delegate = self)
func textViewDidChange(textView: UITextView) {
self.tableView.beginUpdates()
textView.frame = CGRectMake(textView.frame.minX, textView.frame.minY, textView.frame.width, textView.contentSize.height + 40)
self.tableView.endUpdates()
}
Thanks to "Jose Tomy Joseph" for inspiring (enabling, really) this answer.
I've implemented a similar approach using a UITextView however to do so I had to implement heightForRowAtIndexPath
#pragma mark - SizingCell
- (USNTextViewTableViewCell *)sizingCell
{
if (!_sizingCell)
{
_sizingCell = [[USNTextViewTableViewCell alloc] initWithFrame:CGRectMake(0.0f,
0.0f,
self.tableView.frame.size.width,
0.0f)];
}
return _sizingCell;
}
#pragma mark - UITableViewDelegate
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
self.sizingCell.textView.text = self.profileUpdate.bio;
[self.sizingCell setNeedsUpdateConstraints];
[self.sizingCell updateConstraintsIfNeeded];
[self.sizingCell setNeedsLayout];
[self.sizingCell layoutIfNeeded];
CGSize cellSize = [self.sizingCell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
return cellSize.height;
}
sizingCell is an instance of the cell that is only used for sizing calculations.
What's important to note is that you need to attach the UITextView's upper and lower edge to the UITableViewCells contentView's upper and lower edge so that as the UITableViewCell changes in height the UITextView also changes in height.
For constraint layout I use a PureLayout (https://github.com/smileyborg/PureLayout) so the following constraint layout code may be unusual for you:
#pragma mark - Init
- (id)initWithStyle:(UITableViewCellStyle)style
reuseIdentifier:(NSString *)reuseIdentifier
{
self = [super initWithStyle:style
reuseIdentifier:reuseIdentifier];
if (self)
{
[self.contentView addSubview:self.textView];
}
return self;
}
#pragma mark - AutoLayout
- (void)updateConstraints
{
[super updateConstraints];
/*-------------*/
[self.textView autoPinEdgeToSuperviewEdge:ALEdgeLeft
withInset:10.0f];
[self.textView autoPinEdgeToSuperviewEdge:ALEdgeTop
withInset:5.0f];
[self.textView autoPinEdgeToSuperviewEdge:ALEdgeBottom
withInset:5.0f];
[self.textView autoSetDimension:ALDimensionWidth
toSize:200.0f];
}
Inspired by the two previous answers, I found a way to solve my problem. I think the fact that I had a UITextView was causing some troubles with autoLayout. I added the following two functions to my original code.
- (CGFloat)textViewHeightForAttributedText: (NSAttributedString*)text andWidth: (CGFloat)width {
UITextView *calculationView = [[UITextView alloc] init];
[calculationView setAttributedText:text];
CGSize size = [calculationView sizeThatFits:CGSizeMake(width, FLT_MAX)];
return size.height;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
UIFont *font = [UIFont systemFontOfSize:14.0];
NSDictionary *attrsDictionary = [NSDictionary dictionaryWithObject:font forKey:NSFontAttributeName];
NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:self.sampleStrings[indexPath.row] attributes:attrsDictionary];
return [self textViewHeightForAttributedText:attrString andWidth:CGRectGetWidth(self.tableView.bounds)-31]+20;
}
where in the last line 31 is the sum of my constraints to the left and right sides of the cell and 20 is just some arbitrary slack.
I found this solution while reading this this very interesting answer.
The trick to immediately update the tableview cells height in a smooth way without dismissing the keyboard is to run the following snippet to be called in the textViewDidChange event after you set the size of the textView or other contents you have in the cell:
[tableView beginUpdates];
[tableView endUpdates];
However this will may not be enough. You should also make sure the tableView has enough elasticity to keep the same contentOffset. You get that elasticity by setting the tableView contentInset bottom. I suggest this elasticity value to be at least the maximum distance you need from the bottom of the last cell to the bottom of the tableView. For instance, it could be the height of the keyboard.
self.tableView.contentInset = UIEdgeInsetsMake(0, 0, keyboardHeight, 0);
For more details and some useful extra features around this matter please check out the following link:
Resize and move UITableViewCell smoothly without dismissing keyboard
The solution almost everyone suggested is the way to go, I will add only a minor improvement. As a recap:
Simply set the estimated height, I do it via storyboard:
Make sure you have the constraints for the UITextView correctly set within the cell.
In the func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell
I simply call:
cell.myTextView.sizeToFit()
Previously beginUpdates/endUpdates were the advertised solution.
Since iOS 11, performBatchUpdates is what has been recommended source.
Calling performBatchUpdates after making a change to a cell's content works for me.
Check out the Objective C solution I have provided in the following link below.
Simple to implement, clean, and no need for auto layout. No constraints needed. Tested in iOS10 and iOS11.
Resize and move UITableViewCell smoothly without dismissing keyboard

Effective long text drawing in UITableViewCell

I'm fetching an array of comments, which I need to display in a tableView.
The tableView has custom cells for displaying content. The problem is that comments text can vary in its length and I'm facing a serious memory growth while scrolling the tableView.
Several approaches I've tried to display the text:
UILabel;
UITextView;
drawing on a custom view;
Besides the tableView seems to not be dequeueing my custom cells, so the memory keeps growing while the tableView is being scrolled (my cells are now having 1.5К symbols of text)
Cells are created like this, so nothing special
- (UITableViewCell*) tableView: (UITableView*) tableView
cellForRowAtIndexPath: (NSIndexPath*) indexPath
{
[tableView registerClass: [CommentCell class] forCellReuseIdentifier: kCommentCellId];
CommentCell* cell = [tableView dequeueReusableCellWithIdentifier: kCommentCellId
forIndexPath: indexPath];
Comment* comment = [self.comments objectAtIndex: indexPath.row];
cell.comment = comment;
return cell;
}
Custom setter for comment property
- (void) setComment: (Comment*) aComment
{
self.commentLabel.text = aComment.comment;
[self setNeedsLayout];
}
Adding comment label inside the cell
- (id) initWithStyle: (UITableViewCellStyle) style
reuseIdentifier: (NSString*) reuseIdentifier
{
self = [super initWithStyle: style reuseIdentifier: reuseIdentifier];
if (self)
{
// Comment label
//
self.commentLabel = [UILabel new];
self.commentLabel.textColor = [UIColor colorWithRed: 0.21f green: 0.21f blue: 0.21f alpha: 1.00f];
self.commentLabel.font = [UIFont helveticaNeueRegularOfSize: 13.33f];
self.commentLabel.numberOfLines = 0;
self.commentLabel.lineBreakMode = NSLineBreakByWordWrapping;
[self.contentView addSubview: self.commentLabel];
}
return self;
}
The problem is presumably that you are wrongly retaining and never releasing cell objects in code you are not showing, such as whatever it is you are doing to calculate the varying row heights.
It has nothing to do with the labels themselves, or anything having to do with displaying text of varying heights. I've made tables whose rows vary in height with text of varying length displayed in labels or text drawn directly, and there's no such leakage.
You might want to look over the section from my book on this topic.
The problem was that I somehow was setting the tableview frame height to the height of tableviews' content, so in fact all the cells were visible - thus not being reused.
Thanks #matt for pushing me in the right direction.

UITableViewCell with UIImage, width not updating on initial displayed cells

I would like to dynamically adjust the width of a UIImage inside of a UITableViewCell, I'm using the storyboard to design the UITableViewCell, I just added a label and an image, the properties get updated correctly, I'm even loading the value of the width into the label to show that it's the correct value, for the image, I'm loading a background image that I want to repeat, but the image won't update the width initially, if I scroll up and down, the images are shown as expected, here's the code for the cellForRowAtIndexPath, I've also tried to put the code on the willDisplayCell method, same result
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"mycustomcell"];
int r = [[data objectAtIndex:indexPath.row] intValue];
UIImageView *img = (UIImageView *)[cell viewWithTag:2];
img.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:#"some_img" ofType:#"png"]]];
CGRect frame = img.frame;
frame.size.width = r*16;
img.frame = frame;
int n = img.frame.size.width;
UILabel *label = (UILabel *)[cell viewWithTag:1];
label.text = [NSString stringWithFormat:#"custom %d", n];
[cell setNeedsDisplay];
return cell;
}
I just want this to work initially as it works after scrolling, thoughts?
The dynamic resizing of contents of a tableview cell is a well known problem. While there are kludgy workarounds, I believe proper solution depends upon whether you're using autolayout or not:
If using auto layout, make sure that your cell's image view has a width constraint, and then you can change the constraint's constant:
for (NSLayoutConstraint *constraint in img.constraints)
{
if (constraint.firstAttribute == NSLayoutAttributeWidth)
constraint.constant = r*16;
}
Frankly, I'd rather use a custom UITableViewCell subclass and have an IBOutlet for the width constraint (e.g. imageWidthConstraint), and it saves you from having to enumerate through the constraints to find the right one, and you can simply:
cell.imageWidthConstraint.constant = r*16;
If not using auto layout, you should subclass UITableViewCell, use that for your cell prototype's base class, and then override layoutSubviews, and resize the image view there. See Changing bounds of imageView of UITableViewCell.
Regardless of which approach you adopt, using a UITableViewCell subclass eliminates the need to use viewForTag construct, which makes the view controller code a little more intuitive.
argh, removing Auto Layout fixed the problem

Dynamic Height UILabel works after moving offscreen

I am trying to create multi-line dynamic UILabels in UITableViewCells. I have a custom UITableViewCell that has a 'comment' label. The cell and the label are created in storyboard.
I can compute the heights of the UITableViewCells properly based on the multi-line data to be stored in the UILabel (using heightForRowAtIndexPath). However, my problem lies in the actual UILabel content. The UILabel content will display only 1 line of data on table load. However, once a cell containing multiline UILabel data moves offscreen and comes back on screen, the multi-line data appears properly in the UILabel with multiple lines. Is there any way to fix this so that the multi-line data appears properly on table load?
- (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
CustomCell *cCell = (CustomCell *)cell;
MyObject = [myArray objectAtIndex:indexPath.row];
cCell.commentLabel.frame = CGRectMake(65.0f, 28.0f, 243.0f, 200.0f);
cCell.commentLabel.text = MyObject.multi_line_text_data;
cCell.commentLabel.adjustsFontSizeToFitWidth = NO;
cCell.commentLabel.autoresizingMask = UIViewAutoresizingFlexibleWidth;
cCell.commentLabel.font = [UIFont systemFontOfSize:13.0];
cCell.commentLabel.lineBreakMode = NSLineBreakByWordWrapping;
cCell.commentLabel.numberOfLines = 0;
[cCell.commentLabel sizeToFit];
}
Thanks!
Since you're doing this in the storyboard, you can set the necessary label properties there (lineBreakMode and number of lines). Just give the label a specific width constraint and constraints to the top, bottom, and left sides of the cell. Then, in code use sizeWithFont:constrainedToSize:lineBreakMode: in heightForRowAtIndexPath: to calculate the appropriate height for the cell based on the content of the label -- the label, because of its constraints, will expand along with the cell to the proper size. Something like this:
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
CGSize rowSize = [self.theData[indexPath.row] sizeWithFont:[UIFont systemFontOfSize:14] constrainedToSize:CGSizeMake(260, CGFLOAT_MAX) lineBreakMode:NSLineBreakByWordWrapping];
return rowSize.height + 30;
}
Here, 260 was the width I gave my label in IB, and the 30 is a fudge factor (determined empirically) to account for padding above and below the label.
I met the same problems. Unchecking Autolayout can fix it.

Dynamic UITableView Cell Height Based on Contents

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;}

Resources