How to protect tableview from crashing during insertRowsAtIndexPaths and device rotation - ios

This sample app will crash after some time if you constantly rotate the device during the row animations. My real app crashes sometime even on the first rotate during the row animations.
How should I protect my app from crashing during rotation with simultanous row animations? Please don't suggest to forbid rotation until the animations are done. DataSource is dependent on network fetches which may take anything from 1 to 30 seconds depending on user network and user wants to rotate the device if he sees the app is better viewed in landscape for example right after launch.
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
for (int i = 0; i < 30; i++) {
[NSThread sleepForTimeInterval:0.2]; // imitates fetching and parsing
[self.array addObject:[NSString stringWithFormat:#"cell number %d", i]];
dispatch_async(dispatch_get_main_queue(), ^{
// perform on main
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
[self.tableView insertRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
});
}
});
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.array.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"cell" forIndexPath:indexPath];
// Configure the cell...
cell.textLabel.text = self.array[indexPath.row];
return cell;
}
- (NSMutableArray *)array
{
if (!_array) {
_array = [[NSMutableArray alloc] init];
}
return _array;
}
Crash report
2014-02-21 12:47:24.667 RowsAnimationRotate[2062:60b] *** Assertion failure in -[UITableView _endCellAnimationsWithContext:], /SourceCache/UIKit/UIKit-2903.23/UITableView.m:1330
2014-02-21 12:47:24.673 RowsAnimationRotate[2062:60b] *** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (8) must be equal to the number of rows contained in that section before the update (8), plus or minus the number of rows inserted or deleted from that section (1 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).'

You have essentially created a race condition.
The problem is you are manipulating self.array in a background thread while self.tableView insertRowsAtIndexPaths is running on the main thread and will be accessing self.array.
So at some pointself.tableView insertRowsAtIndexPaths (or other tableView methods called as a result of this) is running on the main thread, expecting a certain number of objects in self.array, but the background thread gets in there and adds another one...
To fix your simulation:
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
for (int i = 0; i < 30; i++) {
[NSThread sleepForTimeInterval:0.2]; // imitates fetching and parsing
NSString *myNewObject = [NSString stringWithFormat:#"cell number %d", i]];
dispatch_async(dispatch_get_main_queue(), ^{
// perform on main
[self.array addObject: myNewObject];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
[self.tableView insertRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
});
}
});

can you change
dispatch_async(dispatch_get_main_queue(), ^{
// perform on main
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
[self.tableView insertRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
});
with:
dispatch_async(dispatch_get_main_queue(), ^{
// perform on main
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
[self.tableView insertRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationNone];
});

Can you please try your simulation with synchronized:-
It declares a critical section around the code block. In multithreaded code, #synchronized guarantees that only one thread can be executing that code in the block at any given time.
#synchronized(self.tableView) {
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(queue, ^{
for (int i = 0; i < 30; i++) {
[NSThread sleepForTimeInterval:0.2]; // imitates fetching and parsing
[self.array addObject:[NSString stringWithFormat:#"cell number %d", i]];
dispatch_async(dispatch_get_main_queue(), ^{
// perform on main
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i inSection:0];
[self.tableView insertRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
});
}
});
}
Hope that helps. Please let me know if we have to go with another solution.

Related

assertion failure with insertRowsAtIndexPaths

I'm facing this error:
Assertion failure in -[UITableView _endCellAnimationsWithContext:]
When I'm trying to use insertRowsAtIndexPaths:withRowAnimation:
Basically I have a NSMutableArray with objects, call this self.objects.
I'm adding objects to it like so:
MyObject *something = [MyObject new];
[self.objects addObject:something];
NSInteger count = self.objects.count;
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:count];
[self.tableView insertRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
A thing to note is that I'm using section rather than row to achieve cell spacing.
Update
Error message:
*** Assertion failure in -[UITableView _endCellAnimationsWithContext:], /BuildRoot/Library/Caches/com.apple.xbs/Sources/UIKit/UIKit-3512.30.14/UITableView.m:1704
data source methods:
-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return [self.objects count];
}
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return 1;
}
Code Which will work:
MyObject *something = [MyObject new];
[self.objects addObject:something];
NSInteger count = self.objects.count;
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:count-1]
withRowAnimation:UITableViewRowAnimationAutomatic];
Description:
In your table view implementation you are using one row for reach section. And you are trying to insert row to the section which in not available(created).
So you have to first insert the section that only you can insert rows(in your implementation you are having one row per section so only insert the section).
It will be
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:0 inSection:count-1];

