UITableViewCell autosizing with content hugging - ios

What I want to achieve is autosizing cell with hugging UILabel inside it.
I was able to make it work, until I started the hugging part.
By hugging I mean making UILabel as small as possible if text is really short.
I prepared a really simple example of my problem:
http://s000.tinyupload.com/index.php?file_id=11087347944522931899
After I set trailing constrain to be "500" priority (lower then hugging which is 750) everything was fine.
Right now it seems that sizing cell is decreasing a size of UILabel, but never increases and all height calculation are really wrong.
After first really short cell comes in - all after this one are broken:
From debugging I found out that once UILabel hits 8px, it will gets bigger anymore and all height calculation are done for 8px width.
I found a work around and resetting preferredWidth after cell is dequeued for whatever reason helps, but I'm not sure if there are any better ways of solving the problem.
My workaround is comments in attached project (line 55, line 81) ChatTableController.m and I'm looking for proper solution for that.

Firstly, set the trailing constraint on your label to be >=20. This lets the label shrink.
Your real problem though is this method:
- (void)setBounds:(CGRect)bounds {
[super setBounds:bounds];
if (self.numberOfLines == 0 && bounds.size.width != self.preferredMaxLayoutWidth) {
self.preferredMaxLayoutWidth = self.bounds.size.width;
[self setNeedsUpdateConstraints];
}
}
You would be better to set cell.theLabel.preferedMaxLayoutWidth when you create the cell in both heightForRowAtIndexPath and cellForRowAtIndexPath.
If you comment this code out completely it all seems to work.
You can also make your code work if you create a new prototype cell every time heightForRowAtIndexPath is called. This is what led me to the above function.
It seems that every time heightForRowAtIndexPath is called, the reuse of the prototype cell results in a combinatorial increase in height. You see this if you scroll down and back up.
So I would remove this method completely and let the constraints do the work for you. I see the cells shrink when one line to exact size and everything else compact.
Before you set the text in createCellForRowAtIndexPath add the following to make the layout width full screen minus your 20 margins:
cell.messageLabel.preferredMaxLayoutWidth=self.view.bounds.size.width-40;
and before you set the text in the prototype cells in heightForRowAtIndexPath:
prototypeCell.messageLabel.preferredMaxLayoutWidth=self.view.bounds.size.width-40;
There will be an issue with rotation caused by setting the preferredMaxLayoutWidth. Setting this per cell means that each cell has constraints specific now to an orientation. To combat this you have to reload the table on rotation by adding the following to your view controller:
- (void) willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
{
[self.tableView reloadData];
}
Final result:

What I would recommend to do is to pre-calculate cell heights by measuring text bounds. The easiest and worst performant way of doing that is using - (CGRect)boundingRectWithSize:(CGSize)size options:(NSStringDrawingOptions)options attributes:(NSDictionary *)attributes context:(NSStringDrawingContext *)context.
Ideally you would like to calculate bounds as quick as possible. CoreText will help you to approach that.
A good example of using CoreText to pre-calculate text bounds/layout would be this GitHub repo.
Let me know if you need more info/help on that.

Related

UITableViewCell prevents UILabel from expanding height

