How to calculate the height of a custom expand/collapsable UITableViewCell dynamically - ios

I have an interesting scenario. I have some custom UITableViewCells designed in separate xib files. One of them have an UIImageView, which will load images whom size are not constant, so it means UIImageView's height must be flexible. The cell also has a UIView containing some UILabels, and UIView's size is constant, say 100. I want to expand and collapse the cells on didselectrowatindexpath event. To collapse the cell I have to hide the UIView that have some labels. Please guide me in this regard to achieve the goal.
Also my question is "How can I calculate the height of the row when cell is expanded and collapsed." Thanks
Edit: This is what I have tried. . .But failed
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
static DynamicTableViewCell *cell = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
cell = [self.tableView dequeueReusableCellWithIdentifier:CellIdentifier];
});
[self setUpCell:cell atIndexPath:indexPath];
CGFloat cellHeight = [self calculateHeightForConfiguredSizingCell:cell];
if(cellHeight >0) {
if(cell.isExpanded) {
return cellHeight;
}
else
{
return (cellHeight - detailViewHeight); // cell.detailView.frame.size.height = 100
}
}
else {
return cellHeight;
}
}

First of all you should not have any reference to instance to your cell, because of performance reasons.
Second you should use models to build your cells in the right way. Provided code does not show usage of model storage at all.
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
// Say you have a array of models NSArray<YourModel *> *dataSource;
YourModel *model = dataSource[indexPath.row]; // hope you have only one section.
}
Third it's a good way to use any architecture like MVVM or VIPER, or MVC, because if you have no one you will probably have problems in future support of your product. So in case of MVVM YourModel is like ViewModel.
To define a state of dynamic height cell, you use property isExpanded. That's a good point, but it should be defined in another place - YourModel. If you'll do that in a proper way, you'll know the state of cell without the cell, actually.
#interface YourModel ...
#property BOOL isExpanded;
#end
Be sure that you correctly change your state in didSelect:
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
YourModel *model = dataSource[indexPath.row];
model.isExpanded = !model.isExpanded; // inverse works like switch
// Then relayout your row
[tableView beginUpdates];
// I commented next line because it might not be needed for you
// Test it please
// [tableView reloadRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationNone];
[tableView endUpdates];
}
So back to heightForRow:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
// Say you have a array of models NSArray<YourModel *> *dataSource;
YourModel *model = dataSource[indexPath.row]; // hope you have only one section.
// It's better to extract next code to function or any class, but not in your YourModel.
CGSize imageSize = [model.image doSizeCalulation]; // Use your algorithm to calculate image size
CGSize shortText = [model.shortText sizeWithAttributes:#[/* font attributes */]];
// If the cell is expanded then we should calculate height of text else height is zero.
CGSize fullText = model.isExpanded ? [model.fullText sizeWithAttributes:#[/* font attributes */]]; : 0;
// just sum
return imageSize.height + shortText.height + fullText.height;
}
Another way to achieve this to use UITableViewAutomaticDimension (just return it in heightForRow method). In that case you should properly setup constraints, and in runtime change the constant of fullText's height constraint depending on isExpanded property.

Related

UITableView reuse Questions

About UITableView reuse, when there are multiple different Cell, use a different identifier to distinguish good or use an identifier and the Cell subViews remove, add content again good, if the Cell is very many cases, these reusable, what kind of specific access rules, when an identifier in the queue on the position is how to remove the master answer, thank you
_testTableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain];
_testTableView.dataSource = self;
_testTableView.delegate = self;
[_testTableView setRowHeight:80.];
[self.view addSubview:_testTableView];
[_testTableView registerClass:[TestTableViewCell class] forCellReuseIdentifier:testKeyOne];
[_testTableView registerClass:[TestTwoTableViewCell class] forCellReuseIdentifier:testKeyTwo];
//one way
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
//one
if(indexPath.row < 15){
TestTableViewCell * oneCell = [tableView dequeueReusableCellWithIdentifier:testKeyOne forIndexPath:indexPath];
return oneCell;
}else{
TestTwoTableViewCell * oneCell = [tableView dequeueReusableCellWithIdentifier:testKeyTwo forIndexPath:indexPath];
return oneCell;
}
return nil;
}
two way:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell * cell = [tableView dequeueReusableCellWithIdentifier:testKeyOne forIndexPath:indexPath];
for(UIView * view in cell.subviews){
[view removeFromSuperview];
}
//[cell addSubview:];
return cell;
}
one way or two,or Other better way,and Reuse of specific originally, enter the reuse and take out the order of the queue order
You want to go with option one. The table view data source should not be adding or removing views from table view cells. That just gets too messy.
Another option is to have just one cell subclass, but code the subclass to hide and show views as needed. I wouldn't have it add and remove views. That's way more complex code and way more expensive time-wise, which isn't great when you're trying to get a high frame rate when scrolling.

