How can I do variable height table cells on the iPhone properly? - iphone

My app needs to have variable height table cells (as in each table cell differs in height, not that each cell needs to be able to resize itself).
I have a solution that currently works, but it's kludgy and slow.
My Current Solution:
Before the table cells are rendered, I calculate how high each cell needs to be by calling sizing methods such as -sizeWithFont:constrainedToSize: on its data. I then add up the heights, allow for some padding and store the result with the data.
Then when my UITableViewDelegate receives the -tableview:heightForRowAtIndexPath: I work out which item will be rendered for that cell and return the height that I calculated previously.
As I said, this works, but calling -sizeWithFont:constrainedToSize: is very slow when you're doing it for hundreds of items sequentially, and I feel it can be done better.
So for this to work, I had to maintain two parts of code - one that would calculate the cell heights, and one that would actually draw the cells when the time comes.
If anything about the model item changed, I had to update both of these chunks of code, and now and again they still don't even match up perfectly, sometimes resulting in table cells that are slightly too small for a given item, or too large.
My Proposed Solution:
So I want to do away with the precalculating the cell height. A) because it breaks the MVC paradigm and B) because it's slow.
So my cell draws itself, and as a result, ends up with the correct cell height. My problem is that I have no way of telling the table view the height of the cell before its drawn - by which time its too late.
I tried calling -cellForRowAtIndexPath: from within -tableView:heightForRowAtIndexPath: but this gets stuck in an infinite loop, since the first calls the second at some point, and vice versa (at least this is what I saw when I tried it).
So that option is out of the question.
If I don't specify a size in the height for row delegate method, then the table view goes screwwy. The cells are the perfect height, but their x position is that of cells of fixed heights.
Messed Table Cells http://jamsoftonline.com/images/messed_table_cells.png
Notice how the bottom cell is the correct size - it's just overlapping the previous cell, and the previous cell overlaps its previous, and so on and so forth.
Also using this method, while scrolling there is some artifacting occurring which I think may be related to the reuse identifier for the cells.
So any help here would be gratefully appreciated.

Here's what I use. NSString has a method that will tell you the dimensions of a textbox based on the font information and the height/width constraints you give it.
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *text = [self getTextForIndexPath:indexPath];
UIFont *font = [UIFont systemFontOfSize:14];
CGSize size = [self getSizeOfText:text withFont:font];
return (size.height + 11); // I put some padding on it.
}
Then you write a method pull the text for this cell...
- (NSString *)getTextForIndexPath:(NSIndexPath *)indexPath
{
NSString *sectionHeader = [self.tableSections objectAtIndex:[indexPath section]];
NSString *sectionContent = [self.tableData objectForKey:sectionHeader];
return sectionContent;
}
And this is to get the size of the text.
- (CGSize)getSizeOfText:(NSString *)text withFont:(UIFont *)font
{
return [text sizeWithFont:font constrainedToSize:CGSizeMake(280, 500)];
}

Just a thought:
What if you had, say, six different types of cells each with their own identifier and a fixed height. One would be for a single-line cell, the other for a two-line cell, etc...
Every time your model changes, calculate the height for that row then find the nearest cell type that has height closest to what you need. Save that celltype identifier with the model. You can also store the fixed row height for that cell in the model so you can return it in the tableview:heightForRowAtIndexPath call (I wouldn't get too hung up on forcing it to calculate inside the cell class itself--technically it's not part of the cell drawing functionality and more something the tableview uses to decide which cell type to create).
At runtime, when asked to return a cell for that row all you need to do is create (or obtain from the cell cache) a cell with the celltype identifier, load the values and you're good to go.
If the cell height calculation is too slow, then you could pull the same trick the tableview cache does and do it only on-demand when the cell comes into view. At any given time, you would only have to do it for the cells in view, and then only for a single cell as it scrolls into view at either end.