I have a TableViewController with a custom UITableViewCell containing a single UILabel. The label will receive variable lengths of text, and should resize in height accordingly. I want to use auto layout, iOS10++.
However, it seems that the cell is preventing the label from expanding its height.
I have constraints on the label to pin top, bottom, left and right to the cell's contentView.
The label number of lines = 0, and is set to line break mode = WordWrap.
I have set the self.tableview.rowHeight to UITableViewAutomaticDimension, and have set the estimated row height to various sizes with no success.
I have increased (and decreased) the label's content hugging priority and and the vertical compression resistance, but this has no effect.
This sounds like a duplicate of so many other questions, but none I have read has solved my problem.
Some clues I have noticed:
1) If I remove the label's bottom constraint, the label expands correctly, but (of course) the cell doesn't expand, so the label cannot be fully seen after it expands below the bottom of the cell. So I conclude that the cell is preventing the label from expanding.
2) if I rotate the tableview to landscape and back to portrait, the first cell expands correctly. So something that occurs during the rotation solves the problem at least for the first cell, and also proves that the cell and label can expand as required.
I feel something is not right, but cannot figure it out. I am very close to going back to the old version of calculating the height manually and returning it in heightForRowAtIndexPath delegate method.
I would appreciate any suggestions. Thanks in advance.
I finally figured it all out.
In summary, I was configuring the cell (including setting the label's text) in tableView willDisplayCellAtIndexPath...
But it seems (obvious really) that for the autoresizing to work, the cell must be configured in tableView cellForRowAtIndexPath.
Moving the configuration into cellForRowAtIndexPath and suddenly everything started working perfectly.
Hope this helps anybody who has the same problem. I struggled with it for days.

Vertical compression ambiguity in auto layout sized UITableView rows

I'm using auto layout to get the heights of rows in UITableView, as described here. That part works.
I stack some labels, so they look like this in Interface Builder:
The constraints are what you'd expect, V:|-lab1-lab2-lab3-|. Problem is, it thinks that's ambiguous and demands you change vertical content hugging priorities.
It shouldn't be ambiguous. The labels should define the height of the row. But IB seems to be taking the height of the row, which you can drag to whatever you want in IB, as something that will constrain the labels, when really at run time the intent is for the label content to determine the height of the row. In this image the row height is slightly larger than it should be at run time, so the green label is getting pulled away from it's text content, and IB starts asking about content hugging priorities. Normally I'd resolve this problem by clicking "Update Frames", which would pull the row height down to the label heights, but "Update Frames" has no effect on row heights in UITableView.
Is there a standard way to resolve this? I would think people hit this all the time if they use auto layout for row heights.
The issue here is that your constraints doesn't specify any explicit heigh for each labels and it seems that the heigh of your cell is fixed to some value.
First you need to let the table view use UITableViewAutomaticDimension in the method heightForRowAtIndexPath
like this in obj-c :
-(CGFloat) tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
return UITableViewAutomaticDimension;
}
- (CGFloat) tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return UITableViewAutomaticDimension;
}
Then you need to fix height of your labels :
Either by setting them equal to each other. That way your cell will be able to calculate it's size.
Or you allow them to expend by setting one label content hugging priority to low (this is what you are currently doing)
In your case it seems that you want each label to have the same height, so the first solution should work ;)
I found a solution that worked for me. I'm sorry that the following explanation is hand-wavey, but it's better than I can do myself.
It's addressed here (https://useyourloaf.com/blog/table-view-cells-with-varying-row-heights/) under the section Auto Layout of the Prototype Cell.
Auto layout does not know that we will be adjusting the size of the content view to ensure that both labels fit. It therefore expects a hint over which label should expand or compress first to fit the space. To remove the warning lower the vertical compression resistance and increased the vertical hugging priority of the line number label so that it wants to stay at its intrinsic size.

iOS: UITableViewCell - Cells interrupting/overlaying eachother

I've build a UITableView with different costum cells. One is with a UIImageView and one without. But they are overlaying each other. I've set all constraints.
every cell owns its own section.
screens:
(image and text are samples.)
Does the actual cell height change?
If so, you need to make sure you're returning the correct height with:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return heightOfCell;
}
It looks like you might have the constraints wrong on the cell without the image as it is drawing beyond where the table thinks the height suggests it should end. Hence why it is drawing beyond where the second cell top starts.
Possible issues are:
0) You are returning a height from heightForRowAtIndexPath which is too small. Use auto layout for iOS8 or if suppoting iOS7, you will need to calculate the height.
1) Perhaps your constraints are wrong. Have you pinned the bottom text to the bottom of the cell contentView and the top of the date to the top of the contentView?
2) If using iOS8 auto layout, try adding [self.tableView reloadData]; in viewWillAppear. Sometimes it needs an extra kick to get the layout right, especially if text needs resized. I have no explanation for this other than I had to do this and I've seen it mentioned a few times elsewhere.

Multiple UILabels inside a self sizing UITableViewCell

