I made a custom cell, placed a UITextView inside it and I want to change the height of the cell based on the length of UITextView's text length. I know how to statically change the cell height using heightForRowAtIndexPath, but I can't put my head around doing it dynamically, based on content.
I have read about a dozen topics on this forum and on several other, but I didn't find the answer I was looking for.
Any ideas?
in heightForRowAtIndexPath
float height = [cell.textView.text sizeWithFont:cell.textView.font constrainedToSize:CGSizeMake(cell.textView.frame.size.width, 10000)].height;
return height;
10000 it's max height of cell, actually you can set max integer value
In your textViewDidChange method, call
[tableView beginUpdates];
[tableView endUpdates];
This will trigger heightForRowAtIndexPath to recalculate all your cell sizes each time the user types a letter. Then in heightForRowAtIndexPath, you can then calculate the necessary size for your cell.
The sizeWithFont method will return the size needed to display the text in a UILabel, which is slightly different than that for a UITextView due to content insets, line spacing, etc. I've used a somewhat hacky solution in the past. If you create a temporary UITextView, set it's text, and use [UIView sizeThatFits:constraintSize] to get the size that will fit all its content within the constraints. (The documentation on this method is a little unclear - take a look at this answer for more info: question about UIView's sizeThatFits)
UITextView *temp = [UITextView alloc] initWithFrame:someArbitraryFrame];
temp.font = DISPLAY_FONT
temp.text = cell.textView.text;
//This gets the necessary size to fit the view's content within the specified constraints
CGSize correctSize = [self.temp sizeThatFits:CGSizeMake(CONSTRAINT_WIDTH, CONSTRAINT_HEIGHT)];
return correctSize.height
In interest of efficiency, you should probably store/lazy-load the temporary textView so that you're not creating a new UITextView for every cell, but rather re-using the same one to calculate height for different text.
Related
I have some pretty specific requirements on a layout that forces me to create a bunch of UILabels, use sizeThatFits and position them accordingly. These labels are then positioned in each cell of a UITableView. The height of the cell is determined by the size of the labels.
When generating each cell, the time it takes is acceptable. The problem comes when I add new data to the table. As the cell heights are dependent on the labels, what I currently do is create the labels for a cell and measure the resulting height. When I add 20 or so new objects, things get slow as tableView:heightForRowAtIndexPath: get called for all new objects at once. The second problem with this approach is that since it uses a bunch of UILabels, it has to run on the main thread, so doing the calculation beforehand is not really an option.
What I need is a good way to capture the behaviour of UILabel sizeThatFits, only faster, and preferrably able to run on a background thread.
It only needs to run on iOS 7.
Use this method to pass in an NSString and it will return the estimated size for a given font (I use system font size 15 here and 320 is the cell's width):
- (CGSize)sizeOfText:(NSString*)text{
NSAttributedString *attributedText = [[NSAttributedString alloc] initWithString:text attributes:#{NSFontAttributeName:[UIFont systemFontOfSize:15.0f]}];
CGRect rect = [attributedText boundingRectWithSize:(CGSize){320, CGFLOAT_MAX}
options:NSStringDrawingUsesLineFragmentOrigin
context:nil];
return CGSizeMake(rect.size.width, rect.size.height);
}
Then in your heightForRowAtIndexPath, this is how you set the size of the cell:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return [self sizeOfText:[yourdatasource objectAtIndex:indexPath.row]].height;
}
iOS 7 adds the method - (CGSize)sizeWithAttributes:(NSDictionary *)attrs to NSString that gives you the size of a string. Under iOS7, UILabel objects are generally very close in size to this value. It can be called on a background thread.
https://developer.apple.com/library/ios/documentation/UIKit/Reference/NSString_UIKit_Additions/Reference/Reference.html
You can also implement tableView:estimatedHeightForRowAtIndexPath: on your UITableViewDelegate. Providing an estimate the height of rows can improve the user experience when loading the table view. If the table contains variable height rows, it might be expensive to calculate all their heights and so lead to a longer load time. Using estimation allows you to defer some of the cost of geometry calculation from load time to scrolling time.
I have a UITableViewCell subclass which has an image, title and description.
I am supposed to resize the cell height according to the description content length i.e. if it spans more than 5 lines, I should extend it (+other subviews like image etc) till it lasts.
The next coming cells should begin only after that.
I have my UITableViewCell subclass instantiated from xib which has a fixed row height = 160.
I know this is pretty standard requirement but I am unable to find any guidelines.
I already extended layoutSubViews like this:
- (void) layoutSubviews
{
[self resizeCellImage];
}
- (void) resizeCellImage
{
CGRect descriptionRect = self.cellDescriptionLabel.frame;
CGRect imageRect = self.cellImageView.frame;
float descriptionBottomEdgeY = descriptionRect.origin.y + descriptionRect.size.height;
float imageBottomEdgeY = imageRect.origin.y + imageRect.size.height;
if (imageBottomEdgeY >= descriptionBottomEdgeY)
return;
//push the bottom of image to the bottom of description
imageBottomEdgeY = descriptionBottomEdgeY;
float newImageHeight = imageBottomEdgeY - imageRect.origin.y;
imageRect.size.height = newImageHeight;
self.cellImageView.frame = imageRect;
CGRect cellFrame = self.frame;
cellFrame.size.height = imageRect.size.height + imageRect.origin.y + 5;
CGRect contentFrame = self.contentView.frame;
contentFrame.size.height = cellFrame.size.height - 1;
self.contentView.frame = contentFrame;
self.frame = cellFrame;
}
It pretty much tells that if description is taller than image, we must resize the image as well as cell height to fit the description.
However when I invoke this code by doing this:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
cell.cellDescriptionLabel.text = #"Some long string";
[cell.cellDescriptionLabel sizeToFit];
[cell setNeedsLayout];
return cell;
}
It appears that while cell frame is changed due to layoutSubViews call, other cells do not respect it. That is, they appear on the same position had the previous cell would not have resized itself.
Two questions:
How to make it possible what I want?
Am I doing right by calling setNeedsLayout within cellForRowAtIndexPath?
P.S.: I know heightForRowAtIndexPath holds key to changing the cell height, but I feel that the data parsing (not shown here) that I do as part of cellForRowAtIndexPath would be an overkill just to calculate height. I need something that can directly tell the UITableViewCell to resize itself according to content needs.
-tableView:heightForRowAtIndexPath: is by design how variable sized cells are calculated. The actual frame of a cell is of no importance and is changed by the table view to fit its needs.
You are sort of thinking of this backwards. The delegate tells the table view how cells need to be drawn, then the table view forces cells to fit those characteristics. The only thing you need to provide to the cell is the data it needs to hold.
This is because a table view calculates all the heights of all the cells before it has any cells to draw. This is done to allow a table view to size it's scroll view correctly. It allows for properly sized scroll bars and smooth quick-pans through the table view. Cells are only requested when a table view thinks a cell needs to be displayed to the screen.
UPDATE: How Do I Get Cell Heights
I've had to do this a couple of times. I have my view controller keep a cell which is never used in the table view.
#property (nonatomic) MyTableViewCell *standInCell;
I then use this cell as a stand in when I need measurements. I determine the base height of the cell without the variable sized views.
#property (nonatomic) CGFloat standInCellBaseHeight;
Then in -tableView:heightForRowAtIndexPath:, I get the height for all my variable sized views with the actual data for that index path. I add the variable sized heights to my stand in cell base height. I return that new calculated height.
Note, this is all non-autolayout. I'm sure the approach would be similar, but not identical to this, but I have no experience.
-tableView:heightForRowAtIndexPath: is the preferred way to tell tableview the size of its cells. You may either precalculate and cache it in a dictionary and reuse, or alternatively in ios7, you can use -tableView:estimatedHeightForRowAtIndexPath: to estimate the sizes.
Take a look at this thread - https://stackoverflow.com/questions/18746929/using-auto-layout-in-uitableview-for-dynamic-cell-layouts-variable-row-heights, the answer points to a very good example project here - https://github.com/caoimghgin/TableViewCellWithAutoLayout.
Sorry, but as far as I know you have to implement tableView:heightForRowAtIndexPath:. Warning, in iOS 6 this gets called on every row in you UITableView right away, I think to draw the scrollbar. iOS7 introduces tableView:estimatedHeightForRowAtIndexPath: which if implemented allows you to just guess at the height before doing all the calculation. This can help out a lot on very large tables.
What I found works well is just have your tableView:heightForRowAtIndexPath: call cellForRowAtIndexPath: to get the cell for that row, and then query that cell for it's height cell.bounds.size.height and return that.
This works pretty well for small tables or in iOS7 with tableView:estimatedHeightForRowAtIndexPath implemented.
The Goal
I'm trying to create a dynamic message cell using auto-layout.
What I've Tried
The cell is positioning correctly, for the most part, with auto-layout given the following constraints:
The Problem
My first problem was the message label (Copyable Label) width was constrained. That seems to be resolved by using setPreferredMaxLayoutWidth: as described in this question.
Height is still a problem. As you can see, the message bubble is still cutting off. In addition, I'm not sure how to determine the message cell height for the table view.
I expected auto-layout to somehow just work. I've read the answer here, but it seems like a lot to of steps.
The Question
First, is a case where auto-layout is more complex than traditional frame arithmetic?
Second, using auto-layout, how can I determine the height of the resulting cell?
I fully use Auto Layout and what you speak about is kinda a problem.
I didn't want to modify the way intrinsic size is calculated for performance purpose of UITable.
So I used a very simple way that is correct in the end. It's ok if your cell is simple, can become such hard if your cell contains more than one variable text.
I defined my cells normally, where you can put a UILabel that fits the insets (no problem about it).
Then, in your table datasource, you define directly the height of the cell:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return [TEXTOFYOURCELL sizeWithFont:[UIFont systemFontOfSize:14] constrainedToSize:CGSizeMake(300, 1000)].height + 31; // Here it's defined for 15 of top and bottom insets, define +1 than the size of the cell is important.
}
EDIT :
Here some code about the UILabel in the cell (in init method).
__titleLabel = [UILabel new];
__titleLabel.numberOfLines = 0;
[self.contentView addSubview:__titleLabel]; // adding to contentView rather than self is very important !
[__titleLabel keepInsets:UIEdgeInsetsMake(0, 15, 0, 15)];
I use this API : https://github.com/iMartinKiss/KeepLayout to manage auto layout simpler.
This is possible on iOS 8 as can be read on AppCoda
Basically:
Set the label lines to 0.
Set the row height UITableViewAutomaticDimension
I want to use auto-layout for UITableViewCells. These table cells have a dynamic height (depending on text length).
I'm using [UIView -systemLayoutSizeFittingSize:] to calculate the appropriate cell height (to return in [UITableView -heightForRowAtIndexPath:]) but I keep getting the following results:
If I pass UILayoutFittingCompressedSize, I get back a CGSize of (0,0).
If I pass UILayoutFittingExpandedSize, my app crashes with this error:
*** Assertion failure in -[NSISLinearExpression incrementConstant:], /SourceCache/Foundation_Sim/Foundation-1043.1/Layout.subproj/IncrementalSimplex/NSISLinearExpression.m:620
(My guess is that this means some number is infinite.)
My implementation is simple. I calculate the height for each object, and then cache it:
MessageCell *cell = // allocate a new cell...
// set up the cell (assign text, images, etc)
CGSize size = [cell systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
self.cellHeight = size.height; // size always equals (0, 0)
I hypothesize that this is a problem with the constraints I set, but:
If I manually set cellHeight to a large value, the cells all look fine except the height is wrong.
Interface Builder gives me no warnings about ambiguous restraints
[cell hasAmbiguousLayout] returns NO.
My cell has, among other things, an image set at 48x48, so a size of (0, 0) shouldn't satisfy all the constraints.
Any ideas?
This is what works for me (note 'contentView')
CGSize size = [cell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
EDIT: I answered a similar question and provided a complete example, here:
How to resize superview to fit all subviews with autolayout?
It is hard to say something concrete basing on your post because you didn't post constraints that you use.
Apple Documentation:
systemLayoutSizeFittingSize:
Returns the size of the view that satisfies the constraints it holds.
Maybe you created constraints that can be interpreted in the way the size of the cell is equal to (0,0).
There is another way you can check the height of the cell with the text. You can put your text in the UITextView and then:
UITextView textView;
textView.text = #"l";
textView.font = [UIFont fontWithName:FONT_NAME size:FONT_SIZE];
//some code here
//
CGFloat cellHeight = textView.contentSize.height;
It is important to set the text and font (and every other property that can cause the change of the height of the UITextView) of the UITextView before using contentSize property. Also you must first add UITextView to the view.
////// EDIT
The problem with your approach with using constraints can be that you want to measure the cell which ISN'T added to the view so the system don't have all the informations it needs. It doesn't know how much space that will be for the space etc because it doesn't know the surrounding area of the cell
I'm writing a simple IRC client that I'm modeling after Twitter's iOS app appearance. I've taken a screenshot of the Twitter app, for reference:
It looks like a simple table view with a few labels inside of each cell. So, in my app, I am programmatically creating a table and the cell formatting. My custom cell has only two labels in it, which I have positioned one on top of the other. The top label is a simple 1-liner. The bottom label I would like to contain longer messages, and need it to word-wrap to multiple lines while staying within my specified width.
How do I achieve this?
So far, I've tried explicitly setting the frame of the label to the dimensions that I want, but it does not word-wrap, if this is all I do. It just flows out of the cell horizontally. I then tried calling sizeToFit, within the tableView:cellForRowAtIndexPath: function, for this label, but it appears to word-wrap at a very small width - the text wraps after like two or three letters and then flows out of the cell vertically.
I can't seem to figure out how to get the text within the label to wrap after a specified width. Any ideas?
My custom cell class: https://github.com/ryancole/pound-client/blob/master/pound-client/views/MessageListCell.m
The cellForRowAtIndexPath function: https://github.com/ryancole/pound-client/blob/master/pound-client/controllers/MessageListViewController.m#L62-L84
Edit 1:
To demonstrate what happened when I set numberOfLines to 0, for unlimited, I have attached a screenshots of that being called. It wraps after a few characters, instead of first taking up the specified width of the UILabel's frame. This is being set prior to called sizeToFit.
You need to set numberOfLines to the number of lines you want, or 0 which allows for an unlimited number of lines (the default is 1). You might also need to set the lineBreakMode to NSLineBreakByWordWrapping (although that might be the default).
After Edit: If you want the text to start at the top, then I think you'll have to use variable height cells, and not set an explicit size for your custom cell. I did it this way in one of my projects:
-(CGFloat) tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
UILabel *label = [[UILabel alloc] init];
label.numberOfLines = 0; // allows label to have as many lines as needed
label.text = _objects[indexPath.row][#"detail2"];
CGSize labelSize = [label.text sizeWithFont:label.font constrainedToSize:CGSizeMake(300, 300000) lineBreakMode:NSLineBreakByWordWrapping];
CGFloat h = labelSize.height;
return h + 50;
}
The label I create here, is just for calculating the height of the row, it's discarded after this method ends. The width of the cell is determined by the 300 argument I have in the constrainedToSize: parameter. The +50 was just a fudge factor I added to get my cells looking right -- you'd probably want to mess with that number to get what you want. In my custom cell class, I used initWithStyle:reuseIdentifier, and didn't set any size.