I realise this won't work for you due to the infinite loop you mention, but I've had some success with calling the cells layoutSubViews method
Though this may be a little inefficient due to multiple calls to both cellForRowAtIndexPath and layoutSubViews, I find the code is cleaner.
-(float)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
MyCell *cell = (MyCell *)[self tableView:tableView cellForRowAtIndexPath:indexPath];
[cell layoutSubviews];
return CGRectGetHeight(cell.frame);
}
And in the layout code:
- (void)layoutSubviews
{
[super layoutSubviews];
//First expand the label to a large height to so sizeToFit isn't constrained
[self.myArbitrarilyLengthLabel setFrame:CGRectMake(self.myArbitrarilyLengthLabel.frame.origin.x,
self.myArbitrarilyLengthLabel.frame.origin.y,
self.myArbitrarilyLengthLabel.frame.size.width,
1000)];
//let sizeToFit do its magic
[self.myArbitrarilyLengthLabel sizeToFit];
//resize the cell to encompass the newly expanded label
[self setFrame:CGRectMake(self.frame.origin.x,
self.frame.origin.y,
self.frame.size.width,
CGRectGetMaxY(self.myArbitrarilyLengthLabel.frame) + 10)];
}

Related

Within cellForRowAtIndexPath getting height of cell when cells are variable height

I create custom cells within my tableview some have images and are tall some are just text. The height of the cells are calculated in heightForRowAtIndexPath, which I beleive is done before cellForRowAtIndexPath is called. I want to place an imageview at the bottom of the cell regardless of heigh, but I am not sure how to get the calculated height from within cellForRowAtIndexPath?
Too late for an answer..
But, like #user216661 pointed out, the problem with taking the height of the Cell or the ContentView is that it returns the cells original height. Incase of rows with Variable height, this is an issue.
A better solution is to get the Rect of the Cell (rectForRowAtIndexPath) and then get the Height from it.
- (UITableViewCell *)tableView:(UITableView *)iTableView cellForRowAtIndexPath:(NSIndexPath *)iIndexPath {
UITableViewCell *aCell = (UITableViewCell *)[iTableView dequeueReusableCellWithIdentifier:aCellIdentifier];
if (aCell == nil) {
CGFloat aHeight = [iTableView rectForRowAtIndexPath:iIndexPath].size.height;
// Use Height as per your requirement.
}
return aCell;
}
You can ask the delegate, but you'll be asking twice since the tableView already asks and sizes the cell accordingly. It's better to find out from the cell itself...
// in cellForRowAtIndexPath:, deque or create UITableViewCell *cell
// this makes the call to heightForRow... and sizes the cell
CGFloat cellHeight = cell.contentView.bounds.size.height;
// alter the imageView y position (assuming the rest of the frame is correct)
CGRect imageFrame = myImageView.frame;
imageFrame.y = cellHeight - imageFrame.size.height; // place the bottom edge against the cell bottom
myImageView.frame = imageFrame;
You are allowed to call heightForRowAtIndexPath yourself! Just pass the indexPath from cellForRowAtIndexPath as an argument and you can know the height of the cell you are setting up.
Assuming you are using a UITableViewController, just use this inside cellForRowAtIndexPath...
float height = [self heightForRowAtIndexPath:indexPath]

Trying to calculate frame size of UILabel based on the amount of text for subview of cell

