Dynamically sized views in UITableViewCell - ios

I have a UITableViewCell that has two views in it that are dynamically sized. How would I handle something like this in terms of the constraints used, where to actually set the dynamic heights (height for row? cell for index?). I've been cracking away at this for a few hours but can't seem to figure it out, thanks.

If the dynamic heights are just based on content, constraints like this "V:|[fx1][dh1][dh2][fx2]|" will get you unpredictable results. Autolayout will try it's best, but one of the dynamic views will "giveup" it's intrinsic size to fill up the space.
I think you have two options:
Give the first "dynamic" view a higher content hugging priority
This would perhaps work if you need all cells to be the same height in your table.
[self.dh1 setContentHuggingPriority:UILayoutPriorityRequired
forAxis:UILayoutConstraintAxisVertical];
[self.dh2 setContentHuggingPriority:UILayoutPriorityDefaultLow
forAxis:UILayoutConstraintAxisVertical];
Size the whole cell according to it's content
Don't think of it as having to fit stuff into the cell. Size the cell to fit it's content instead.
Use constraints like this: "V:|[fx1(20)][dh1][dh2][fx2(20)]" ... without 'anchoring' the last fixed view to the bottom
Then, in heightForRowAtIndexPath, layout out the cell in the background and measure it.
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
MyCell *cell = [tableView dequeueReusableCellWithIdentifier:#"CELL"];
// etc...
[cell setNeedsLayout];
[cell layoutIfNeeded];
return cell.fx2.frame.origin.y + cell.fx2.frame.size.height;
}

Related

UITableViewCell not shrinking to wrap UIImageView

I have a TableView whose first element is a fixed aspect ratio header image. I embedded a UIImageView inside the UITableViewCell's Content View. The desired outcome is the entire cell adopting the size of the image. I tried a couple of things, first of all, pinning all four edges of the UIImageView to the parent Content View and setting the aspect ratio to my desired value (2h by 3w). With this setup the aspect ratio constraint gets completely ignored and the image view takes up the height of the cell, which shrinks my asset. Next, I tried to remove the bottom constraint. As a result, the aspect ratio is respected, but the cell height is larger than that of the image view.
My question is, can I make the cell shrink to wrap the height of the image view using auto layout?
Can I make the cell shrink to wrap the height of the image view using autolayout?
Yes you can, but there is a trick.
UITableViews have a fixed height for all their cells. If you want variable height you must implement UITableViewDelegate method
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
If your cell constraints are properly setup (and by that I mean if instead of a cell you had a UIView, it's size would be defined by it's content), you will be able to do something like this:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
MyCell *cell = [tableView dequeueReusableCellWithIdentifier:kIdentifier];
// setup your cell image
return [cell systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
}
The problem is, if you are retrieving the images from the web, there is no possible way that you know the size of the image without loading it.
If you are using local images, this should work.

Adjust UITableViewCell's size to its contents