In this iOS 8 app I'm creating, I have a tableview and I need them to be self resizing. I implemented it using Auto Layout and it works. Almost. Here's how it looks now.
There are 3 labels inside a cell. Main label which has the lorem ipsum text. Subtitle which has the string of numbers (Those are two separate labels. Might be confusing because they have the same color.) Then the third label with the small black text.
The first label resized itself correctly with no problem and the second label moves up and down accordingly. But the problem is with the third small label. As you can see, its not resizing itself to fit all the text.
Now there's a weird thing happening. I turn it landscape and here's it is.
Since there is space the label is displaying the entire text its supposed to. Fine. Then I turn it back to portrait.
Now the small label has resized itself to fit all its text but it overflows the cells boundaries. I tried making the cell bigger but it didn't work. Since this is self sizing cells, I don't think that's the correct way even.
I'm not getting any errors or even warning on my auto layout constraints either.
I have set these two lines of code in the viewDidLoad() method.
tableView.estimatedRowHeight = 100
tableView.rowHeight = UITableViewAutomaticDimension
Can anyone please tell me what I might be doing wrong here?
Since its difficult to answer just by looking at images and I don't have any more code to post beside the above snippet, I uploaded a runnable Xcode project demonstrating the issue here. (There are 2 custom cells. Basically its the same cell just the height is increased in the second one.)
I've been fiddling with auto layout constraints but I can't seem to get this working. Any help would be appreciated.
Thank you.
UPDATE:
With the help of this tutorial I found some helpful pointers. According to it, each subview should have constraints that pin all its sides and there should be constraints that goes from top to bottom which helps auto layout to calculate the height of the cell. In my original post, I had vertical spaces between each label so I think that's the reason auto layout couldn't calculate the proper height.
So I made some changes.
I reduced the vertical space between labels to 0 and set the Vertical space constraints between top and middle labels and middle and bottom labels.
I added leading, top, trailing constraints to the top label.
Leading and trailing to the middle label.
Leading, bottom, trailing to the bottom label.
Now here's another weird part. When I first run it, the bottom label cropping issue is still there.
But if I rotate the device to landscape and turn it back to portrait, all the all the cells are resized properly to fit both labels!
Still can't figure out why this doesn't happen at first though. Updated Xcode project is here.
The issue here is with the multi-line labels' preferredMaxLayoutWidth property. This is the property that tells the label when it should word wrap. It must be set correctly in order for each label's intrinsicContentSize to have the correct height, which is ultimately what Auto Layout will be using to determine the cell's height.
Xcode 6 Interface Builder introduced a new option to have this property set to Automatic. Unfortunately, there are some serious bugs (as of Xcode 6.2/iOS 8.2) where this is not set correctly/automatically when loading a cell from a nib or Storyboard.
In order to work around this bug, we need to have the preferredMaxLayoutWidth set to be exactly equal to the final width of the label once it is displayed in the table view. Effectively, we want to do the following before returning the cell from tableView:cellForRowAtIndexPath::
cell.nameLabel.preferredMaxLayoutWidth = CGRectGetWidth(cell.nameLabel.frame)
cell.idLabel.preferredMaxLayoutWidth = CGRectGetWidth(cell.idLabel.frame)
cell.actionsLabel.preferredMaxLayoutWidth = CGRectGetWidth(cell.actionsLabel.frame)
The reason that just adding this code alone doesn't work is because when these 3 lines of code execute in tableView:cellForRowAtIndexPath:, we are using the width of each label to set the preferredMaxLayoutWidth -- however, if you check the width of the labels at this point in time, the label width is totally different from what it will end up being once the cell is displayed and its subviews have been laid out.
How do we get the label widths to be accurate at this point, so that they reflect their final width? Here's the code that makes it all come together:
// Inside of tableView:cellForRowAtIndexPath:, after dequeueing the cell
cell.bounds = CGRect(x: 0, y: 0, width: CGRectGetWidth(tableView.bounds), height: 99999)
cell.contentView.bounds = cell.bounds
cell.layoutIfNeeded()
cell.nameLabel.preferredMaxLayoutWidth = CGRectGetWidth(cell.nameLabel.frame)
cell.idLabel.preferredMaxLayoutWidth = CGRectGetWidth(cell.idLabel.frame)
cell.actionsLabel.preferredMaxLayoutWidth = CGRectGetWidth(cell.actionsLabel.frame)
OK, so what are we doing here? Well, you'll notice there are 3 new lines of code added. First, we need to set this table view cell's width so that it matches the actual width of the table view (this assumes the table view has already been laid out and has its final width, which should be the case). We're effectively just making the cell width correct early, since the table view is going to do this eventually.
You'll also notice that we're using 99999 for the height. What's that about? That is a simple workaround for the problem discussed in detail here, where if your constraints require more vertical space than the current height of the cell's contentView, you get a constraint exception that doesn't actually indicate any real problem. The height of the cell or any of its subviews doesn't actually matter at this point, because we only care about getting the final widths for each label.
Next, we make sure that the contentView of the cell has the same size as we just assigned to the cell itself, by setting the contentView's bounds to equal the cell's bounds. This is necessary because all of the auto layout constraints you have created are relative to the contentView, so the contentView must be the correct size in order for them to get solved correctly. Just setting the cell's size manually does not automatically size the contentView to match.
Finally, we force a layout pass on the cell, which will have the auto layout engine solve your constraints and update the frames of all the subviews. Since the cell & contentView now have the same widths they will at runtime in the table view, the label widths will also be correct, which means that the preferredMaxLayoutWidth set to each label will be accurate and will cause the label to wrap at the right time, which of course means the labels' heights will be set correctly when the cell is used in the table view!
This is definitely an Apple bug in UIKit that we have to workaround for now (so please do file bug reports with Apple so they prioritize a fix!).
One final note: this workaround will run into trouble if your table view cell's contentView width doesn't extend the full width of the table view, for example when there is a section index showing on the right. In this case, you'll need to make sure that you manually take this into account when setting the width of the cell -- you may need to hardcode these values, something like:
let cellWidth = CGRectGetWidth(tableView.bounds) - kTableViewSectionIndexWidth
cell.bounds = CGRect(x: 0, y: 0, width: cellWidth, height: 99999)
I met the same issue as you and I found a simple solution to resolve it.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// dequeue cell...
// do autolayout staffs...or if the autolayout rule has been set in xib, do nothing
[cell layoutIfNeeded];
return cell;
}
And the self-sizing worked well. In my code, I laid two labels in vertical, both of them are dynamic height. The height of cell is correctly set to contain the two labels.
Assuming you don't have any errors with your constraints as others have suggested, this problem seems to stem from using a UILabel that allows multiple lines in conjunction with a UITableViewCellAccessory. When iOS lays out the cell and determines the height, it does not account for the offset change in width that occurs because of this accessory, and you get truncation where you wouldn't expect to.
Assuming you want the UILabel to extend the full width of the content view, I wrote up a method that fixes this for all font sizes
-(void)fixWidth:(UILabel *)label forCell:(UITableViewCell *)cell {
float offset = 0;
switch ([cell accessoryType]) {
case UITableViewCellAccessoryCheckmark:
offset = 39.0;
break;
case UITableViewCellAccessoryDetailButton:
offset = 47.0;
break;
case UITableViewCellAccessoryDetailDisclosureButton:
offset = 67.0;
break;
case UITableViewCellAccessoryDisclosureIndicator:
offset = 33.0;
break;
case UITableViewCellAccessoryNone:
offset = 0;
break;
}
[label setPreferredMaxLayoutWidth:CGRectGetWidth([[self tableView]frame]) - offset - 8];
}
Simply put this in your cellForRowAtIndexPath
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
#Setup the cell
...
// Fix layout with accessory view
[self fixWidth:[cell label] forCell:cell];
return cell;
}
Do this for any labels that are going to have multiple lines to adjust the width properly and then recalculate the appropriate heights. This works with dynamic font sizes as well.
Like smileyborg had mentioned, if you weren't using the full width of the contentView you could reference the constraints and subtract them from the width as well.
Edit: I previously was running 'layoutIfNeeded' on the cell but this was creating performance issues and didn't seem to be needed anyway. Removing it hasn't caused any problems for me.
You have two problems here.
1/ Right now, using Visual Format Language, your cell's vertical constraints can be translated like this:
Cell: "V:|-(10)-[nameLabel]-(67)-|"
Then, you set a second group of constraints:
Cell: "V:|-(10)-[nameLabel]-(8)-[pnrLabel]-(2)-[actionsLabel]"
Those two groups of constraints can't mix well and will reveal their ambiguity with your second problem.
2/ For some reasons, actionsLabel is limited to one line when you launch your app. Then, when you rotate your device to landscape mode, actionsLabel accepts to be displayed with two lines or more. Then, when you rotate your device back to portrait mode, actionsLabel keeps displaying two lines or more. But, because actionsLabel is not really part of your cell's height constraints, it overlap your cell's boundaries.
If you want to solve all those problems, I recommend first that you rebuild your xib file from scratch. This will cure your actionsLabel strange behavior (two lines or more only when you rotate your device).
Then, you will have to define your constraints like this:
Cell: "V:|-(10)-[nameLabel(>=21)]-(8)-[pnrLabel(>=21)]-(2)-[actionsLabel(>=21)]-(10)-|"
Of course, you can define other minimum height constraints for you labels than (>=21). In the same way, your bottom margin can be set to another value than -(10)-.
Addendum
In order to answer your question, I created a simple project with the previous constraints pattern in my .xib file. The following image may help you build your own constraints.
I tried the very easy and elegant looking solution of "fogisland" - and it did not work. Luckily I found out that one additional line makes it work in all directions. Just tell the system that you not only suggest a new layout (layoutIfNeeded), you explicitly ask for it (setNeedLayout)
cell.setNeedsLayout()
cell.layoutIfNeeded()
return cell