The code below has code that determines the frame size of a UILabel, and I think it does work, however when I place it within my rowAtIndexPath for a UItable I get wonky results.
Perhaps, I dont fully understand how or what the reuseIdentifier does, but I placed the code to calculate the frame only when the cell is nil. What happens is that the heights are calculated only for the first three cells, then it repeats in sequence for the rest of the cells. For example, cell one's height is used for cell four's height.
Maybe someone can point me in the right direction as to how I should setup my calculations.
Thanks!
if(cell == nil){
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:DisclosureButtonCellIdentifier] autorelease];
//start adding custom subviews to the table cell
//addSubview for Description
UILabel *descValue = [[UILabel alloc] init];
NSString *descString = rowData.summary;
CGSize maximumSize = CGSizeMake(185, 130);
UIFont *descFont = [UIFont fontWithName:#"HelveticaNeue" size:12];
CGSize descStringSize = [descString sizeWithFont:descFont
constrainedToSize:maximumSize
lineBreakMode:descValue.lineBreakMode];
CGRect descFrame = CGRectMake(125, 60, 185, descStringSize.height);
descValue.frame = descFrame;
descValue.backgroundColor = [UIColor redColor];
descValue.font = descFont;
descValue.tag = kDescriptionValueTag;
descValue.lineBreakMode = UILineBreakModeWordWrap;
descValue.numberOfLines = 0;
[cell.contentView addSubview:descValue];
[descValue release];
}
UILabel *desc = (UILabel *)[cell.contentView viewWithTag:kDescriptionValueTag];
desc.text = rowData.summary;
Using NSString UIKit Additions, you can get the width of a string using a particular font:
[myString sizeWithFont:myFont];
There are several other functions in that Additions set that can help figure out how big a piece of text will be, with different layout options, for single and multi-line text, etc.
The purpose of the reuseIdentifier is to let you reuse a cell -- with particular subviews in particular places -- without the device having to spend the execution time to do all that layout. Definitely more useful back in the iPhone 1 days when the processor was much slower. Until you are more familiar with reuseIdentifiers, I would suggest you just create a new cell every time that function is called.
Each time the OS calls your cellForRowAtIndexPath, you need to fill out the content correctly. Anything that needs to get resized or changed depending on the row should be set.
Frankly, I did not try understanding your code completely. That is nearly impossible especially because you close the method with brackets without returning anything but cellForRowAtIndexPath which I assume you are referring to, requires the return of a UITableViewCell object.
Apparently we are looking only at some fraction of the code.
However, layouting a cell properly is not the full task. You need to tell the table the height of each cell.
You may want to implement
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
for that task. That method is good if the cell height varies.
If the cell hight is different from the standard but does not vary from cell to cell then setting of the rowHeight property of the tableView will be sufficient.
I think that in your case the implementation of the method heightForRowAtIndexPath is mandatory.
BTW, you may want to look at subclassing the UITableCellView and implement the layoutSubviews method in your subclass.
You didn't show all of your code but usually to implement tableView:cellForRowAtIndexPath: you start with the following:
-(UITableViewCell*)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
// get a cell from the queue of reusable cells, if any
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier: DisclosureButtonCellIdentifier];
if (cell == nil) {
// No cell to reuse, need to create a new one
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:DisclosureButtonCellIdentifier] autorelease];
// add cell subviews here
}
// fill in cell content and resize subviews here
...
return cell;
}
Once you have created 3 (in your case) cells they get reused as the table scrolls so you don't have to keep creating cells. The call to dequeueReusableCellWithIdentifier will return one of the previously created cells if it was not longer in use (it scrolled off the top or the bottom).

Get width/frame of entire UITableViewCell