UITableViewCell: Dynamic Cell Height by 'Cell Identifier'

(note: The tableView I am using is from the Parse.com iOS SDK - PFQueryTableViewController)
Scenario = I have a TableViewController that has two different types of cells (each with their own identifier). Each object upon being queried and loaded into the datasource is checked if a key on the object is true. Depending on the result I dequeueReusableCellWithIdentifier to the correct cell.
-(PFTableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath object:(PFObject *)object {
myTableViewCell *cell;
if ([object[#"orientation"] isEqualToString:#"left"] || [object[#"orientation"] isEqualToString:#"right"]) {
myTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell"];
else {
myTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"Cell2"];
}
This all does its job. Each cell is being loaded at the correct indexPath.row and everything. Problem is my tableView "Row Height" itself does not readjust for the new cell. This causes overlapping of cells and makes everything ugly. I can tell the tableView in storyboard to set the row height to whatever the larger of the two cell heights is, but that leaves big spaces in-between cells too which also makes it look ugly.
Question = It is my belief (and correct me if I'm wrong) that I need to use the
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
method in order to achieve this. Where I need help is I am not sure how to set the height of each cell at indexPath depending upon the 'identifier' that I gave each cell in the cellForRowAtIndexPath method.
What I'm looking for = Something like this (please excuse the SuedoCode)
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
if ([cell.identifier isEqual:#"Cell"] {
return 100;
}
else {
return 200;
}
}
ANSWER: I figured it out! (I marked the answer below as accepted because it pointed me in the right direction)
Because I am using a PFQueryTableViewController all I had to do this...
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
PFObject *object = [self.objects objectAtIndex:indexPath.row];
if ([object[#"orientation"] isEqual:#"left"] || [object[#"orientation"] isEqual:#"right"]) {
return 100;
}
else {
return 200;
}
}
First, some things to keep in mind. heightForRowAtindexPath is calledbefore CellForRowatIndexPath, and simply says, if object is at indexPath X, then return Y or Z.
The more correct approach might be to subclass the tableCell class, set a property in the .h file and then figure out the path... I'll give you a dirty way :)
Create an NSMutableArray property (don't forget to init it somewhere/somehow), and based on your dataSource, populate it with Height A or Height B (a float). Now, back in heightForRowAtIndexPath, you can say something to the effect of:
return (int)self.myMutableArray[indexPath.row];

Hide cells in a UITableView with static cells - and no autolayout crash

I have a table view form created using Static Cells in IB/Storyboard. However, I need to hide some of the cells at runtime depending on certain conditions.
I have found a few 'answers; to this question on SO, e.g.
UITableView set to static cells. Is it possible to hide some of the cells programmatically?
.. and they focus on setting the height of the cell / row to 0. This is great, except I now get exceptions from AutoLayout because the constraints can't be satisfied. How do I get around this last problem? Can I temporarily disable Auto-Layout for a subview? Is there a better way to be doing this in iOS7?
I found the best way to do this is to simply handle the numberOfRowsInSection, cellForRowAtIndexPath and heightForRowAtIndexPath to selectively drop certain rows. Here's a 'hardcoded' example for my scenario, you could do something a little smarter to intelligently remove certain cells rather than hard code it like this, but this was easiest for my simple scenario.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [super tableView:tableView cellForRowAtIndexPath:indexPath];
if (indexPath.section == 0 && hideStuff) {
cell = self.cellIWantToShow;
}
return cell;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
CGFloat height = [super tableView:tableView heightForRowAtIndexPath:indexPath];
if (indexPath.section == 0 && hideStuff) {
height = [super tableView:tableView heightForRowAtIndexPath:[NSIndexPath indexPathForRow:2 inSection:0]];
}
return height;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
NSInteger count = [super tableView:tableView numberOfRowsInSection:section];
if (section == 0 && hideStuff) {
count -= hiddenCells.count;
}
return count;
}
Hide the cells on the storyboard and set the height to 0:
override func tableView(tableView: UITableView, heightForRowAtIndexPath indexPath: NSIndexPath) -> CGFloat {
let cell: UITableViewCell = super.tableView(tableView, cellForRowAtIndexPath:indexPath)
return cell.hidden ? 0 : super.tableView(tableView, heightForRowAtIndexPath:indexPath)
}
}
If you ensure there's no constraint touching the bottom edge of the cell, autolayout shouldn't barf (tested on iOS 6.0, 6.1, 7.0). You'll still 'anchor' to the top edge and have to pin the heights. (You can do the reverse and anchor to the bottom, of course.)
If your layout depends on both the top and bottom edge positions, it may be possible to programmatically remove the constraints (they're just objects, after all).
The simplest way is to change height of sections and rows. It works for me.
- (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section
{
if (section == 3) {
return 0;
} else {
return [super tableView:tableView heightForHeaderInSection:section];
}
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [super tableView:tableView cellForRowAtIndexPath:indexPath];
if (indexPath.section == 3) {
cell.hidden = YES;
return 0;
} else {
cell.hidden = NO;
return [super tableView:tableView heightForRowAtIndexPath:indexPath];
}
}
The kosher way to do this is to use dynamic cells, setting the row height to 0 is a hack. Static cells are very convenient but limited in functionality.
I managed to avoid exceptions from Auto Layout by first removing the constraints on the cell's contentView programmatically in viewDidLoad, and then setting that cell's height to 0 in heightForRowAtIndexPath.
I have found a way that allows you even row animations and is working on iOS 8.3. All you need is to implement the tableView:numberOfRowsInSection: data source method and then add/delete row by UITableView methods
insertRowsAtIndexPaths:withRowAnimation: and deleteRowsAtIndexPaths:withRowAnimation:.
Here is example of hiding exact row based on UISwitch state:
- (IBAction)alowNotif:(id)sender {
UISwitch *sw = (UISwitch*)sender;
NSIndexPath *index = [NSIndexPath indexPathForRow:5 inSection:0];
if ([sw isOn]) {
[self.tableView insertRowsAtIndexPaths:#[index] withRowAnimation:UITableViewRowAnimationAutomatic];
}
else {
[self.tableView deleteRowsAtIndexPaths:#[index] withRowAnimation:UITableViewRowAnimationAutomatic];
}
}
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
if (![notifications isOn]) {
return 5;
}
return 6;
}
As was mentioned above by #algal, numberOfRowInSection: is still UITableViewDataSource method, so one does not simply know how long its gonna work.
The best way for me was to modify numberOfRowsInSection method. I removed datasource which i did not want to display. Best solution for me, because everything is in one function.
(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
if(section==0) {
NSUInteger productCount = _products.count;
for(loop trough your data) {
if(your condition is true)
productCount--;
}
}
return productCount;
} else
return self.groups.count;
}
Implemented this too, for a tableview in a StoryBoard. My cells are embedded in sections with a header, represented by that blue cube in xcode6 IB. Indeed if you implement heightForHeaderInSection, heightForFooterInSection and titleForHeaderInSection,titleForFooterInSection you can access the headers when the table is displayed, and return 0.0 and nil respectively, and return 0 for numberOfRowsInSection.
Basically it all works fine, except that for every cell hidden a ca. 10 pixel high vertical space remains for every cell (section) you hide. Any idea what that could be?

Is there any way refresh cell's height without reload/reloadRow?

I make a view like imessage, just input text into the bottom text view. I use table view to do this, and the text view in the last cell. when I input long text that more than one line, I need the text view and the cell become tailer. so I need refresh cell's height. but if I use table view's reload or reload row, the content in text view will disappear and the keyboard will disappear too. Is there any way better to fix it?
May be I should use tool bar to do it easy? but I still doubt table view can do it.
The cells will resize smoothly when you call beginUpdates and endUpdates. After those calls the tableView will send tableView:heightForRowAtIndexPath: for all the cells in the table, when the tableView got all the heights for all the cells it will animate the resizing.
And you can update cells without reloading them by setting the properties of the cell directly. There is no need to involve the tableView and tableView:cellForRowAtIndexPath:
To resize the cells you would use code similar to this
- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
NSString *newText = [textView.text stringByReplacingCharactersInRange:range withString:text];
CGSize size = // calculate size of new text
if ((NSInteger)size.height != (NSInteger)[self tableView:nil heightForRowAtIndexPath:nil]) {
// if new size is different to old size resize cells.
// since beginUpdate/endUpdates calls tableView:heightForRowAtIndexPath: for all cells in the table this should only be done when really necessary.
[self.tableView beginUpdates];
[self.tableView endUpdates];
}
return YES;
}
to change the content of a cell without reloading I use something like this:
- (void)configureCell:(FancyCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath {
MyFancyObject *object = ...
cell.textView.text = object.text;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
FancyCell *cell = (FancyCell *)[tableView dequeueReusableCellWithIdentifier:#"CellWithTextView"];
[self configureCell:cell forRowAtIndexPath:indexPath];
return cell;
}
// whenever you want to change the cell content use something like this:
NSIndexPath *indexPath = ...
FancyCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
[self configureCell:cell forRowAtIndexPath:indexPath];
I've written a subclass of UITableViewCell to handle this functionality.
.h file:
#import <UIKit/UIKit.h>
#protocol AECELLSizeableDelegate;
#interface AECELLSizeable : UITableViewCell
#property (weak, nonatomic) id <AECELLSizeableDelegate> delegate;
#property IBOutlet UIView *viewMinimized;
#property IBOutlet UIView *viewMaximized;
#property BOOL maximized;
#property CGFloat height;
- (IBAction)clickedConfirm:(id)sender;
- (IBAction)clickedCancel:(id)sender;
- (void)minimizeForTableview: (UITableView*)tableView;
- (void)maximizeForTableview: (UITableView*)tableView;
- (void)toggleForTableview: (UITableView*)tableView;
#end
#protocol AECELLSizeableDelegate <NSObject>
- (void)sizeableCellConfirmedForCell: (AECELLSizeable*)cell;
- (void)sizeableCellCancelledForCell: (AECELLSizeable*)cell;
#end
.m file:
#import "AECELLSizeable.h"
#implementation AECELLSizeable
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
// Initialization code
}
return self;
}
- (void)setSelected:(BOOL)selected animated:(BOOL)animated
{
[super setSelected:selected animated:animated];
// Configure the view for the selected state
}
- (void)minimizeForTableview: (UITableView*)tableView
{
self.maximized = NO;
[self.viewMinimized setHidden:NO];
[self.viewMaximized setHidden:YES];
self.height = self.viewMinimized.frame.size.height;
[tableView beginUpdates];
[tableView endUpdates];
}
- (void)maximizeForTableview: (UITableView*)tableView
{
self.maximized = YES;
[self.viewMinimized setHidden:YES];
[self.viewMaximized setHidden:NO];
self.height = self.viewMaximized.frame.size.height;
[tableView beginUpdates];
[tableView endUpdates];
}
- (void)toggleForTableview:(UITableView *)tableView
{
if (self.maximized) {
[self minimizeForTableview:tableView];
} else {
[self maximizeForTableview:tableView];
}
}
- (void)clickedConfirm:(id)sender
{
[self.delegate sizeableCellConfirmedForCell:self];
}
- (void)clickedCancel:(id)sender
{
[self.delegate sizeableCellCancelledForCell:self];
}
#end
Example Usage:
Create a UITableViewController with a static UITableView in IB
Add a cell to the tableview that is a AECELLSizeable (or subclass of it)
Create two UIViews in this cell. One UIView will be used for the content visible while minimized, the other will be used for the content visible while maximized. Ensure these cells start at 0 on the y axis and that their height is equal to that of the height you wish to have the cell for each state.
Add any subviews to these two views you desired. Optionally add confirm and cancel UIButtons to the maximized UIView and hook up the provided IBActions to receive delegate callbacks on these events.
Set your tableview controller to conform to the AECELLSizeableDelegate and set the cell's delegate property to the tableview controller.
Create an IBOutlet in your UIViewController's interface file for the AECELLSizeable cell.
Back in IB, ensure the cell's initial height is that of the minimized version and connect the IBOutlet you just previously created.
Define the tableview's heightForRowAtIndexPath callback method in the tableview controller's implementation file as such:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.row == 0) {
//Sizeable Cell
return self.cellSizeable.height;
} else {
return [super tableView:tableView heightForRowAtIndexPath:indexPath];
}
}
In your tableview controller's viewDidLoad method, call minimizeForTableview on your cell to ensure it starts off minimized.
Then, call maximizeForTableview: on the cell when it is selected via the didSelectRowAtIndexPath callback from the tableview (or however else you would like to handle it) and call the minimizeForTableview: method on the cell when you receive the canceled / confirmed delegate callbacks from the cell (or, again, however else you'd like to handle it).
Check out this library. This is an implementation of message bubbles using UITableView. Keep in mind that every time a cell is displayed, cellForRow:atIndexPath is called and the cell is drawed.
EDIT
You can use heightForRowAtIndexPath
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
Messages *message = [self.messageList objectAtIndex:[indexPath row]];
CGSize stringSize = [message.text sizeWithFont:[UIFont fontWithName:#"HelveticaNeue-Light" size:13]
constrainedToSize:CGSizeMake(320, 9999) lineBreakMode:UILineBreakModeWordWrap];
return stringSize.height + 78;
}
Table view cells won't smoothly resize. Dot.
However, since your text view is in the last cell, you are lucky because you can easily simulate a row resizing. Having a text view in the middle of the table would be much more difficult.
Here is how I would do it: put your text view on top of the table view. Sync its position with the contentOffset of the tableView in scrollViewDidScroll:. And use the animatable contentInset of the tableView in order to leave room for the textView, as the user types in it. You may have to make sure your textView is not scrollable.

Iphone - when to calculate heightForRowAtIndexPath for a tableview when each cell height is dynamic?

I have seen this question asked many times but astoundingly, I have not seen a consistent answer, so I will give it a try myself:
If you have a tableview containing your own custom UITableViewCells that contain UITextViews and UILabels whose height must be determined at runtime, how are you supposed to determine the height for each row in heightForRowAtIndexPath?
The most obvious first idea is to calculate the height for each cell by calculating and then summing the heights of each view inside the cell inside of cellForRowAtIndexPath, and store that final total height for later retrieval.
This will not work however because cellForRowAtIndexPath is called AFTER heightForRowAtIndexPath.
The only thing I can think of is to do all the calculations inside viewDidLoad, create all the UITableViewCells then, calculate the cells height and store that in a custom field inside your UITableViewCell subclass, and put each cell in an NSMutableDictionary with the indexPath as the the key, and then simply retrieve the cell from the dictionary using the indexPath inside cellForRowAtIndexPath and heightForRowAtIndexPath, returning either the custom height value or the cell object itself.
This approach seems wrong though because it does not make use of dequeueReusableCellWithIdentifier, instead I would be loading all the cells at once into a dictionary in my controller, and the delegate methods would be doing nothing more than retrieving the correct cell from the dictionary.
I don't see any other way to do it though. Is this a bad idea - if so, what is the correct way to do this?
The way Apple implements UITableView is not intuitive to everyone and it's easy to misunderstand the role of heightForRowAtIndexPath:. The general intention is that this is a faster and light-on-memory method that can be called for every row in the table quite frequently. This contrasts with cellForRowAtIndexPath: which is often slower and more memory intensive, but is only called for the rows that are actually need to be displayed at any given time.
Why do Apple implement it like this? Part of the reason is that it's almost always cheaper (or can be cheaper if you code it right) to calculate the height of a row than it is to build and populate a whole cell. Given that in many tables the height of every cell will be identical, it is often vastly cheaper. And another part of the reason is because iOS needs to know the size of the whole table: this allows it to create the scroll bars and set it up on a scroll view etc.
So, unless every cell height is the same, then when a UITableView is created and whenever you send it a reloadData message, the datasource is sent one heightForRowAtIndexPath message for each cell. So if your table has 30 cells, that message gets sent 30 times. Say only six of those 30 cells are visible on screen. In that case, when created and when you send it a reloadData message, the UITableView will send one cellForRowAtIndexPath message per visible row, i.e. that message gets sent six times.
Some people are sometimes puzzled about how to calculate a cell height without creating the views themselves. But usually this is easy to do.
For example, if your row heights vary in size because they hold varying amounts of text, you can use one of the sizeWithFont: methods on the relevant string to do the calculations. This is quicker than building a view and then measuring the result. Note, that if you change the height of a cell, you will need to either reload the whole table (with reloadData - this will ask the delegate for every height, but only ask for visible cells) OR selectively reload the rows where the size has changed (which, last time I checked, also calls heightForRowAtIndexPath: on ever row but also does some scrolling work for good measure).
See this question and perhaps also this one.
So, I think you can do this without having to create your cells all at once (which, as you suggest, is wasteful and also probably impractical for a large number of cells).
UIKit adds a couple of methods to NSString, you may have missed them as they're not part of the main NSString documentation. The ones of interest to you begin:
- (CGSize)sizeWithFont...
Here is the link to the Apple docs.
In theory, these NSString additions exist for this exact problem: to figure out the size that a block of text will take up without needing to load the view itself. You presumably already have access to the text for each cell as part of your table view datasource.
I say 'in theory' because if you're doing formatting in your UITextView your mileage may vary with this solution. But I'm hoping it will get you at least part way there. There's an example of this on Cocoa is My Girlfriend.
An approach I have used in the past is to create a class variable to hold a single instance of the cell you are going to be using in the table (I call it a prototype cell). Then in the custom cell class I have a method to populate the data and determine the height the cell needs to be. Note that it can be a simpler variant of the method to really populate the data - instead of actually resizing a UILabel in a cell for example, it can just use the NSString height methods to determine how tall the UILabel would be in the final cell and then use the total cell height (plus a border on the bottom) and UILabel placement to determine the real height. YOu use the prototype cell just to get an idea of where elements are placed so you know what it means when a label is going to be 44 units high.
In heightForRow: I then call that method to return the height.
In cellForRow: I use the method that actually populates labels and resizes them (you never resize the UITableView cell yourself).
If you want to get fancy, you can also cache the height for each cell based on the data you pass in (for instance it could just be on one NSString if that's all that determines height). If you have a lot of data that's often the same it may make sense to have a permanent cache instead of just in-memory.
You can also try estimating line count based on character or word count, but in my experience that never works - and when it goes wrong it usually messes up a cell and all the cells below it.
This is how I calculate the height of a cell based on the amount of text in a UTextView:
#define PADDING 21.0f
- (CGFloat)tableView:(UITableView *)t heightForRowAtIndexPath:(NSIndexPath *)indexPath {
if(indexPath.section == 0 && indexPath.row == 0)
{
NSString *practiceText = [practiceItem objectForKey:#"Practice"];
CGSize practiceSize = [practiceText sizeWithFont:[UIFont systemFontOfSize:14.0f]
constrainedToSize:CGSizeMake(tblPractice.frame.size.width - PADDING * 3, 1000.0f)];
return practiceSize.height + PADDING * 3;
}
return 72;
}
Of course, you would need to adjust the PADDING and other variables to fit your needs, but this sets the height of the cell which has a UITextView in it, based on the amount of text supplied. so if there are only 3 lines of text, the cell is fairly short, where as if there are 14 lines of text, the cell is rather large in height.
The best implementation of this that I've seen is the way the Three20 TTTableView classes do it.
Basically they have a class derived from UITableViewController that delegates the heightForRowAtIndexPath: method to a class method on a TTTableCell class.
That class then returns the right height, invariably by doing the same sort of layout calculations as you do in the draw methods. By moving it to the class it avoids writing code that depends on the cell instance.
There's really no other option - for performance reasons the framework won't create cells before asking for their heights, and you don't really want to do that either if there could be a lot of rows.
The problem with moving the calculation of each cell to tableView:heightForRowAtIndexPath: is that all the cells are then recalculated every time reloadData is called. Way too slow, at least for my application where there may be 100's of rows. Here's an alternative solution that uses a default row height, and caches the row heights when they are calculated. When a height changes, or is first calculated, a table reload is scheduled to inform the table view of the new heights. This does mean that rows are displayed twice when their heights change, but that's minor in comparison:
#interface MyTableViewController : UITableViewController {
NSMutableDictionary *heightForRowCache;
BOOL reloadRequested;
NSInteger maxElementBottom;
NSInteger minElementTop;
}
tableView:heightForRowAtIndexPath:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
// If we've calculated the height for this cell before, get it from the height cache. If
// not, return a default height. The actual size will be calculated by cellForRowAtIndexPath
// when it is called. Do not set too low a default or UITableViewController will request
// too many cells (with cellForRowAtIndexPath). Too high a value will cause reloadData to
// be called more times than needed (as more rows become visible). The best value is an
// average of real cell sizes.
NSNumber *height = [heightForRowCache objectForKey:[NSNumber numberWithInt:indexPath.row]];
if (height != nil) {
return height.floatValue;
}
return 200.0;
}
tableView:cellForRowAtIndexPath:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
// Get a reusable cell
UITableViewCell *currentCell = [tableView dequeueReusableCellWithIdentifier:_filter.templateName];
if (currentCell == nil) {
currentCell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:_filter.templateName];
}
// Configure the cell
// +++ unlisted method sets maxElementBottom & minElementTop +++
[self configureCellElementLayout:currentCell withIndexPath:indexPath];
// Calculate the new cell height
NSNumber *newHeight = [NSNumber numberWithInt:maxElementBottom - minElementTop];
// When the height of a cell changes (or is calculated for the first time) add a
// reloadData request to the event queue. This will cause heightForRowAtIndexPath
// to be called again and inform the table of the new heights (after this refresh
// cycle is complete since it's already been called for the current one). (Calling
// reloadData directly can work, but causes a reload for each new height)
NSNumber *key = [NSNumber numberWithInt:indexPath.row];
NSNumber *oldHeight = [heightForRowCache objectForKey:key];
if (oldHeight == nil || newHeight.intValue != oldHeight.intValue) {
if (!reloadRequested) {
[self.tableView performSelector:#selector(reloadData) withObject:nil afterDelay:0];
reloadRequested = TRUE;
}
}
// Save the new height in the cache
[heightForRowCache setObject:newHeight forKey:key];
NSLog(#"cellForRow: %# height=%# >> %#", indexPath, oldHeight, newHeight);
return currentCell;
}
Really good question: looking for more insight on this as well.
Clarifying the issue:
Height for Row is called Before the (cellForRowAtIndexPath)
Most people calculate the height-type information within the CELL (cellForRowAtIndexPath).
Some of the solutions are surprisingly simple/effective:
solution 1: force the heightForRowAtIndexPath to calculate the the cell's specs. Massimo Cafaro Sept 9th
solution 2: do a first pass "standard size" for the cells, cache results when you do have cell heights, then reload the table using the new heights - Symmetric
solution 3: the other interesting answer seems to be the involving three20 but based on the answer it seems that there isn't a cell drawn in storyboard/xib which would make this "problem" much easier to solve.
I went with the idea I originally proposed, which appears to work fine, whereby I load all the custom cells ahead of time in viewDidLoad, store them in a NSMutableDictionary with their index as the key. I am posting the relevant code and would love any critiques or opinions anyone has about this approach. Specifically, I am not sure whether there is any memory leak issue with the way I am creating the UITableViewCells from the nib in viewDidLoad - since I don't release them.
#interface RecentController : UIViewController <UITableViewDelegate, UITableViewDataSource> {
NSArray *listData;
NSMutableDictionary *cellBank;
}
#property (nonatomic, retain) NSArray *listData;
#property (nonatomic, retain) NSMutableDictionary *cellBank;
#end
#implementation RecentController
#synthesize listData;
#synthesize cellBank;
---
- (void)viewDidLoad {
---
self.cellBank = [[NSMutableDictionary alloc] init];
---
//create question objects…
---
NSArray *array = [[NSArray alloc] initWithObjects:question1,question2,question3, nil];
self.listData = array;
//Pre load all table row cells
int count = 0;
for (id question in self.listData) {
NSArray *nib = [[NSBundle mainBundle] loadNibNamed:#"QuestionHeaderCell"
owner:self
options:nil];
QuestionHeaderCell *cell;
for (id oneObject in nib) {
if([oneObject isKindOfClass:[QuestionHeaderCell class]])
cell = (QuestionHeaderCell *) oneObject;
NSNumber *key = [NSNumber numberWithInt:count];
[cellBank setObject:[QuestionHeaderCell makeCell:cell
fromObject:question]
forKey:key];
count++;
}
}
[array release];
[super viewDidLoad];
}
#pragma mark -
#pragma mark Table View Data Source Methods
-(NSInteger) tableView: (UITableView *) tableView
numberOfRowsInSection: (NSInteger) section{
return [self.listData count];
}
-(UITableViewCell *) tableView: (UITableView *) tableView
cellForRowAtIndexPath: (NSIndexPath *) indexPath{
NSNumber *key = [NSNumber numberWithInt:indexPath.row];
return [cellBank objectForKey:key];
}
-(CGFloat) tableView: (UITableView *) tableView
heightForRowAtIndexPath: (NSIndexPath *) indexPath{
NSNumber *key = [NSNumber numberWithInt:indexPath.row];
return [[cellBank objectForKey:key] totalCellHeight];
}
#end
#interface QuestionHeaderCell : UITableViewCell {
UITextView *title;
UILabel *createdBy;
UILabel *category;
UILabel *questionText;
UILabel *givenBy;
UILabel *date;
int totalCellHeight;
}
#property (nonatomic, retain) IBOutlet UITextView *title;
#property (nonatomic, retain) IBOutlet UILabel *category;
#property (nonatomic, retain) IBOutlet UILabel *questionText;
#property (nonatomic, retain) IBOutlet UILabel *createdBy;
#property (nonatomic, retain) IBOutlet UILabel *givenBy;
#property (nonatomic, retain) IBOutlet UILabel *date;
#property int totalCellHeight;
+(UITableViewCell *) makeCell:(QuestionHeaderCell *) cell
fromObject:(Question *) question;
#end
#implementation QuestionHeaderCell
#synthesize title;
#synthesize createdBy;
#synthesize givenBy;
#synthesize questionText;
#synthesize date;
#synthesize category;
#synthesize totalCellHeight;
- (void)dealloc {
[title release];
[createdBy release];
[givenBy release];
[category release];
[date release];
[questionText release];
[super dealloc];
}
+(UITableViewCell *) makeCell:(QuestionHeaderCell *) cell
fromObject:(Question *) question{
NSUInteger currentYpos = 0;
cell.title.text = question.title;
CGRect frame = cell.title.frame;
frame.size.height = cell.title.contentSize.height;
cell.title.frame = frame;
currentYpos += cell.title.frame.size.height + 2;
NSMutableString *tempString = [[NSMutableString alloc] initWithString:question.categoryName];
[tempString appendString:#"/"];
[tempString appendString:question.subCategoryName];
cell.category.text = tempString;
frame = cell.category.frame;
frame.origin.y = currentYpos;
cell.category.frame = frame;
currentYpos += cell.category.frame.size.height;
[tempString setString:#"Asked by "];
[tempString appendString:question.username];
cell.createdBy.text = tempString;
frame = cell.createdBy.frame;
frame.origin.y = currentYpos;
cell.createdBy.frame = frame;
currentYpos += cell.createdBy.frame.size.height;
cell.questionText.text = question.text;
frame = cell.questionText.frame;
frame.origin.y = currentYpos;
cell.questionText.frame = frame;
currentYpos += cell.questionText.frame.size.height;
[tempString setString:#"Advice by "];
[tempString appendString:question.lastNexusUsername];
cell.givenBy.text = tempString;
frame = cell.givenBy.frame;
frame.origin.y = currentYpos;
cell.givenBy.frame = frame;
currentYpos += cell.givenBy.frame.size.height;
cell.date.text = [[[MortalDataStore sharedInstance] dateFormat] stringFromDate: question.lastOnDeck];
frame = cell.date.frame;
frame.origin.y = currentYpos-6;
cell.date.frame = frame;
currentYpos += cell.date.frame.size.height;
//Set the total height of cell to be used in heightForRowAtIndexPath
cell.totalCellHeight = currentYpos;
[tempString release];
return cell;
}
#end
Here is what I do in very simple case, a cell containing a note held in a label. The note itself is constrained to a maximum length I am imposing, so I use a multi-line UILabel and I compute dynamically the correct eight for each cell as shown in the following example. You can deal with an UITextView pretty much the same.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier] autorelease];
}
// Configure the cell...
Note *note = (Note *) [fetchedResultsController objectAtIndexPath:indexPath];
cell.textLabel.text = note.text;
cell.textLabel.numberOfLines = 0; // no limits
DateTimeHelper *dateTimeHelper = [DateTimeHelper sharedDateTimeHelper];
cell.detailTextLabel.text = [dateTimeHelper mediumStringForDate:note.date];
cell.accessoryType = UITableViewCellAccessoryDetailDisclosureButton;
return cell;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
//NSLog(#"heightForRowAtIndexPath: Section %d Row %d", indexPath.section, indexPath.row);
UITableViewCell *cell = [self tableView: self.tableView cellForRowAtIndexPath: indexPath];
NSString *note = cell.textLabel.text;
UIFont *font = [UIFont fontWithName:#"Helvetica" size:14.0];
CGSize constraintSize = CGSizeMake(280.0f, MAXFLOAT);
CGSize bounds = [note sizeWithFont:font constrainedToSize:constraintSize lineBreakMode:UILineBreakModeWordWrap];
return (CGFloat) cell.bounds.size.height + bounds.height;
}
as i searched over and over about this topic, finally this logic came to my thought. a simple code, but maybe not efficient enough, but so far it's the best i can find.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSDictionary * Object=[[NSDictionary alloc]init];
Object=[Rentals objectAtIndex:indexPath.row];
static NSString *CellIdentifier = #"RentalCell";
RentalCell *cell = (RentalCell *)[tableView
dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil)
{
cell = [self.tableView dequeueReusableCellWithIdentifier:CellIdentifier];
}
NSString* temp=[Object objectForKey:#"desc"];
int lines= (temp.length/51)+1;
//so maybe here, i count how many characters that fit in one line in this case 51
CGRect correctSize=CGRectMake(cell.infoLabel.frame.origin.x, cell.infoLabel.frame.origin.y, cell.infoLabel.frame.size.width, (15*lines));
//15 (for new line height)
[cell.infoLabel setFrame:correctSize];
//manage your cell here
}
and here is the rest of the code
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
NSDictionary * Object=[[NSDictionary alloc]init];
Object=[Rentals objectAtIndex:indexPath.row];
static NSString *CellIdentifier = #"RentalCell";
RentalCell *cells = (RentalCell *)[tableView
dequeueReusableCellWithIdentifier:CellIdentifier];
NSString* temp=[Object objectForKey:#"desc"];
int lines= temp.length/51;
return (CGFloat) cells.bounds.size.height + (13*lines);
}

Resources