iOS - Redraw TableViewCells with Dynamic Cell heights on rotation

How can I go about getting my cells to redraw once the device is rotated?
When I am testing on iPhone, the default orientation is portrait, so my dynamic cells are as tall as they need to be when the device is in portrait orientation: this means when I rotate to landscape, there is a lot of empty space in the cell. When I test on iPad, it's the opposite. I get part of my text cut off when rotating to portrait orientation, as the default is landscape. I have separate storyboards for both devices, and both view controllers are set to "Orientation: Inferred"
(Portrait)
(Landscape)
This is what's happening on iPhone - the stretching label has a dark red background so you can see the actual size of the label. However, the cell height is not recalculating and since the bottom labels are constrained to the bottom of the cell, they are all the way at the bottom by themselves. If I add a constraint from the stretching label (the one with the background) to the bottom of the cell, all it does is stretch the label like so:
(Label is stretched)
What this tells me is that right now I'm just unable to get the cell to know what the orientation is and to redraw once it changes to fit. Here is my heightForRowAtIndexPath code that is calculating the height of the cell:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (!self.opCell)
{
self.opCell = [self.container dequeueReusableCellWithIdentifier:#"OPCell"];
}
self.opCell.opName.text = [[self.data objectAtIndex:indexPath.row] valueForKey:#"name"];
[self.opCell layoutIfNeeded];
CGFloat height = [self.opCell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
return height + 1;
}
So far to get it to rotate I have tried [self.container reloadData], where container is my TableView with willRotateToInterfaceOrientation and didRotateToInterfaceOrientation to no avail. I also tried putting it in viewDidLoad, so as to maybe detect and load when the view is loaded in landscape mode with the correct height, but this didn't work either. For iPhone, no matter what I do it loads the height for portrait view and does not update on rotation.
I have found a few questions that I thought might help, but I can't seem to get this answer working for me, even though it seems like the same question I have, and this one seems to focus more on cell width than height and I'm not sure that it applies in the same way.
Here is even a tutorial that, when I ran it on the iPhone, is also not resizing the TableViewCells on rotation.
This tutorial seemed to have some insight into changing table heights, so I added the begin/endUpdates code into my didRotateToInterfaceOrientation, thinking that might cause a change, but this didn't work as well.
So, if anyone has any suggestions or knows a solution, it would be much appreciated, as I'm hitting a brick wall with this one!
It seems you are doing a word wrap here. So as per my prior experience I suggest you to :
1) use sizeWithFont or the newer iOS7 version with appropriate linebreakmode.
2) You can implement layoutSubview for your "opCell" where you can redefine the frame size.
3) Calculate the cell height during the rotation.. ensure that you properly set the width as per the new tabelView Bounds.
4) Also you can implement estimatedHeightForRowAtIndexPath for quick estimated size for table reload operation.
Hope this helps !!
So I figured out my issue. Following this tutorial I found out that I was missing.
I had to:
Subclass my label that was stretching and state that preferredMaxLayout is meeting the screen width
In my heightForRowAtIndexPath I need to tell the cell to get and update the current width of the screen
See the tutorial for more details. Hope this question and answer helps someone along in the future, because this was extremely troublesome.
You could use the same trick as here. That is calling:
[tableView beginUpdates];
[tableView endUpdates];
Without anything in between. This will trigger heigh recalculation for the table view.

Resources