I have a method where I have a (rendered) UITableViewCell*. I would like to know the width/frame of the entire cell.
cell.frame doesn't seem to work - it gives me the frame of a random cell. The closest I got is cell.contentView.frame but it doesn't include the area covered by the accessoryView (see http://developer.apple.com/library/ios/#documentation/UserExperience/Conceptual/TableView_iPhone/TableViewCells/TableViewCells.html#//apple_ref/doc/uid/TP40007451-CH7). cell.contentView.superview.frame doesn't work either - it gives me a frame at some arbitrary location just like cell.frame does.
cell.contentView.width + cell.accessoryView.width works except in cases where the accessory view is UITableViewCellAccessoryNone
Any ideas how I can get the entire frame/width of the UITableViewCell in all cases?
Try this,
NSLog(#"Cell Width = %f",cell.frame.size.width);
NSLog(#"Cell Height = %f",cell.frame.size.height);
It will show the current cell Width and Height.
This code gives you the frame of the particular cell selected by indexPath:
// Get the cell rect and adjust it to consider scroll offset
CGRect cellRect = [tableView rectForRowAtIndexPath:indexPath];
cellRect = CGRectMake(cellRect.origin.x - tableView.contentOffset.x, cellRect.origin.y - tableView.contentOffset.y, cellRect.size.width, cellRect.size.height);
cell.frame will always give you the frame of the cell you're messaging.
And even if it did return a random cell, you should still be able to use the width of the frame, because all the cells have the same width.
I think by default, a cell will use 100% of the width of the UITableView so a UITableView's width is equivalent to a cell's width and because you would likely have set your table cell height using:
[UITableView setRowHeight:tableCellHeight];
Where tableCellHeight is a number of your choice and UITableView is the name of your UITableView not the UITableView class itself.
Then you would have both the width and height of a table cell, or did I misinterpret your question?

How to limit the number of cells UITableview displays?

Im using a tableview to display some information in a quiz app that Im working on. My question is how do i make the tableview only show the number of cells that I need. Ive set the number of rows delegate method like this:
-(NSInteger) tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return 5;
}
but at the bottom of the table view are empty cells that are not needed. If I set the tableview style to grouped I get 5 cells and no empty ones below them. Ive seen that other people have done this but cant seem to work it out. I was wondering if they have somehow added a custom view to the table footer to cancel the empty cells out?
Any ideas or help appreciated.
If you do want to keep the separator, you could insert a dummy footer view. This will limit the tableview to only show the amount of cells you returned in tableView:numberOfRowsInSection:
self.tableView.tableFooterView = [[UIView alloc] initWithFrame:CGRectZero];
In swift:
self.tableView.tableFooterView = UIView(frame: CGRectZero)
A much nicer method which doesn't require cell resizing is to turn off the default separator (set the style to none) and then have a separator line in the cell itself.
I was having a similar problem, how to show only separators for the cells that contain data.
What I did was the following:
Disable separators for the whole tableView. You can do that in the
inspector for the tableview in Interface builder or by calling
[yourTableView setSeparatorStyle:UITableViewCellSeparatorStyleNone];.
Inside your cellForRowAtIndexPath where you populate your tableview with cells create a new UIView and set it as a subview to the cell. Have the background of this view lightgray and slightly transparent. You can do that with the following:
UIView *separatorView = [[UIView alloc] initWithFrame:CGRectMake:
(0, cell.frame.size.height-1,
cell.frame.size.width, 1)];
[separatorView setBackgroundColor:[UIColor lightGrayColor]];
[separatorView setAlpha:0.8f];
[cell addSubView:separatorView];
The width of this view is 1 pixel which is the same as the default separator, it runs the length of the cell, at the bottom.
Since cellForRowAtIndexPath is only called as often as you have specified in numberOfRowsInSection these subviews will only be created for the cells that possess data and should have a separator.
Hope this helps.
This worked for me - I had extra empty rows at the bottom of the screen on an iphone 5 -
In my case I needed 9 rows
- (CGFloat)tableView:(UITableView *)tabelView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return self.tableView.frame.size.height / 9;
}
You can implement heightForRowAtIndexPath: and compute the correct height to only show 5 cells on the screen.
Are you always going to have 5 rows? If it's a dynamic situation you should set the number of rows according to the datasource of the tableview. For example:
return [postListData count];
This returns the count of the records in the array holding the content.
The tableview is only going to display the number of rows and sections that you tell it to. If you're always going to have just a single section, DON'T implement the method below.
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 2;
}
Without this the tableview will only have 1 section. With it, as you would imagine, you can specify the number of sections.
It is quite Simple. Just set the size of the popover like this:
self.optionPickerPopOver.popoverContentSize = CGSizeMake(200, 200);
Certainly you can adjust the size (200,200) depending upon the size of contents and number if rows.
Easy way would be to shrink tableView size. I.e. 5 cells 20 points each gives 100.0f, setting height to 100.0f will cause only 5 rows will be visible. Another way would be to return more rows, but rows 6,7 and so would be some views with alpha 0, but that seems cumbersome. Have you tried to return some clerColor view as footerView?
I think u can try changing the frame of the table view, if you want to adjust with the number of cells.
Try something like this:
[table setFrame:CGRectMake(x, y, width, height*[list count])];
height refers to height of the cell
As Nyx0uf said, limiting the size of the cell can accomplish this. For example:
- (CGFloat)tableView:(UITableView *)tabelView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
CGFloat result;
result = 100;
return result;
}
implement these two methods in your UITableViewController:
- (UIView *)tableView:(UITableView *)tableView viewForFooterInSection:(NSInteger)section
{
if (section == tableView.numberOfSections - 1) {
return [[UIView alloc] initWithFrame:CGRectMake(0, 0, 1, 1)];
}
return nil;
}
- (CGFloat)tableView:(UITableView *)tableView heightForFooterInSection:(NSInteger)section
{
if (section == tableView.numberOfSections - 1) {
return 1;
}
return 0;
}
In fact, these codes are telling tableview that you don't need to render the seperator line for me anymore, so that it looks the empty cells won't be displayed(in fact , the empty cell can not be selected too)

