I am able to expand and collapse the tableView sections successfully however I am not able to do it for individual sections so far.So all the sections collapse or expand at the same time.
You can collapse and expand tableView section by removing and adding tableViewCells on demand, like if you want to collapse reload that tableViewSection data and return zero in numberOfRowsInSection, and when you want to expand it back just return right amount of rows from numberOfRowsInSection method, it should be something like below
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
//Check if section is collapsed
if (section_is_collapsed) return 0;
return actual_num_of_rows_in_Section;
}
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.
I've looked through lots of examples of how to hide a static UITableViewCell by overridding heightForRowAtIndexPath, and while I've now got it working it just seems so cumbersome that I'd like to see if I'm doing something wrong.
I have a UITableViewController with a table view that has about 8 rows. This screen in my app shows a single object, so for example one row is description, one is an image, one holds a map view, etc. All of the rows are static.
In some cases, some of the objects that are shown don't have a map, so I want to hide the row that holds the mapview. Since it's a static row, I was thinking that by having an outlet property for that row (e.g. #property (weak, nonatomic) IBOutlet UITableViewCell *mapViewRow;), then I could somehow set that row's height to 0 or hide that row in viewDidLoad or viewWillAppear. However, it seems like the only way to do this is to override the heightForRowAtIndexPath method, which is kind of annoying because then I need to hardcode the index of the map row in my code, e.g.
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
if (indexPath.row == 6 && self.displayItem.shouldHideMap) {
return 0;
}
return [super tableView:tableView heightForRowAtIndexPath:indexPath];
}
Of course, not a big deal, but just the whole way of sizing static rows in a tableview seems like it defeats the point of setting them up in the storyboard in the first place.
EDIT - rationale behind my answer
To change the height of a row you must reload either the whole table or a subset containing that row. B/c it's a bit odd to have a row in the table w/ zero height, I prefer modifying your data source such that the row doesn't exist in the table.
There are a number of ways to do that. You could build an array from your displayItem where each row in the array corresponds to a row in the table w/ appropriate data. You would rebuild this array and then call [tableView reloadData]. My original answer would also eliminate the unwanted row by treating each data element as a section with 0 or 1 rows.
ORIGINAL ANSWER
Is your tableview a plain or grouped style? If it's a plain style, you could treat each row as a section with either 0 or 1 rows in it. In your tableView dataSource and delegate methods you would use the section index to identify the data within self.displayItem that you care about for that section.
Your code would be something like:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 8; // max number of possible rows in table
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
NSInteger rows = 1;
// set self.mapSectionIndex during initialization or hard code it
if (section == self.mapSectionIndex && self.displayItem.shouldHideMap) {
rows = 0;
}
return rows;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:indexPath
{
return 60.0f; // whatever you want the height to be
}
// also modify tableView:cellForRowAtIndexPath: and any other tableView delegate and dataSource methods appropriately
you can override heightForRowAtIndexPath and just write in it return UITableViewAutomaticDimension; this will make cell calculate the height automatically as UILabel height is >= . it worked for me.
I am trying to implement a UITableView where the odd indexes are a 'separator'. The problem is, when I have one object in my dataSource and load the tableView, I see the first cell fine. However, when there are two objects in my dataSource and then load the tableView, I only see the one cell even though there should be two cells. I have tried to reloadData to cell if that would change anything and it did not.
If you need code to see, just let me know!
I suspect your problem is in tableView:numberOfRowsInSection:. Because you are inserting "spacer" cells before your table's data cells, the number of rows returned from that method needs to take into account the spacer cells also, so the correct number to return is twice the number of elements in your array.
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [self.cellArray count] * 2;
}
I want to expand UITableView section where I have to show UITableViewCells. What should I do to achieve this?
A simple implementation would be keep your cell height as zero for
sections.
Make the viewForSectionHeader touchable
When you touch it, set proper height for cells under the section
Write a logic for switching between sections
OR,
On touching the section header reload the table with updated rows count for section touched.
Many other ways to do it. Apple example.
According to Vignesh's answer, I have tried the second solution."On touching the section header reload the table with updated rows count for section touched."
First, declare an array to store the isExpanded tag for each section.Initial, all value is BOOL NO.Means all the section rows are collapsed.
Then, in the
- (UIView *)tableView:(UITableView *)tableView viewForHeaderInSection:(NSInteger)section
method, implement the sectionHeader touch event by following code:
Because I use a custom cell as section header view. so here is the "cell",you can use your own view.
cell.tag=section;
UITapGestureRecognizer *recognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(sectionTapped:)];
[cell addGestureRecognizer:recognizer];
cell.userInteractionEnabled=YES;
And, do something when the sectionHeader is tapped.
- (void)sectionTapped:(UITapGestureRecognizer *)recognizer
{
NSMutableArray *isSectionTouched=[[NSMutableArray alloc]initWithCapacity:_sectionExpandBool.count];
isSectionTouched=[_sectionExpandBool mutableCopy];
if ([[isSectionTouched objectAtIndex:recognizer.view.tag]boolValue]==YES) {
[isSectionTouched replaceObjectAtIndex:recognizer.view.tag withObject:[NSNumber numberWithBool:NO]];
}else if ([[isSectionTouched objectAtIndex:recognizer.view.tag]boolValue]==NO){
[isSectionTouched replaceObjectAtIndex:recognizer.view.tag withObject:[NSNumber numberWithBool:YES]];
}
_sectionExpandBool=isSectionTouched;
[self.tableView reloadData];
}
Don't forget to modify the numberOfRowsInSection method. The row.count should change according the value of _sectionExpandBool, if the section's ExpandBool is YES, should return the correct number of your datasource,else return 0.
It works as my expectation. However, I'm a little worried about the memory leak or something, because every time tap the header, the whole table view will reload again.
I wonder if there is some solution for only reload the specific section. Thanks.