SUMMARY
Given that we don't always know what the frame of a cell or its content view is going to be (due to editing, rotation, accessory views etc.), what is the best way to calculate the height in tableView:heightForRowAtIndexPath: when the cell contains a variable height text field or label?
One of my UITableViewController's contains the following presentation:
UITableViewCell with UITextView.
UITextView should be the same width and height as UITableViewCell.
I created the UITableViewCell subclass, and then and initialized it with UITextView (UITextView is a private field of my UITableViewController)
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
static NSString *CellIdentifier = #"TextViewCell";
UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[BTExpandableTextViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier textView:_notesTextView] autorelease];
}
return cell;
}
I implemented the following method in my UITableViewCell subclass:
- (void)layoutSubviews{
[super layoutSubviews];
CGFloat height = [textView.text sizeWithFont:textView.font constrainedToSize:CGSizeMake(textView.frame.size.width, MAXFLOAT)].height + textView.font.lineHeight;
textView.frame = CGRectMake(0, 0, self.contentView.frame.size.width, (height < textView.font.lineHeight * 4) ? textView.font.lineHeight * 4 : height);
[self.contentView addSubview:textView];
}
and of course i implemented the following UITableViewDataSource method (look! I am using self.view.frame.size.width (but really i need UITableViewCell contentView frame width):
- (CGFloat)tableView:(UITableView*)tableView heightForRowAtIndexPath:(NSIndexPath*)indexPath{
CGFloat height = [_notesTextView.text sizeWithFont:_notesTextView.font
constrainedToSize:CGSizeMake(self.view.frame.size.width, MAXFLOAT)].height;
CGFloat groupedCellCap = 20.0;
height += groupedCellCap;
if(height < [BTExpandableTextViewCell minimumTextViewHeightWithFont:_notesTextView.font]){
height = [BTExpandableTextViewCell minimumTextViewHeightWithFont:_notesTextView.font];
}
return height;
}
also I implemented the following method (thats not so important but ill post it anyway, just to explain that cell's height is dynamical, it will shrink or expand after changing text in UITextView)
- (void)textViewDidChange:(UITextView *)textView{
CGFloat height = [_notesTextView.text sizeWithFont:_notesTextView.font
constrainedToSize:CGSizeMake(_notesTextView.frame.size.width, MAXFLOAT)].height;
if(height > _notesTextView.frame.size.height){
[self.tableView beginUpdates];
[self.tableView endUpdates];
}
}
And now, my question is:
After loading view, UITableViewController is calling methods in the following order: (ill remove some, like titleForHeaderInSection and etc for simplification)
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
- (CGFloat)tableView:(UITableView*)tableView heightForRowAtIndexPath:(NSIndexPath*)indexPath{
and only then
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
Look! I should return the correct UITableViewCell height before cellForRowAtIndexPath!
That means: I don't know UITableViewCell contentView frame. And i can't get it programmatically.
This width can be one of:
iPhone plain table, portrait orientation
iPhone plain table, landscape orientation
iPhone grouped table, portrait orientation
iPhone grouped table, landscape orientation
and the same for the iPad ( another 4 values )
And don't forget that contentView frame can be smaller because of UITableViewCell accessoryType, or because of UITableView editing state. (for example if we have UITableViewCell with multiline UILabel of any height in any editing state and with any accessoryView)
So this problem is fundamental: I just can't get cell contentView frame width for constraining, because I should return this height before cell layouts contentView. (And this is pretty logical, by the way) But this contentView frame really matters.
Of course sometimes I can know this width exactly and "hardcode" it
(for example: UITableViewCellAccessoryDisclosureIndicator has 20 px width, and tableView cannot be in editing state, then I can write self.view.frame.size.width - 20 and the task is done)!
Or sometimes contentView is equal to UITableViewController's view frame!
Sometimes I'm using self.view.frame.width in -tableView:heightForRowAtIndexPath: method.. (like now, and it works pretty well, but not perfectly because of grouped UITableView, should subtract some constant values, and they are different for 2 devices * 2 orientations)
Sometimes I have some #defined constants in UITableViewCell (if I know width exactly)...
Sometimes I'm using some dummy pre-allocated UITableViewCell (what is just stupid, but sometimes is pretty elegant and easy for use)...
But I don't like anything of that.
What's the best decision?
Maybe i should create some helper class, that will be initialized with such parameters:
accessory views, device orientation, device type, table view editing state, table view style (plain, grouped), controller view frame, and some other, that will include some constants (like grouped tableView offset, etc) and use it to find the expected UITableViewCell contentView width? ;)
Thanks
Table view uses the tableView:heightForRowAtIndexPath: method to determine its contentSize before creating any UITableViewCellcells. If you stop and think about it, this makes sense, as the very first thing you would do with a UIScrollView is set its contentSize. I have run into a similar problem before, and what I've found is that it is best to have a helper function that can take the content going into the UITableViewCell and predict the height of that UITableViewCell. So I think you will want to create some sort of data structure that stores the text in each UITableViewCell, an NSDictionary with NSIndexPaths as keys and the text as values would do nicely. That way, you can find the height of the text needed without referencing the UITableViewCell.
Although you can calculate heights for labels contained in table view cells, truly dynamically, in '- layoutSubviews' of a UITableViewCell subclass, there's no similar way of doing this (that I know of) for cell heights in '- tableView:heightForRowAtIndexPath:' of a table view delegate.
Consider this:
- (void)layoutSubviews
{
[super layoutSubviews];
CGSize size = [self.textLabel.text sizeWithFont:self.textLabel.font
constrainedToSize:CGSizeMake(self.textLabel.$width, CGFLOAT_MAX)
lineBreakMode:self.textLabel.lineBreakMode];
self.textLabel.$height = size.height;
}
Unfortunately though, by the time '- tableView:heightForRowAtIndexPath:' is called, that is too early, because cell.textLabel.frame is yet set to CGRectZero = {{0, 0}, {0, 0}}.
AFAIK you won't be able to do this neither with content view's frame, nor summing up individual labels' frames...
The only way I can think of is to come up with a convenience class, methods, constants, or such that will try to cover up all possible width in any device orientation, on any device:
#interface UITableView (Additions)
#property (nonatomic, readonly) CGFloat padding;
#end
#implementation UITableView (Additions)
- (CGFloat)padding
{
if (self.formStyle == PTFormViewStylePlain) {
return 0;
}
if (self.$width < 20.0) {
return self.$width - 10.0;
}
if (self.$width < 400.0 || [[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
return 10.0;
}
return MAX(31.0, MIN(45.0, self.$width * 0.06));
}
#end
Also note that, recently we also have new iPhone 5's 4-inch width (568 instead of 480) in landscape orientation.
This whole thing is pretty disturbing, I know... Cheers.
Related
I have the a custom tableview cell with applied constraints but the first time the table is displayed the row height is not resized properly unless new cells are created, is there a way to do this without calling reloadData again?
Yes. This is actually an issue with self-sizing that you need to work around until it is fixed.
The problem is that when a cell is instantiated, its initial width is based on the storyboard width. Since this is different from the tableView width, the initial layout incorrectly determines how many lines the content actually would require.
This is why the content isn't sized properly the first time, but appears correctly once you (reload the data, or) scroll the cell off-screen, then on-screen.
You can work around this by ensuring the cell's width matches the tableView width. Your initial layout will then be correct, eliminating the need to reload the tableView:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
TableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell" forIndexPath:indexPath];
[cell adjustSizeToMatchWidth:CGRectGetWidth(self.tableView.frame)];
[self configureCell:cell forRowAtIndexPath:indexPath];
return cell;
}
In TableViewCell.m:
- (void)adjustSizeToMatchWidth:(CGFloat)width
{
// Workaround for visible cells not laid out properly since their layout was
// based on a different (initial) width from the tableView.
CGRect rect = self.frame;
rect.size.width = width;
self.frame = rect;
// Workaround for initial cell height less than auto layout required height.
rect = self.contentView.bounds;
rect.size.height = 99999.0;
rect.size.width = 99999.0;
self.contentView.bounds = rect;
}
I'd also recommend checking out smileyborg's excellent answer about self-sizing cells, along with his sample code. It's what tipped me off to the solution, when I bumped into the same issue you are having.
Update:
configureCell:forRowAtIndexPath: is an approach Apple uses in its sample code. When you have more than one tableViewController, it is common to subclass it, and break out the controller-specific cellForRowAtIndexPath: code within each view controller. The superclass handles the common code (such as dequeuing cells) then calls the subclass so it can configure the cell's views (which would vary from controller to controller). If you're not using subclassing, just replace that line with the specific code to set your cell's (custom) properties:
cell.textLabel.text = ...;
cell.detailTextLabel.text = ...;
I am trying to set height of UILabel dynamically in the UITableView. During the launch height update is not reflected but as soon as I scroll down and scroll up back, update can be seen.
At Launch
After Scroll down and Scrolling back up again - This what I need. See the change in text in front of player icon. I need the complete text at launch itself.
Here is the code that I am trying to use:
- (void) updateMessageTextForRow:(long)row ofCell:(ESGameStreamCellView *)cell
{
NSString *item = _gameFeedItems[row];
NSString *title = item ?: NSLocalizedString(#"[No Title]", nil);
cell.message.preferredMaxLayoutWidth = cell.message.bounds.size.width;
// Update message label height
CGSize maximumLabelSize = CGSizeMake(296, FLT_MAX);
CGSize expectedLabelSize = [title sizeWithFont:cell.message.font
constrainedToSize:maximumLabelSize
lineBreakMode:cell.message.lineBreakMode];
//adjust the label the the new height.
CGRect newFrame = cell.message.frame;
newFrame.size.height = expectedLabelSize.height;
cell.message.frame = newFrame;
NSLog(#"Message = %#, Height: %f", title, cell.message.frame.size.height);
}
During Custom TableCellView Initialization
- (void)awakeFromNib {
// Initialization code
self.backgroundColor = [UIColor clearColor];
_message.lineBreakMode = NSLineBreakByWordWrapping;
_message.numberOfLines = 0;
}
Code for the row height
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
// Currently fixed height. Will be calculating dynamic height after adjusting the views.
return 300;
}
Are you using autoLayout ? I highly suspect that your constraints aren't set up properly: the UIImageView top should by tied with the titleLabel's bottom.
Also, you should use the new property for dynamic row : rowHeight and estimatedRowHeight.
You are getting the correct height after the cell's reuse : set the preferredMaxLayoutWidth property in the viewDidLayoutSubviews inside your custom cell class.
firstly, you should understand of the working flow of tableview delegate in objective c. Your cell height & position will be fixed after init. that's why you have to define each row's height in
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
// Currently fixed height. Will be calculating dynamic height after adjusting the views.
return 300;
}
From this point forward, your cell's height will be fixed, even if you re-config the frame.
The best practice is some article called "Dynamic height tableview cell" and you can easily find it here
http://www.raywenderlich.com/87975/dynamic-table-view-cell-height-ios-8-swift
I learn on above article (thanks for Joshua Greene) and re-write it to another library allow you to make a dynamic tableview easily. You can find it here
https://github.com/EugeneNguyen/XBMobile
it's not too perfect, but hope that if can help.
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.
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.
I am trying to resize my UITableViewCell's frame via:
[cell setFrame:CGRectMake(cell.frame.origin.x,
cell.frame.origin.y,
cell.frame.size.width,
cell.frame.size.height+25)];
however, it's not resizing after I do this... why is this?
This is weird as if I add a UIToolBar into the cell, it resizes but when I am adding a UIView, it doesn't:
[cell.contentView addSubview:sideSwipeView];
Here's the long and short of it:
Your cell width is determined by the width of the tableview it's in.
[EDIT: If it's a grouped table view, the cell is 20 - 60 pixels narrower than the tableview width, depending if you're using an iPhone, or an iPad.]
Your cell height is determined by the heightForRowAtIndexPath method.
If you're manually setting the cell's frame, it's going to be useless except when you're using a subclassed cell where you want to add subviews based on the cell's dimensions.
Even in this case, it's recommended to get the cell's frame from the tableview by using rectForRowAtIndexPath:(NSIndexPath*)indexPath method and then setting that frame as the cell's frame (after setting the frame's origin Y as 0).
I'm not quite sure about the UIToolBar, but your subview's frame won't change on changing the cell frame.
Maybe if you could tell us what you're trying to achieve, we can suggest a solution for you?
--------------------EDIT--------------------
So you need to dynamically add a subview to a cell on tapping it and resize it's height according to the new subview. This is gonna get hairy so here goes:
In your .h file declare:
BOOL subviewAdded;
In your .m file, in the init, do:
subviewAdded = NO;
Let's assume that you want the cell's height to be 50 without the subview and 100 with the subview. Accordingly, your heightForRow method should be:
- (CGFloat)tableView:(UITableView *)tableView
heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return (subviewAdded?100.0f:50.0f);
}
This means that initially since subviewAdded is NO, all your cells will have the smaller height.
Now, to add a subview to a cell on tapping it, and to change it's height dynamically, do this in your didSelectRow method:
- (void)tableView:(UITableView *)tableView
didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
//Get the cell at this indexPath
UITableViewCell *thisCell = [tableView cellForRowAtIndexPath:indexPath];
if(subviewAdded)
{
subviewAdded = NO;
for(int i = 0; i < [thisCell.contentView.subviews count]; i++)
{
UIView *thisSubview = [thisCell.contentView.subviews objectAtIndex:i];
[thisSubview removeFromSuperview];
}
}
else
{
UIView *someView = [[UIView alloc] initWithFrame:someFrame];
[thisCell.contentView addSubview:someView];
[someView release];
subviewAdded = YES;
}
NSMutableArray *array = [NSMutableArray array];
[array addObject:indexPath];
[tableView reloadRowsAtIndexPaths:array
withRowAnimation:UITableViewRowAnimationFade];
}
So what's going to happen here is you're adding a subview to this cell you've tapped. Reloading this cell will call heightForRowAtIndexPath and do a nice little fade animation and change your tableview heights.
IMPORTANT: Ideally, you should maintain an array of NSNumbers with boolean values. The array size should be the same size as the number of tableview cells you have.
In heightForRow, you would then check against this array instead of using a single boolean for the whole tableView. This would ensure that you could have different heights for different cells.
That would look something like:
- (CGFloat)tableView:(UITableView *)tableView
heightForRowAtIndexPath:(NSIndexPath *)indexPath {
BOOL thisBool = (BOOL)[[booleanArray objectAtIndex:indexPath.row] boolValue];
return (thisBool?100.0f:50.0f);
}
I didn't post all that code here since it's implied and what I've posted should put you well on your way to doing the boolean array thing.
Anyway, there you are. I just tested this code myself so it works :)
If you want to increase the height of your cell based on some parameter eg. text, image,
you must implement the heightForRowAtIndexPath method of UITableViewDelegate in your code.
- (CGFloat)tableView:(UITableView *)tableView
heightForRowAtIndexPath:(NSIndexPath *)indexPath