I have a UITableView for which I created two different custom cells, let's call one "RegularCell" and the other "BigCell". The reason to do so is that I need different representation for the data model objects, where in a certain case I wish to present the data differently.
I read a bit about ways to approach it via heightForRowAtIndexPath vs. cellForRowAtIndexPath, but I'm not clear how to approach it in my case >> In my table, I don't know, in advance, which row will include which custom cell; I only get this data in cellForRowAtIndexPath where I check in the data array which case I need to represent for a specific row.
It seems silly to do the calculation in heightForRowAtIndexPath since it's called before cellForRowAtIndexPath and the whole idea there is that you don't create all the cells in advance and just "make room" for things like the scroller size.
On the other hand, only when I realise which content I'm representing, I can tell which cell I require and therefore what should be the row height.
Anyone encountered a case like that and can share some wisdom?
David is right!
I don't agree with you saying
In my table, I don't know, in advance, which row will include which
custom cell; I only get this data in cellForRowAtIndexPath where I
check in the data array which case I need to represent for a specific
row.
in heightForRowAtIndexPath you can access you datasource the same way you do it in cellForRowAtIndexPath:
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
CGFloat bigCellHeight = 80.0;
CGFloar regularCellHeight = 44.0;
MyDataObject *object = [myArray objectAtIndex: indexPath.row];
if ([object anyConditionToChoseBigCell]) {
return bigCellHeight;
}
return regularCellHeight;
}
heightForRowAtIndexPath is called before cellForRowAtIndexPath as the tableView layouter needs to know how big the scroll view is going to be as well as what cells should be visible on screen at the time its going to be displayed, this may vary depending on the height of each cell.
I've come across this problem before and my recommendation is that you calculate the height of each of the cells in your model before your tableView is even run.
I've had similar problem. That's what I ended up with:
-(CGFloat)tableView:(UITableView *)tableView
heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
return cell.frame.size.height;
}
OK...
I found an easy solution - Since I have an array of data objects, I can check per each row, the relevant data object relevant property >> in my case, I have a BOOL value called isSuper >> and in this way I can set the row height per each object in the DB without the need to create or upload an actual UITableViewCell in heightForRowAtIndexPath.
Related
I have an UITableView with ±10 different UITableViewCells to display full information about an object (description cell, photos cell etc.). So when UITableView is loaded, I do not need UITableView cells to be reused. Wouldn't performance be better if I somehow store UITableViewCells and prevent cellForRowAtIndexPath from being called? If so, what is the way to achieve alike behaviour?
First of all, you can not prevent cellForRowAtIndexPath from being called if you are gonna use UITableView. It is a UITableViewDataSource function and it's not an optional one. Otherwise, you won't be able to populate your tableview.
What you can do is use switch case on indexpath.row in cellForRowAtIndexPath and return necessary cell.
You could try by increasing prototype cell in Storyboard. each cell assign new cell identifier and you need to keep array of identifier matching their index with storyboard.
Your cellForRowAtIndexpath will reduced to:
- (UITableViewCell *)tableView:(UITableView *)tableView1 cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView1 dequeueReusableCellWithIdentifier:[cellIdArray objectAtIndex:indexPath.row] forIndexPath:indexPath];
return cell;
}
Note: cellForRowAtIndexpath method will always called. In above case you can put your data in storyboard itself.
Check similar case at https://www.appcoda.com/sidebar-menu-swift/
I have a table view which has prototype cells. How do I set the height of all cells using a specific identifier? For example, I have two cells; one with an idenfier of "cell10" and another with the identifier "cell50". How do I set it so all cells with the identifier "cell10" have a height of 10 while all cells with the identifier "cell50" have a height of 50? Any answers are appreciated. (By the way, I am using Swift 2.)
Within heightForRowAtIndexPath() you can call cellForRowAtIndexPath().
Once you have the cell then you can call reuseIdentifier to get its identifier.
Then return either 10 or 50 based on the identifier
I'm not sure how to do it in swift but in objective C i would do the following things-
Implement the following function of tableview-
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
//your custom cell
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
if(cell.reuseIdentifier isEqualToString:#"cell10"){
return 10;
}else{
return 50;
}
}
heightForRowAtIndexPath called first and then cellForRowAtIndexPath get called. So it may be possible that in heightForRowAtIndexPath method we do not get cell or some inconsistency.
You must have some value or field on basis of which you can specify or differentiate cell identifier. You have array of models which may contain that value.
So in heightForRowAtIndexPath(), you can get model from datasource array for that indexpath using objectAtIndex method, and check which type it is and depending on that return the height.
This will help in every case. It is working for me.
I am working on an ios application,
I have a normal table view. When calling heightForRowAtIndexPath I am doing the folowing
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *cellId = [self getCellIds][indexPath.row];
BaseTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellId];
return [cell calculateHeigh];
}
Basically I am dequeueing the cell because I have a function calculateHeigh inside every cell that will do the height calculation. this is working fine as intended however I have a concern:
Is it safe to call dequeueReusableCellWithIdentifier: inside the heightForRowAtIndexPath ? will it cause any issue?
EDIT:
Just to clarify why I did this, I have a big amount of custom cells with different identifier that needs to be loaded. and to avoid having a huge if-else statement in my heightForRowAtIndexPath I placed the getter of the cell height in the custom cell that way I just ask it to return it (no calculation is made there), I can't do it as a class method as I don't know which class, I can get the object from the identifier and not the class. And I want to avoid a big if-else just for code readability.
So my concern was with the dequeueReusableCellWithIdentifier: is it heavy to call it when getting the height? will it cause memory issues or lags? or is it worth to just do a bug if-else of use a dictionary?
First of all you should avoid any calculations in table drawing methods(such as heightForRow, cellForRow, etc). These methods are called a lot and although your table may be short and/or not complicated(with custom cells with a lot of labels, buttons and images) you should always try to optimize this drawing process or otherwise user will experience some nasty lag when scrolling.
So you should call some method to prepare data before calling 'reloadTable'
-(void)prepareMethod
{
//get only one cell to calculate all row heights
BaseTableViewCell *cell = [_myTableView dequeueReusableCellWithIdentifier:cellId];
for (NSDictionary* dataObj in _dataArray)
{
//loop through all rows data and set new property for row height
dataObj[#"rowHeight"] = [cell calculateHeigh];
}
}
And then when calling heightForRow just pass this value without any expensive operation(such as probably string calculations):
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
//always make sure you don't access unexisting array index
return ( indexPath.row < _dataArray.count ) ? _dataArray[indexPath.row][indexPath.row][#"rowHeight"] : 1.0;
}
Of course you don't need separate method just to populate row heights in your data array - you can populate this value when populating(formatting) your data array to avoid second array iteration. It all depends on your current implementation.
Just remember that expensive drawing methods(not only for table though) should always be as short as possible and just get data needed for drawing and draw. It's really so simple. If you need to make some complicated calculations do it before that(maybe in view init) so your data is prepared before actual drawing. This way your application will be working smoothly even with bigger tables(because no matter how big the table is, UITableViewController draws only visible cells).
Regards,
hris.to
I don't like to have big if statements in heightForRowAtIndexPath and accessing a cell using dequeueReusableCellWithIdentifier. Your approach getting cell height from each cell quite is reasonable. I believe your calculateHeigh return value depends on the table data you pass into the cell.
In BaseTableViewCell.h
+ (CGFloat)heightWithData:(id)data;
In BaseTableViewCell.m
+ (CGFloat)heightWithData:(id)data
{
//put your calculateHeigh logic here. I believe your calculateHeigh depends on the data each cell has.
}
Then you can do
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return [BaseTableViewCell heightWithData:[self.tableData objectAtIndex:indexPath.row]];
}
If you do this, you don't need to access each cell object to get cell height.
You should not use this method to provide the calculation. Based on what I can see on your setup, you are calculating the height based on the values already on your cell. What happens is that the cell dequeue system will give you a cell to reuse, but because it's sharing cells from multiple index paths, that cell probably has data that belongs to a record of an index path different from the current one. Get the calculate height code and try to reproduce it inside the datasource callback you are using.
I'm trying to create detail view controller as a list of information and I think it would be nice and clean to present this with a static UITableView. But after that it came to my mind that on some level it might be difficult, so please resolve my doubts!
Every UITableViewCell has different style (some are custom, some are basic and few are right-detailed etc.).
What is more, content size of each cell may vary as I have long names put inside labels so they use autolayout to fit.
There is no problem when I have the same cells repeating but with different tex inside UILabels. In that case I use a simple:
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (!self.prototypeCell) {
self.prototypeCell = [self.tableView dequeueReusableCellWithIdentifier:#"ActivityCell"];
}
[self fetchedResultsController:[self fetchedResultsController] configureCell:self.prototypeCell atIndexPath:indexPath];
CGSize size = [self.prototypeCell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
return size.height;
}
I don't know how to deal with heightForRowAtIndexPath. I can give an identifier to each cell, call cellForRowAtIndexPath:, and make a big switch or if statement, but is it right? The same problem occurs while I think of cellForRowAtIndexPath: and populating those UITableViewCells. With those testing statements this code won't be pretty and readable.
Any ideas on that case?
In the delegate function of the table view named heightForRowAtIndexPath try to calculate the height for each row and then return it.
//return height for row
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if(tableView==tblLanguage)
{
//Here calculate the dynamic height according to songs count for specific language
return (([[arrSongListForSpecificLanguage objectAtIndex:indexPath.row] count]*40)+40);
}
return 40.0;
}
I have a UITableView with a few different sections. One section contains cells that will resize as a user types text into a UITextView. Another section contains cells that render HTML content, for which calculating the height is relatively expensive.
Right now when the user types into the UITextView, in order to get the table view to update the height of the cell, I call
[self.tableView beginUpdates];
[self.tableView endUpdates];
However, this causes the table to recalculate the height of every cell in the table, when I really only need to update the single cell that was typed into. Not only that, but instead of recalculating the estimated height using tableView:estimatedHeightForRowAtIndexPath:, it calls tableView:heightForRowAtIndexPath: for every cell, even those not being displayed.
Is there any way to ask the table view to update just the height of a single cell, without doing all of this unnecessary work?
Update
I'm still looking for a solution to this. As suggested, I've tried using reloadRowsAtIndexPaths:, but it doesn't look like this will work. Calling reloadRowsAtIndexPaths: with even a single row will still cause heightForRowAtIndexPath: to be called for every row, even though cellForRowAtIndexPath: will only be called for the row you requested. In fact, it looks like any time a row is inserted, deleted, or reloaded, heightForRowAtIndexPath: is called for every row in the table cell.
I've also tried putting code in willDisplayCell:forRowAtIndexPath: to calculate the height just before a cell is going to appear. In order for this to work, I would need to force the table view to re-request the height for the row after I do the calculation. Unfortunately, calling [self.tableView beginUpdates]; [self.tableView endUpdates]; from willDisplayCell:forRowAtIndexPath: causes an index out of bounds exception deep in UITableView's internal code. I guess they don't expect us to do this.
I can't help but feel like it's a bug in the SDK that in response to [self.tableView endUpdates] it doesn't call estimatedHeightForRowAtIndexPath: for cells that aren't visible, but I'm still trying to find some kind of workaround. Any help is appreciated.
As noted, reloadRowsAtIndexPaths:withRowAnimation: will only cause the table view to ask its UITableViewDataSource for a new cell view but won't ask the UITableViewDelegate for an updated cell height.
Unfortunately the height will only be refreshed by calling:
[tableView beginUpdates];
[tableView endUpdates];
Even without any change between the two calls.
If your algorithm to calculate heights is too time consuming maybe you should cache those values.
Something like:
- (CGFloat)tableView:(UITableView *)tableView
heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
CGFloat height = [self cachedHeightForIndexPath:indexPath];
// Not cached ?
if (height < 0)
{
height = [self heightForIndexPath:indexPath];
[self setCachedHeight:height
forIndexPath:indexPath];
}
return height;
}
And making sure to reset those heights to -1 when the contents change or at init time.
Edit:
Also if you want to delay height calculation as much as possible (until they are scrolled to) you should try implementing this (iOS 7+ only):
#property (nonatomic) CGFloat estimatedRowHeight
Providing a nonnegative estimate of the height of rows can improve the
performance of loading the table view. If the table contains variable
height rows, it might be expensive to calculate all their heights when
the table loads. Using estimation allows you to defer some of the cost
of geometry calculation from load time to scrolling time.
The default value is 0, which means there is no estimate.
This bug has been fixed in iOS 7.1.
In iOS 7.0, there doesn't seem to be any way around this problem. Calling [self.tableView endUpdates] causes heightForRowAtIndexPath: to be called for every cell in the table.
However, in iOS 7.1, calling [self.tableView endUpdates] causes heightForRowAtIndexPath: to be called for visible cells, and estimatedHeightForRowAtIndexPath: to be called for non-visible cells.
Variable row heights have a very negative impact on your table view performance. You are talking about web content that is displayed in some of the cells. If we are not talking about thousands of rows, thinking about implementing your solution with a UIWebView instead of a UITableView might be worth considering. We had a similar situation and went with a UIWebView with custom generated HTML markup and it worked beautifully. As you probably know, you have a nasty asynchronous problem when you have a dynamic cell with web content:
After setting the content of the cell you have to
wait until the web view in the cell is done rendering the web content,
then you have to go into the UIWebView and - using JavaScript - ask the HTML document how high it is
and THEN update the height of the UITableViewCell.
No fun at all and lots of jumping and jittering for the user.
If you do have to go with a UITableView, definitely cache the calculated row heights. That way it will be cheap to return them in heightForRowAtIndexPath:. Instead of telling the UITableView what to do, just make your data source fast.
Is there a way?
The answer is no.
You can only use heightForRowAtIndexPath for this.
So all you can do is make this as inexpensive as possible by for example keeping an NSmutableArray of your cell heights in your data model.
I had a similar issue(jumping scroll of the tableview on any change) because I had
(CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath {
return 500; }
commenting the entire function helped.
Use the following UITableView method:
- (void)reloadRowsAtIndexPaths:(NSArray *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation
You have to specify an NSArray of NSIndexPath which you want to reload. If you want to reload only one cell, then you can supply an NSArray that holds only one NSIndexPath.
NSIndexPath* rowTobeReloaded = [NSIndexPath indexPathForRow:1 inSection:0];
NSArray* rowsTobeReloaded = [NSArray arrayWithObjects:rowTobeReloaded, nil];
[UITableView reloadRowsAtIndexPaths:rowsTobeReloaded withRowAnimation:UITableViewRowAnimationNone];
The method heightForRowAtIndexPath: will always be called but here's a workaround that I would suggest.
Whenever the user is typing in the UITextView, save in a local variable the indexPath of the cell. Then, when heightForRowAtIndexPath: is called, verify the value of the saved indexPath. If the saved indexPath isn't nil, retrieve the cell that should be resized and do so. As for the other cells, use your cached values. If the saved indexPath is nil, execute your regular lines of code which in your case are demanding.
Here's how I would recommend doing it:
Use the property tag of UITextView to keep track of which row needs to be resized.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
...
[textView setDelegate:self];
[textView setTag:indexPath.row];
...
}
Then, in your UITextView delegate's method textViewDidChange:, retrieve the indexPath and store it. savedIndexPath is a local variable.
- (void)textViewDidChange:(UITextView *)textView
{
savedIndexPath = [NSIndexPath indexPathForRow:textView.tag inSection:0];
}
Finally, check the value of savedIndexPath and execute what it's needed.
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if (savedIndexPath != nil) {
if (savedIndexPath == indexPath.row) {
savedIndexPath = nil;
// return the new height
}
else {
// return cached value
}
}
else {
// your normal calculating methods...
}
}
I hope this helps! Good luck.
I ended up figuring out a way to work around the problem. I was able to pre-calculate the height of the HTML content I need to render, and include the height along with the content in the database. That way, although I'm still forced to provide the height for all cells when I update the height of any cell, I don't have to do any expensive HTML rendering so it's pretty snappy.
Unfortunately, this solution only works if you've got all your HTML content up-front.