Possibly a duplicate but I couldn't find a specific question on SO, so here it is.
I'm curious about dynamically changing heights for all rows, typically, because you don't know the length of an NSString that's used for a label.
I know you must use this delegate method to change the row heights:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
The problem is this delegate method is called BEFORE the cell is created (i.e. called before cellForRowAtIndexPath).
So, what I've thought of is to create a mock cell in viewWillAppear and a method that adds cell heights to an array that maps to the table view's data source (which in my case is also an array).
viewWillAppear implements this one important method to get the height:
[NSString sizeWithFont: constrainedToSize: lineBreakMode:]
Then in heightForRowAtIndexPath I can return the cell height like so:
//cellHeights is an ivar populated in viewWillAppear
return [[cellHeights objectAtIndex:indexPath.row] floatValue];
I was wondering if there was a better way to dynamically change the row height?
I realize this will degrade performance for a large number of rows (greater than 1000, I believe). But in my case, my rows won't ever come close to that number. So the performance hit is negligible.
Thanks in advance!
Great question! In fact, I did something similar in some of my applications.
I can think of a couple of alternatives, but all of these are along the same theme. You could also just to use sizeWithFont: inside of heightForRowAtIndexPath: and do away with the array. In that case, you might take a performance hit for recalculating the size each time, if that operation is expensive.
You could do "lazy loading" of the cellHeights array inside of heightForRowAtIndexPAth: so it might look something like this:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
if ([cellHeights objectAtIndex:indexPath.row] == nil) {
... calculate height and store it in the array at the correct index...
}
return [[cellHeights objectAtIndex:indexPath.row] floatValue];
}
The advantage I am thinking of here is that you will only calculate the heights for cells that are definitely going to be loaded. If you do the calculation in viewWillAppear, I guess you end up doing it for every cell, regardless of whether it is displayed?
Finally, you could put the size in your data model itself. If it is, for example, an array of strings, you could make a class that has two properties: a string and a "representationSize" property. Then you can recalculate the size of the string each time the value of the string is changed. Then, there would just be one array, not two, that maps onto your data source, filled with a data class containing both the string and display size of the string, and the value would be calculated when the string changes, not at all once when the view appears.
Anyway, I would love to hear some comments about these various approaches.
Matthew's idea of putting the height in the data model sounds interesting. Here's another answer that proposes a very similar solution: How can I do variable height table cells on the iPhone properly?
I have too problem in my UITableView,when I scroll tableview (on 3gs iPhone). I saw a lot of lags. So i open time profiler (very good tool for optimization) and problem was when I call function sizeWithFont. The best solution for resolve this problem is call sizeWithFont in constructor.
Related
I have implemented a UITableView with only one giant section and now I need to implement an index to this UITableView (something like the contacts apps) however my index does not represent sections it represents rows. And as far as I know you can only jump to a section and not to a row with the index in tableview.
I don't want to add sections because then I'll have to add a section for each row, which would be kinda stupid.
So my question is: Is there any way to implement an index to a UITableView such that it when I tap on any part of the index it takes me to the relative row instead of section in the tableview?
I would probably endup writing a hack for this thing which a really wanna avoid and do it the way it should be done (if there is any such way) so any help would be greatly appreciated. Cheers!
Just implement the sections and set their height to 0 in tableView:heightForHeaderInSection:
It's not such a big hack. Just change rows for sections with one row. Move what you have in tableView:numberOfRowsInSection: to numberOfSectionsInTableView:. You have to slightly adapt cellForRowAtIndexPath: to check the section you are in instead of the row and there you are.
Use this method:
- (void)scrollToRowAtIndexPath:(NSIndexPath *)indexPath atScrollPosition:(UITableViewScrollPosition)scrollPosition animated:(BOOL)animated;
Question - How does one best calculate the height for a row in the "heightForRowAtIndexPath" method of a UITableViewController, given that:
I'm using a custom subclassed UITableViewCell & the actually size of the subview (e.g. UILabels) is calculated at runtime & dependant on things such as if the user changed the font size
the cell's aren't actually prepared it seems prior to a "heightForRowAtIndexPath", so you can't rely on calling your specific custom cell instant to query it
Only thing I can think of for the moment is to:
1. In your custom UITableViewCell subclass create a method that calculates the heights of each subview (e.g. UILabel) that is in the UITableViewCell subclass - then use this within the cell subclass when it is creating instances
2. Also in the custom subclass create a class method that runs through all the UILabels, calling the above-mentioned method, to sum up the heights and therefore work out the total row height. It would have to get the data passed to it (e.g. text in each of the UILabels)
3. In the UITableViewController "heightForRowAtIndexPath" then you have to call the "calRowHeight" type method from (2) above, passing it the label text data. So effectively call a class method on your custom cell subclass which knows how to work out the total row height, but it's using the same logic that the cell needs too...
Is there an easier way than this I'm missing?
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.
Why do Apple implement it like this? Part of the reason is that it's almost always cheaper to calculate the height of a row than it is to build and populate a whole cell. And given that in many tables the height of every cell will be identical, it is often vastly cheaper. And 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.
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.
Additional material
If I understand the follow up question in the comment, the following may help:
If you are implementing an editing mode, then it's not uncommon to need to change the height of your table rows. For example, you may have text in your table rows and when they cells become narrower - to make space for the delete circles on the right - you may want some of the cells to become taller to accommodate the text. The basic approach here is to:
Make sure the tableView:heightForRowAtIndexPath: method knows whether your are in editing mode or not. (It can ask the tableView using isEditing.) And then get the method to return the right height, depending on whether you are in editing mode or not.
In your setEditing:animated: method in the UITableViewController (or a UIViewController, whichever you are using - there are some differences depending what you use, so it's worth checking the documentation carefully) send a reloadData message to the tableView after you have changed its state. This will force the tableView to grab the heights of every row and it will refetch the cells for the visible rows. The tableView handles making cells narrower when you enter editing mode, but if you want to do more work on the layout, do it in tableView:cellForRowAtIndex:. As noted above, the general strategy is to find a means of calculating the height that is quick. With text sizeWithFont: (and its variants) can do it. If you have images etc., then you can grab their dimensions and do some sums.
In addition to those steps you may also want to scroll the tableView a bit after switching modes. If the heights of your rows are different, then you will end up in the wrong position in the table after switching mode. An approach I have taken here is to use performSelector:withObject:afterDelay after I've reloaded the table to call a method that does the scroll adjusting. You need to use the delay, to allow time for the tableView to collect the new heights and the new table cells. (There may be a smarter way of doing this.) I do some sums to make the scroll adjustment based on the difference between the origin.y of the tableView:cellForRowAtIndexPath: of the cell first visible row on screen before and after the reload. So, for e.g., to get the position before the pre-load, something a bit like this.
CGPoint offset = [[self tableView] contentOffset];
NSIndexPath* indexPath = [[self tableView] indexPathForRowAtPoint:CGPointMake(0,offset.y)];
CGFloat preCellOffset = [[[self tableView] cellForRowAtIndexPath:indexPath] origin].y;
What I have done in the past, which I am not sure is the most efficient, is from within my heightForRowAtIndexPath method call cellForRowAtIndexPath then I ask the view for that cell its height. I have done similar things for header and footer heights. This way if I change the cell, the header, or the footer, I don't have to remember to go and update the corresponding height method.
Is there a way to tell a UITableView to preload all rows?
The tableView is supposed to show several comments (up to 80 comments).
So my CommentCell uses a Setter to adapt the cell to a specific comment.
-(void)setComment:(Comment *)newComment {
if (newComment != comment) {
[comment release];
comment = [newComment retain];
/*
* set the cells view variables here
*/
}
}
This specific setter takes quite a bunch of processing resources and scrolling gets kinda laggy.
I am using a comment-specific reuseIdentifier instead of a static cellIdentifier when calling
dequeueReusableCellWithIdentifier:
in order to assure, that "newComment" equals the old "comment".
And in fact this does work great when scrolling over cells which have already been loaded.
But when scrolling through the comments for the first time, it still lags like hell.
Which leads me to my question:
Is there a way to tell the tableview to preload all cells? (which I doubt)
or
Do I have to implement my own cache instead of relying on "dequeueReusableCellWithIdentifier:"?
Keep in mind that your comment specific reuseIdentifier could be what is causing everything to go slow (or at least, it isn't helping). The reason we use reuseIdentifier for UITableViewCells is because if you try to allocate a new cell every time you need one it isn't as performant as if you can just reuse one that was already made.
I'd recommend pre-computing your comments so you can just set properties of your cells and reusing cells after they scroll off the tableview.
No, you have to preload your data in your dataSource. Just put everything you need in an array and fill the table's cells from that array of preloaded objects.
Case
Normally you would use the cellForRowAtIndexPath delegate method to setup your cell. The information set for the cell is important for how the cell is drawn and what the size will be.
Unfortunatly the heightForRowAtIndexPath delegate method is called before the cellForRowAtIndexPath delegate method so we can't simply tell the delegate to return the height of the cell, since this will be zero at that time.
So we need to calculate the size before the cell is drawn in the table. Luckily there is a method that does just that, sizeWithFont, which belongs to the NSString class. However there is problem, in order to calculate the correct size dynamically it needs to know how the elements in the cell will be presented. I will make this clear in an example:
Imagine a UITableViewCell, which contains a label named textLabel. Within the cellForRowAtIndexPath delegate method we place textLabel.numberOfLines = 0, which basically tells the label it can have as many lines as it needs to present the text for a specific width. The problem occurs if we give textLabel a text larger then the width originally given to textLabel. The second line will appear, but the height of the cell will not be automatically adjusted and so we get a messed up looking table view.
As said earlier, we can use sizeWithFont to calculate the height, but it needs to know which Font is used, for what width, etc. If, for simplicity reasons, we just care about the width, we could hardcode that the width would be around 320.0 (not taking padding in consideration). But what would happen if we used UITableViewStyleGrouped instead of plain the width would then be around 300.0 and the cell would again be messed up. Or what happends if we swap from portrait to landscape, we have much more space, yet it won't be used since we hardcoded 300.0.
This is the case in which at some point you have to ask yourself the question how much can you avoid hardcoding.
My Own Thoughts
You could call the cellForRowAtIndexPath method that belongs to the UITableView class to get the cell for a certain section and row. I read a couple of posts that said you don't want to do that, but I don't really understand that. Yes, I agree it will already allocate the cell, but the heightForRowAtIndexPath delegate method is only called for the cells that will be visible so the cell will be allocated anyway. If you properly use the dequeueReusableCellWithIdentifier the cell will not be allocated again in the cellForRowAtIndexPath method, instead a pointer is used and the properties are just adjusted. Then what's the problem?
Note that the cell is NOT drawn within the cellForRowAtIndexPath delegate method, when the table view cell becomes visible the script will call the setNeedDisplay method on the UITableVieCell which triggers the drawRect method to draw the cell. So calling the cellForRowAtIndexPath delegate directly will not lose performance because it needs to be drawn twice.
Okay so by calling the cellForRowAtIndexPath delegate method within the heightForRowAtIndexPath delegate method we receive all the information we need about the cell to determine it's size.
Perhaps you can create your own sizeForCell method that runs through all the options, what if the cell is in Value1 style, or Value2, etc.
Conclusion/Question
It's just a theory I described in my thoughts, I would like to know if what I wrote is correct. Or that maybe there is another way to accomplish the same thing. Note that I want to be able to do things as flexible as possible.
Yes, I agree it will already allocate the cell, but the heightForRowAtIndexPath delegate method is only called for the cells that will be visible so the cell will be allocated anyway.
This is incorrect. The table view needs to call heightForRowAtIndexPath (if it's implemented) for all rows that are in the table view, not just the ones currently being displayed. The reason is that it needs to figure out its total height to display the correct scroll indicators.
I used to do this by:
Creating a collection objects (array of size information (dictionary, NSNumber of row heights, etc.) based on the collection objects that will be used for the table view.
This is done when we're processing the data either from a local or remote source.
I predetermine the type and size of the font that will be used, when I'm creating this collection objects. You can even store the UIFont objects or whatever custom objects used to represent the content.
These collection objects will be used every time I implement UITableViewDataSource or UITableViewDelegate protocols to determine the sizes of the UITableViewCell instances and its subviews, etc.
By doing it this way you can avoid having to subclass UITableViewCell just to get the various size properties of its content.
Don't use an absolute value for initializing the frames. Use a relative value based on the current orientation and bounds.
If we rotate it to any orientation, just do a resizing mechanism at runtime. Make sure the autoresizingMask is set correctly.
You only need the heights, you don't need all of that unnecessary things inside a UITableViewCell to determine the row height. You may not even need the width, because as I said the width value should be relative to the view bounds.
Here is my approach for solving this
I assume in this solution that only one Label has a "dynamic" height
I also assume if we make the label auto size to stretch the height as the cell grows only the cell height is needed to change
I assume that the nib has the appropriate spacing for where the label will be and how much space is above and bellow it
We dont want to change the code every time we change the font or position of the label in the nib
How to update the height:
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
// We want the UIFont to be the same as what is in the nib,
// but we dont want to call tableView dequeue a bunch because its slow.
// If we make the font static and only load it once we can reuse it every
// time we get into this method
static UIFont* dynamicTextFont;
static CGRect textFrame;
static CGFloat extraHeight;
if( !dynamicTextFont ) {
DetailCell *cell = [tableView dequeueReusableCellWithIdentifier:#"cell"];
dynamicTextFont = cell.resizeLabel.font;
CGRect cellFrame = cell.frame;
textFrame = cell.resizeLabel.frame;
extraHeight = cellFrame.size.height-textFrame.size.height; // The space above and below the growing field
}
NSString* text = .... // Get this from the some object using indexPath
CGSize size = [text sizeWithFont:dynamicTextFont constrainedToSize:CGSizeMake(textFrame.size.width, 200000.f) lineBreakMode:UILineBreakModeWordWrap];
return size.height+extraHeight;
}
Issues:
If you are not using a prototype cell you will need to check if the cell is nil and init it
Your nib / storyboard must have the UILabel autosize and have multi line set to 0
You should have a look at TTTableItemCell.m in the Three20 framework. It follows a different approach, basically by having each cell class (with some predefined settings like font, layout etc.) implement a shared method + tableView: sizeForItem: (or something like that), where it gets passed the text in the item object. When you look up the text for a specific cell, you can as well look up the appropriate font, too.
Regarding the cell height: You can check your tableView's width and, if necessary, subtract the margins by UITableViewStyleGrouped and the width an eventual index bar and disclosure item (which you look for in the data storage for your cells' data). When the width of the tableView changes, e.g. by interface rotation, you have to call [tableView reloadData].
To answer the question the original poster asked which was 'is it ok to call cellForRowAtIndexPath?', it's not. That will give you a cell but it will NOT allocate it to that indexPath internally nor will it be re-queued (no method to put it back), so you'll just lose it. I suppose it will be in an autorelease pool and will be deallocated eventually, but you'll still be creating loads of cells over and over again and that is really pretty wasteful.
You can do dynamic cell heights, you can even make them look quite nice, but it's a lot of work to really make them look seamless, even more if you want to support multiple orientations etc.
I have an idea about dynamic cell height.
Just create one instance of your custom cell as member variable of UITableViewController. In the tableView:heightForRowAtIndexPath: method set the cell's content and return the cell's height.
This way you won't be creating/autoreleasing cell multiple times as you will if you call cellForRowAtIndexPath inside the heightForRowAtIndexPath method.
UPD: For convenience, you can also create a static method in your custom cell class that will create a singleton cell instance for height calculation, set the cell's content and then return it's height.
tableView:heightForRowAtIndexPath: function body will now look like this:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return [MyCell cellHeightForContent:yourContent];
}
Here's my solution which I've used to implement some rather slick cells for a chatting app.
Up to this point I've always been really really irritated with heightForCellAtIndexPath: because it leads to violating the DRY principle. With this solution my heightForRowAtIndexPath: costs 1.5ms per cell which I could shave down to ~1ms.
Basically, you want each subview inside your cell to implement sizeThatFits: Create an offscreen cell which you configure then query the root view with sizeThatFits:CGSizeMake(tableViewWidth, CGFLOAT_MAX).
There are a few gotchas along the way. Some UIKit views have expensive setter operations. For example -[UITextView setText] does a lot of work. The trick here is to create a subclass, buffer the variable, then override setNeedsDisplay to call -[super setText:] when the view is about to be rendered. Of course, you'll have to implement your own sizeThatFits: using the UIKit extensions.
I have a UITableView with style "Grouped" which I use to set some options in my App. I'd like for one of the cells of this UITableView to only show up depending on whether another of this UITableView's cells is activated or not. If it's not, the first cell should show up (preferably with a smooth animation), if it is, the first cell should hide.
I tried returning nil in the appropriate -tableView:cellForRowAtIndexPath: to hide the cell, but that doesn't work and instead throws an exception.
I'm currently stuck and out of ideas how to solve this, so I hope some of you can point me in the right direction.
You should remove the data behind the hidden cells from the table view's data source.
For example, if you are using an array, when an action occurs that causes a cell to be hidden, you would remove the object for that row from the array. Then, as the table view's data source, the array will return one less total count and only return valid cells for every row in that count (no nil).
This approach may require maintaining a second array with all of the objects (including hidden).
To update the view, check out reloadRowsAtIndexPaths:withRowAnimation:.
Here's a handy post in which the author provides some source code for performing animations on the currently selected cell:
http://iphonedevelopment.blogspot.com/2010/01/navigation-based-core-data-application.html
He's using this in a NSFetchedResultsController context, but you can see how he's using various calls to add/remove cells & sections.
Now, in your case, you'll need to modify whatever array you're using to host the data used to generate the rows in your tableView when you "activate" your cell, then selectively use:
tableView:insertRowsAtIndexPaths:withRowAnimation:
tableView:deleteRowsAtIndexPaths:withRowAnimation:
tableView:insertSections:withRowAnimation:
tableView:deleteSections:withRowAnimation:
to adjust things accordingly (you can start with tableView:reloadData:, but it's inefficient).
I realize that the API can be a bit daunting, but take the time to read through it and understand what the various calls do. Understanding how the UITableView uses its datasource and delegate, as well as the chain of events that occur when cells are selected/deleted/etc., is important if you want to get things just right (and crash-free).
[tableView beginUpdates];
[tableView deleteRowsAtIndexPaths:withRowAnimation:]; // or insertRowsAtIndexPaths:withAnimation:
[tableView endUpdates];
Before cellForRowAtIndexPath is called, numberOfRowsInSection is called. You should return the appropriate value of cells in the section there, so if you only want to show 1 cell, return one. The logic what cells are shown has to be implemented partially in both methods