Calculate custom cell height with multiple views iphone

I want to calculate my custom cell height with multiple subviews like labels, imageViews, buttons etc.
How do I calculate this cell size?
It seems it's not recommended to do it in cellForRowAtIndxPath, also taking an instance of Cell in heightForRowAtIndexPath, it goes in infinite loop.
I'm able to calculate the height for cell in the custom cell class but I'm not sure wheather where should I return it.
Since the method, heightForRowAtIndexPath doesn't get a cell instance, how to tell this method the type of cell for which I want to render the calculated height?
Any help is appreciated.
Thanx in advance.
Ideally, you should be able to calculate the height of the cell without having to create a cell for it. tableView:heightForRowAtIndexPath: will be called for every cell in your table (not just the ones on screen) and if you have to create a complete cell object in that method it will probably have a big impact on performance.
You have to derive the type of cell based on the indexPath argument, in the same way you do it in tableView:cellForRowAtIndexPath:
You could always load your cells in viewWillAppear or viewDidLoad, and then add them to an NSArray. Then, in tableView:heightForRowAtIndexPath:, using the provided indexPath, return the height of the relevant cell in your array. Then, in tableView:cellForRowAtIndexPath, again, using the provided indexPath, return the relevant cell in your array.
PLEASE NOTE:
If you have a lot of cells, this could cause your UI to lock up while it loads the cells. To avoid this, you could create a custom method to populate the array, and perform it on a background thread.
Also, if it takes too long, the tableView will not display all of your cells, as your array will not have been fully populated. This means that you should call [tableView reloadData]; after the array is populated.
This' how I did it:
I created an array of catgories of the cells in the order of their apearence in tableview called category_type and then I checked for the category types and calculated the height of the text-based controls to calculate the entire height of cell.
NSString *category = [NSString stringWithFormat:[category_type objectAtIndex:indexPath.row]];
if([category isEqualToString:#"xyz"])
{
xyzInstance = [arrayWithObjectsToLoad objectAtIndex:indexPath.row];
float textHeight = [MLUtils calculateHeightOfTextFromWidth:editorialInstance.eletitle : [UIFont fontWithName:#"Helvetica" size:13.0] :320 :UILineBreakModeWordWrap]; //Dyanamic label height
totalHt = 171 - 21 + textHeight + 20; // My calculation for text part of label
return totalHt;
}
I did this for all the categories.