I'm trying to adjust a UITableViewCell's size to its content. This is basically for a chat view, where I list all previous messages and allow the user to scroll the conversation content.
So I have a UITableView with a few different cell prototypes. Incoming text messages, outgoing text messages, incoming image messages, outgoing image messages, and so on. Inside each cell's content view I have a standard UIView which I intent to use to draw the chat balloon. This view takes almost the cell's inner space (8px offset to the top, left, bottom, and right, all around). Inside that view I want the content. In the case of the text cells (incoming and outgoing) I want a UITextView which will display a text message. This is what I mean:
In yellow is the UIView and inside it the UITextView. Now I want to adjust everything to the text's size. I managed to accomplish the following:
sizeToFit accomplishes exactly what I need for the UITextView
I'm still not sure how to adjust the UIView's size to the UITextView's size.
To adjust the cell's height maybe I could use heightForRowAtIndexPath. I don't need (nor do I think I should) to adjust the cell's width. But a few regards on that: when is this method called? Will the cell already have been instantiated? Will it have already layed out the subviews? Otherwise, how can I tell the content's size?
Any input on this is appreciated!
Edit:
I managed to make a few progresses by following the tutorial posted by #vikingosegundo, but I'm stuck again. This is what I have:
So, basically: the text view has constraints for leading, trailing, distance to top, and distance to bottom. The containing view, on the other hand, has constraints for trailing and distance to top, so that if the size is small then it snaps to the right. I can't had leading constraints or otherwise it will always take the full width of the cell. I'm not sure about distance to bottom constraints.
When a enter a small message it looks great. It's well sized and it snaps to the right.
However, long messages don't span to several lines. Instead it still snaps to the right (OK), but the width grows to the left, indefinitely.
The cell is already adjusting its height to the content's height:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
HyConversationTableViewCell * cell = (HyConversationTableViewCell *)[self tableView:tableView cellForRowAtIndexPath:indexPath];
CGSize size;
[cell setNeedsLayout];
[cell layoutIfNeeded];
size = [cell.textMessageView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
return size.height + 32;
}
I'm guessing that what I need now is something like a text view's maximum width or something, but I realise that's not possible. How do I solve this?
Edit: If I had a leading constraint to the containing view it looks great when the text spans multiple lines, but not when it doesn't. Here's what it looks like:
And:
Edit: As suggested by Alex Zavatone, I changed tableView:heightForRowAtIndexPath: to the following:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
HyConversationTableViewCell * cell = (HyConversationTableViewCell *)[self tableView:tableView cellForRowAtIndexPath:indexPath];
CGSize overflowSize = CGSizeMake(cell.textMessageView.frame.size.width, FLT_MAX);
CGSize sizeAdjusted = [cell.textMessageView sizeThatFits:overflowSize];
return sizeAdjusted.height + 32.0f;
}
It shows a little better as the height is already adjusted, but the behaviour is somewhat erratic. Here's what it looks like at the beginning:
So the height is correct, but the text view does not adjust its width. Also, if I scroll the cells out of screen and then back in (which forces them to redraw) they start behaving erratically in what seems a random criteria. Here's a sample:
Sometimes this happens to the last two cells...
Edit: That last part was fixed by setting Content Hugging Priority and Content Compression Resistance Priority to required and the Intrinsic Size to Placeholder. Now the height shows properly.
If you are using iOS 8 you can use UITableViewAutomaticDimension.
You can check out this example
self.tableView.rowHeight = UITableViewAutomaticDimension;
You can take a look also on this video : What's New in Table and Collection Views in the 2014 WWDC.
Here how, we are doing that
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
AFMediaWithHeadlineCell *cell = [[[NSBundle mainBundle] loadNibNamed:#"AFMediaWithHeadlineCell"
owner:nil
options:nil] firstObject];
[cell loadText:#"Some text"];
return [cell height];
}
actually loadText: loads data into UI, and sizeToFits it.
and height is basic method that calculates cell's height
- (void)loadText:(NSString *)aText
{
self.textView.text = aText;
[self.textView sizeToFit];
}
- (CGFloat)height
{
return self.textView.frame.origin.y + self.textView.frame.size.height + 10; // 10 is margin
}
You can resize the text by setting the height to a large number and then using sizeThatFits on it.
Not sizeToFit.
Like so:
UILabel *label = self.prototypeCell.descriptiveText;
label.numberOfLines = 0;
CGSize sillyLargeHeight = CGSizeMake(label.frame.size.width, 9999);
CGSize labelFrameAdjustedForHeight = [label sizeThatFits:sillyLargeHeight];
return labelFrameAdjustedForHeight.height + 24.0; // 24 is 12 above and 12 below padding.
You can use a label or a textView. If you choose to use a UILabel, you'll need to set the # of lines to 0 so that it will be multiple line.
You can do this within - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
You'll also need to do the same adjustments to set the height on the field (use an IBOutlet) in the willDisplayCell method.

How to remove UITableView's NSAutoresizingMaskLayoutLayoutConstraint?