iOS - UITableView Delete All Rows Within A Section

I have a UITableView with expandable sections. When a user goes to another view, I need all the expanded sections to collapse, which I'll need to put in the viewWillDisappear method.
I've found solutions only on how to delete all rows from a table view at once, but is there a way to delete all the rows from a specific section?
EDIT:
I have figured out a solution, but I'm not sure if it's optimal or can lead to inefficiencies in the future. Whenever a cell is expanded, it gets added to an NSMutableIndexSet. So in my viewWillDisappear method, I iterate over the expanded sections like so:
-(void)viewWillDisappear:(BOOL)animated
{
if (expandedSections.count != 0) {
NSLog(#"COLLAPSING CALLED");
[self.tableView beginUpdates];
NSUInteger section = [expandedSections firstIndex];
do
{
NSInteger rows;
NSMutableArray *tmpArray = [NSMutableArray array];
rows = [self tableView:self.tableView numberOfRowsInSection:section];
[expandedSections removeIndex:section];
for (int i=1; i<rows; i++) {
NSIndexPath *tmpIndexPath = [NSIndexPath indexPathForRow:i inSection:section];
[tmpArray addObject:tmpIndexPath];
}
[self.tableView deleteRowsAtIndexPaths:tmpArray withRowAnimation:UITableViewRowAnimationTop];
NSIndexPath *expandableCellIndexPath = [NSIndexPath indexPathForRow:0 inSection:section];
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:expandableCellIndexPath];
cell.accessoryView = [DTCustomColoredAccessory accessoryWithColor:[self.colorHolder objectAtIndex:section] type:DTCustomColoredAccessoryTypeRight];
section = [expandedSections indexGreaterThanIndex:section];
} while (section != NSNotFound);
[self.tableView endUpdates];
}
}
Please let me know if this is a good solution or, if I'm suspecting correctly, if this will lead to slower transitions between views in the future when there are more rows in each expanded section. Any help or advice would be appreciated. Thanks.
If you want to animate changes, you will need to first update your data source (to return 0 for number of rows in the section) then remove section and add section at the same index path in one transaction between [tv beginUpdates] [tv endUpdates]
Otherwise just update the data source and reload the table on your way back to the VC (if you don't want any animations)
I did not read this in detail but surely the for loop should start at zero.
for (int i=0; i<rows; i++) {
NSIndexPath *tmpIndexPath = [NSIndexPath indexPathForRow:i inSection:section];
[tmpArray addObject:tmpIndexPath];
}
otherwise you will only delete all but the first cells in the section.

Add rows to the beginning of a tableview without animation

I want to add rows to a tableview with begin/endUpdates to prevent the jump of the tableview when i do reloadData
this is my code
- (void)updateTableWithNewRowCount:(NSInteger)rowCount
andNewData:(NSArray *)newData {
// Save the tableview content offset
CGPoint tableViewOffset = [self.messagesTableView contentOffset];
// Turn of animations for the update block
// to get the effect of adding rows on top of TableView
[UIView setAnimationsEnabled:NO];
[self.messagesTableView beginUpdates];
NSMutableArray *rowsInsertIndexPath = [[NSMutableArray alloc] init];
int heightForNewRows = 0;
for (NSInteger i = 0; i < rowCount; i++) {
NSIndexPath *tempIndexPath = [NSIndexPath indexPathForRow:i inSection:0];
[rowsInsertIndexPath addObject:tempIndexPath];
// [self.messages insertObject:[newData objectAtIndex:i] atIndex:i];
[self addMessage:[newData objectAtIndex:i]];
heightForNewRows =
heightForNewRows + [self heightForCellAtIndexPath:tempIndexPath];
}
[self.messagesTableView insertRowsAtIndexPaths:rowsInsertIndexPath
withRowAnimation:UITableViewRowAnimationNone];
tableViewOffset.y += heightForNewRows;
[self.messagesTableView endUpdates];
[UIView setAnimationsEnabled:YES];
[self.messagesTableView setContentOffset:tableViewOffset animated:NO];
}
And sometimes (not everytime) I get this error
invalid number of rows in section 0.
The number of rows contained in an existing section after
the update (2) must be equal to the number of rows contained in that section
before the update (2), plus or minus the number of rows inserted or deleted
from that section (2 inserted, 0 deleted) and plus or minus the number of
rows moved into or out of that section (0 moved in, 0 moved out).
How do i prevent this error ?
if you dont want animation then use directly
- (void)updateTableWithNewRowCount:(NSInteger)rowCount
andNewData:(NSArray *)newData {
// Save the tableview content offset
CGPoint tableViewOffset = [self.messagesTableView contentOffset];
// Turn of animations for the update block
// to get the effect of adding rows on top of TableView
NSMutableArray *rowsInsertIndexPath = [[NSMutableArray alloc] init];
int heightForNewRows = 0;
for (NSInteger i = 0; i < rowCount; i++) {
NSIndexPath *tempIndexPath = [NSIndexPath indexPathForRow:i inSection:0];
[rowsInsertIndexPath addObject:tempIndexPath];
// [self.messages insertObject:[newData objectAtIndex:i] atIndex:i];
[self addMessage:[newData objectAtIndex:i]];
heightForNewRows =
heightForNewRows + [self heightForCellAtIndexPath:tempIndexPath];
}
tableViewOffset.y += heightForNewRows;
[self.messagesTableView reloadData];
[self.messagesTableView setContentOffset:tableViewOffset animated:NO];
}
If you want to quickly jump to a selected indexPath (with or without animation), you can use this function:
//My function to populate
//tableView is synthesized
-(void)setNewMessage:(NSString*)message{
// ...
NSIndexPath *selectedIndexPath = [tableView indexPathForSelectedRow];
// if your selectedIndexPath==nil, the table scroll stay in the same position
[self reloadData:selectedIndexPath animated:NO];
}
//My reload data wrapper
-(void)reloadData:(NSIndexPath *)selectedIndexPath animated:(BOOL)animated{
[tableView reloadData];
// atScrollPosition can receive different parameters (eg:UITableViewScrollPositionMiddle)
[tableView scrollToRowAtIndexPath:selectedIndexPath
atScrollPosition:UITableViewScrollPositionTop
animated:animated];
}
Once I needed that same flexibility in a tableView and this function has met my needs. You don`t need the "setAnimationsEnabled", just use:
[tableView scrollToRowAtIndexPath:nil
atScrollPosition:UITableViewScrollPositionNone
animated:NO];
];
I hope it helped.
Try this, I am not sure whether this will work or not.
- (void)updateTableWithNewRowCount:(NSInteger)rowCount
andNewData:(NSArray *)newData {
// Save the tableview content offset
CGPoint tableViewOffset = [self.messagesTableView contentOffset];
// Turn of animations for the update block
// to get the effect of adding rows on top of TableView
[UIView setAnimationsEnabled:NO];
NSMutableArray *rowsInsertIndexPath = [[NSMutableArray alloc] init];
int heightForNewRows = 0;
for (NSInteger i = 0; i < rowCount; i++) {
NSIndexPath *tempIndexPath = [NSIndexPath indexPathForRow:i inSection:0];
[rowsInsertIndexPath addObject:tempIndexPath];
// [self.messages insertObject:[newData objectAtIndex:i] atIndex:i];
[self addMessage:[newData objectAtIndex:i]];
heightForNewRows = heightForNewRows + [self heightForCellAtIndexPath:tempIndexPath];
}
tableViewOffset.y += heightForNewRows;
[UIView setAnimationsEnabled:YES];
[self.messagesTableView setContentOffset:tableViewOffset animated:NO];
double delayInSeconds = 1.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
[self.messagesTableView beginUpdates];
[self.messagesTableView insertRowsAtIndexPaths:rowsInsertIndexPath
withRowAnimation:UITableViewRowAnimationNone];
[self.messagesTableView endUpdates];
});
}
Note: You might require to update rowsInsertIndexPath reference to use inside the block (__block type).

ios: UITableView reloadData not working until scroll

I've noticed a few questions similar to this one on SO but they all seem to end in the person was calling
[self.tableView reloadData]
Outside of the main thread.
My problem is, I'm calling this on the main UI thread and I still don't get the new rows loaded until I like tap the screen or scroll a little bit. Here's what I'm doing: When I enter my ViewController the table displays fully and correctly. After pressing a button on the view I call my external db and load in some new rows to be added to the table. After adding these to my data source, I call reloadData and all of the new rows are blank. The couple rows that still fit on the screen that were already part of the table still show, but nothing new. When I touch the screen or scroll a little bit all of the new cells pop up.
Here's my code starting from when my call to the server finishes:
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
if(connection == self.searchConnection) {
..... some code ......
if(successful) {
// Adding new data to the datasource
[self.locations removeObjectAtIndex:[[NSNumber numberWithInt:self.locations.count - 1] integerValue]];
[self.cellTypeDictionary removeAllObjects];
for(int i = 0; i < [jsonLocations count]; ++i) {
NSDictionary *locationDictionary = jsonLocations[i];
BTRLocation *location = [BTRLocation parseLocationJSONObject:locationDictionary];
[self.locations addObject:location];
}
if(self.currentNextPageToken != nil && ![self.currentNextPageToken isEqual:[NSNull null]] && self.currentNextPageToken.length > 0) {
BTRLocation *fakeLocation = [[BTRLocation alloc] init];
[self.locations addObject:fakeLocation];
}
[self determineCellTypes];
if([self.locations count] < 1) {
self.emptyView.hidden = NO;
} else {
... some code .....
if(self.pendingSearchIsNextPageToken) {
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:[[NSNumber numberWithInt:countBefore] integerValue]
inSection:[[NSNumber numberWithInt:0] integerValue]];
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData];
[self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionMiddle animated:NO];
});
} else {
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:[[NSNumber numberWithInt:0] integerValue]
inSection:[[NSNumber numberWithInt:0] integerValue]];
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData];
[self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionMiddle animated:NO];
});
}
}
} else {
if(invalidAccessToken) {
// TODO: invalidAccessToken need to log out
}
}
You can see I even added
dispatch_async(dispatch_get_main_queue(), ^{
[self.tableView reloadData];
[self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionMiddle animated:NO];
});
Even though I know connectionDidFinishLoading is called on the main thread. But I figured I'd just try it anyway.
I can see no reason for why this isn't working.
the proper way is:
[self.tableView reloadData];
__weak MyViewController* weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
__strong MyViewController *strongSelf = weakSelf;
[strongSelf.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionMiddle animated:NO];
});
Sometimes I found that you add after the reloadData:
[self.tableView layoutIfNeeded];

iOS : Inserting multiple rows in UITableview with insertRowsAtIndexPaths

I have already 10 rows in TableView What I am trying to do is adding another 10 rows for that I am using insertRowsAtIndexPaths but I am getting errors.
Following is the code I am using
-(void)insertDownloadedActions:(NSMutableArray *)dataToAdd
{
__weak CurrentViewController *weakSelf = self;
int64_t delayInSeconds = 2.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void){
[weakSelf.tableView beginUpdates];
[weakSelf.dataSource addObjects:dataToAdd];
NSIndexPath *newIndexPath = [NSIndexPath indexPathForRow:[weakSelf.dataSource count]-dataToAdd.count-1 inSection:0];
[weakSelf.tableView insertRowsAtIndexPaths:#[newIndexPath] withRowAnimation:UITableViewRowAnimationTop];
[weakSelf.tableView endUpdates];
});
}
But I am getting following error for that
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (20) must be equal to the number of rows contained in that section before the update (10), plus or minus the number of rows inserted or deleted from that section (1 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).
The code is close, but the table view needs to be updated with index paths in exact correspondence with what's added to the datasource.
-(void)insertDownloadedActions:(NSMutableArray *)dataToAdd
{
// don't need this
//__weak CurrentViewController *weakSelf = self;
int64_t delayInSeconds = 2.0;
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, delayInSeconds * NSEC_PER_SEC);
dispatch_after(popTime, dispatch_get_main_queue(), ^(void) {
// build the index paths for insertion
// since you're adding to the end of datasource, the new rows will start at count
NSMutableArray *indexPaths = [NSMutableArray array];
NSInteger currentCount = self.datasource.count;
for (int i = 0; i < dataToAdd.count; i++) {
[indexPaths addObject:[NSIndexPath indexPathForRow:currentCount+i inSection:0]];
}
// do the insertion
[self.dataSource addObjects:dataToAdd];
// tell the table view to update (at all of the inserted index paths)
[self.tableView beginUpdates];
[self.tableView insertRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationTop];
[self.tableView endUpdates];
});
}
You want a weakSelf to avoid cycle where the block owner retains the block and the block (by using the block owner "self") retains the owner. There's no need for the weakSelf pattern here since the view controller is not retaining a copy of the dispatched block.
In swift, to add multiple rows, We can do
let indexPaths = (0 ..< messageList.count).map { IndexPath(row: $0, section: 0) }
self.chatTableView.beginUpdates()
self.chatTableView.insertRows(at: indexPaths, with: .bottom)
self.chatTableView.endUpdates()
Here I'm inserting to indexPath:0 as I want to append the list on scrolling up. (Reverse pagination)
The insertRowsAtIndexPaths:withRowAnimation: AND the changes to your data model both need to occur in-between beginUpdates and endUpates
I've created a simple example that should work on its own. I spent a week fiddling around trying to figure this out since I couldn't find any simple examples, so I hope this saves someone time and headache!
#interface MyTableViewController ()
#property (nonatomic, strong) NSMutableArray *expandableArray;
#property (nonatomic, strong) NSMutableArray *indexPaths;
#property (nonatomic, strong) UITableView *myTableView;
#end
#implementation MyTableViewController
- (void)viewDidLoad
{
[self setupArray];
}
- (void)setupArray
{
self.expandableArray = #[#"One", #"Two", #"Three", #"Four", #"Five"].mutableCopy;
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.expandableArray.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
//here you should create a cell that displays information from self.expandableArray, and return it
}
//call this method if your button/cell/whatever is tapped
- (void)didTapTriggerToChangeTableView
{
if (/*some condition occurs that makes you want to expand the tableView*/) {
[self expandArray]
}else if (/*some other condition occurs that makes you want to retract the tableView*/){
[self retractArray]
}
}
//this example adds 1 item
- (void)expandArray
{
//create an array of indexPaths
self.indexPaths = [[NSMutableArray alloc] init];
for (int i = theFirstIndexWhereYouWantToInsertYourAdditionalCells; i < theTotalNumberOfAdditionalCellsToInsert + theFirstIndexWhereYouWantToInsertYourAdditionalCells; i++) {
[self.indexPaths addObject:[NSIndexPath indexPathForRow:i inSection:0]];
}
//modify your array AND call insertRowsAtIndexPaths:withRowAnimation: INBETWEEN beginUpdates and endUpdates
[self.myTableView beginUpdates];
//HERE IS WHERE YOU NEED TO ALTER self.expandableArray to have the additional/new data values, eg:
[self.expandableArray addObject:#"Six"];
[self.myTableView insertRowsAtIndexPaths:self.indexPaths withRowAnimation:(UITableViewRowAnimationFade)]; //or a rowAnimation of your choice
[self.myTableView endUpdates];
}
//this example removes all but the first 3 items
- (void)retractArray
{
NSRange range;
range.location = 3;
range.length = self.expandableArray.count - 3;
//modify your array AND call insertRowsAtIndexPaths:withRowAnimation: INBETWEEN beginUpdates and endUpdates
[self.myTableView beginUpdates];
[self.expandableArray removeObjectsInRange:range];
[self.myTableView deleteRowsAtIndexPaths:self.indexPaths withRowAnimation:UITableViewRowAnimationFade]; //or a rowAnimation of your choice
[self.myTableView endUpdates];
}
#end
Free code, don't knock it.
I am not sure of this, but just try this may be it will solve your problem, this may be the problem that you are adding data after calling begin updates. So just update the data source before begin updates.

Resources