I designed a custom UITableViewCell by adding some subviews in the cell's contentView, I also added some auto layout constraints between the contentView and the subviews.
But when I debug the app, Xcode tells me that there is a constraint conflict. In the list of constraint, there is one NSAutoresizingMaskLayoutLayoutConstraint that limits the cell height to be 43, so Xcode break the constraint of my subview height and 'compress' it.
I have tried:
In Interface builder, uncheck the "autoreize subviews" checkbox. Doesn't work.
In code, cell.contentView.translatesAutoResizingMaskIntoConstraints = NO. This causes the app to crash with an exception: "Auto Layout still required after executing -layoutSubviews". I have tried every proposed solution in this question: "Auto Layout still required after executing -layoutSubviews" with UITableViewCell subclass None of them work for me.
So I guess I can only let the cell do its autoresizing thing, and remove the auto resizing constraint in code. How should I do it without breaking things?
EDIT:
Or, from another perspective, how I can make the tableViewCell height flexible (changes with subview height and constraints)? In IB, I have to set its height, right?
You DON'T need to set cell.contentView.translatesAutoResizingMaskIntoConstraints = NO
In order to get the flexible height in UITableViewCells in autolayout you need to manually compute for the height in - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
Yes this tedious, but there is no other way. To calculate the height automatically you need to fulfill two conditions in your UITableViewCell:
You must make sure all your subviews have
translatesAutoResizingMaskIntoConstraints=NO
Your cell subview's constraints must be pushing against the top and bottom edges of the UITableViewCell.
Then in your - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath, You need to recreate that cell for the specific indexPath and compute the height manually for that cell.
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath
*)indexPath
{
//Configure cell subviews and constraints
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"cell"];
[self configureCell:cell forIndexPath:indexPath];
//Trigger a layout pass on the cell so that it will resolve all the constraints.
[cell setNeedsLayout];
[cell layoutIfNeeded];
//Compute the correct size of the cell and get the height. This is where the magic happens.
CGFloat height = [cartItemCell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
height += 1.0f
return height;
}
Take note that systemLayoutSizeFittingSize is wonky with UITextViews. In that case you have to compute the height of the entire cell manually using another way. There are also some performance optimizations you can do by caching the height per indexPath.
This blog post has a a more detailed description on what to do but the gist is essentially what I mentioned above. : http://johnszumski.com/blog/auto-layout-for-table-view-cells-with-dynamic-heights
I have created some sample code for dynamic tableview cell height with auto layout. You can download the project using following link.
DynamicTableViewCellHeight
I hope this will help you.

Rearranging Cell Subviews When UITableViewCell Resizes

I have a UITableViewCell that is implemented using storyboard that looks like:
Here is what the cell should look like without an image:
I have been fiddling with the constraints and banging my head trying to figure this out but have had no luck. I have a pretty good understanding of constraints and how to add them programmatically but have had no luck with this specific problem and feel like I am just adding layout constraints to the cell willy-nilly with no logical thought process. The cell represents a newsfeed post which may or may not have an image in the main image view at the top, and should behave as follows. If the cell doesn't have an image in it the bottom bar with the like and comment counts, moves up to align with the top of the cell. I achieved this behaviour by setting a constraint that kept the smaller image view, post title, post time and the post content a set distance away from the bottom of the cell. This approach works and when the cell is resized in the heightForRowAtIndexPath method the subviews move appropriately. The problem comes when the text in the post content is larger then a single line. The height of the cell adjusts correctly but the top of the text view stays at the same location and grows downward and overflows into the next cell. When I place the constraints to align the four subviews with the top of the cell I run into issues when there is no image and the post content is larger then a single line. In this case, the cell resizes to be smaller than its original size and the subviews stay at the distance specified by the constraint. The smaller image, post title, time and content are clipped and don't display. This is such an odd problem with so many different cases. I have been working at this for almost two days and could really use someone else's thoughts on how to solve this issue. I hope this isn't too confusing, thanks for the help!
I have one way to solve this, but I'm sure there are many others. I gave both image views a fixed height constraint. The small image view and the top label (Post Title) have fixed heights to the top of the cell -- both of these as well as the height constraint of the large image view have IBOutlets to them so they can be changed in code. The bottom label (Post Content) has its number of lines set to 0, and has an IBOutlet to its height constraint (all the labels had the standard 21 point height to start). In code, I check for the existence of an image at each indexPath, and change the constraints accordingly.
- (void)viewDidLoad {
UIImage *image1 = [UIImage imageNamed:#"House.tiff"];
[super viewDidLoad];
self.theData = #[#{#"pic":image1, #"post":#"short post"},#{#"post":#"short post"},#{#"pic":image1, #"post":#"Long long post with some extra stuff, and even some more"},#{#"post":#"Long long post with some extra stuff, and even some more"}];
[self.tableView reloadData];
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.theData.count;
}
-(CGFloat) tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
CGFloat ivHeight = (self.theData[indexPath.row][#"pic"])? 215 : 0; // 215 is the fixed height of the large image view
CGSize labelSize = [self.theData[indexPath.row][#"post"] sizeWithFont:[UIFont systemFontOfSize:17] constrainedToSize:CGSizeMake(152, CGFLOAT_MAX)];
return 140 + ivHeight + labelSize.height; // the 140 was determined empirically to get the right spacing between the 3 labels and the bottom bar
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
RDCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
cell.label.text = self.theData[indexPath.row][#"post"];
cell.iv.image = self.theData[indexPath.row][#"pic"];
if(self.theData[indexPath.row][#"pic"] == nil){
cell.heightCon.constant = 0; // heightCon is the outlet to the large image view's height constraint
cell.ivTopCon.constant = 8; // ivTopCon is the outlet to the small image view's spacing to the top of the cell
cell.labelTopCon.constant = 8; // labelTopCon is the outlet to thetop label's spacing to the top of the cell
}else{
cell.heightCon.constant = 215; // this number and the following 2 are taken from the values in IB
cell.ivTopCon.constant = 185;
cell.labelTopCon.constant = 233;
}
CGSize labelSize = [self.theData[indexPath.row][#"post"] sizeWithFont:[UIFont systemFontOfSize:17] constrainedToSize:CGSizeMake(152, CGFLOAT_MAX)];
cell.labelHeightCon.constant = labelSize.height;
return cell;
}
Hey #rdelmar thanks for the solution! Eventually I ended up just designing two different cells in the storyboard file with different reuse identifiers but the same subclass. I then checked in the cellForRowAtIndexPath method if the cell had content or not, and assigned the correct identifier. If this is the incorrect way of doing this, or will cause problems down the road please let me no in the comments.

iOS the right way of heightForRowAtIndexPath calculation

I want to learn a common and right way of calculation of height for custom cells.
My cells are loaded from nib, they have two multiline UILabels one above other.
At the moment I create special configuration cell in viewDidLoad and use it in heightForRowAtIndexPath.
-(CGFloat) tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
[self configureCell:self.configurationCell forIndexPath:indexPath];
CGRect configFrame = self.configurationCell.frame;
configFrame.size.width = self.view.frame.size.width;
self.configurationCell.frame = configFrame;
[self.configurationCell layoutSubviews];
UILabel *label = (UILabel *)[self.configurationCell viewWithTag:2];
float height = label.frame.origin.y + label.frame.size.height + 10;
return height;
}
It works but seems to be a bad manner. I think that heights must be precalculated for each item and for each orientation. But I can't find a way to make it nice and straightforward.
Could you help me?
Cell Label's style (like font and offsets from the screen borders) must be loaded from nib file (cell is inside nib).
Added cell's layourSubviews method:
-(void) layoutSubviews {
[super layoutSubviews];
[self.shortDescriptionLabel resize];
CGRect longDescriptionFrame = self.longDescriptionLabel.frame;
longDescriptionFrame.origin.y = self.shortDescriptionLabel.frame.origin.y + self.shortDescriptionLabel.frame.size.height + 5;
self.longDescriptionLabel.frame = longDescriptionFrame;
[self.longDescriptionLabel resize];
}
resize method of label simply increases it's height to fit all the text. So height of cell is calculated as longDescriptionLabel.bottom + 10. Simple code but not very beautiful.
It appears that you are trying to create subviews inside heightForRowAtIndexPath. View creation is supposed to be done in cellForRowAtIndexPath.
According to your implementation, you can only determine the height of the cell after it's been laid out. This is no good because UITableView calls heightForRowAtIndexPath for every cell, not just the visible ones upon data reload. As a result, subviews of all cells are created even if they aren't required to be visible.
To optimize this implementation, you have to work out some kind of formula to allow determination of height without laying out views. Even if your layout is elastic or has variable height, given text rectangle, you can still determine its height. Use [NSString sizeWithFont:] to determine its displayed rectangle. Record this information in your data delegate. When heightForRowAtIndexPath is called, return it directly.

Resources