I have an application with a UITableView. The application communicates with a server.
Problem is the following:
Client 1 deletes a cell of the TableView. The data update is transmitted to the server which sends a data update to Client 2. In the same moment Client 2 deletes a cell from the TableView. Due to the receiving update an NSInternalInconsistencyException is thrown because the number of cells before and after are not as expected (difference is 2 not 1).
The app crashes in
[tableView endUpdates];
Of course I can catch the exception with
- (void) tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
#try{
// Send data update to the server
[sender sendDataToServer:data];
// Update tableview
[tableView beginUpdates];
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObjects:indexPath, nil] withRowAnimation:UITableViewRowAnimationFade];
[tableView endUpdates];
}
#catch (NSException * e) {
// Hide red delete-cell-button
tableView.editing = NO;
// Jump to previous ViewController
[self.navigationController popViewControllerAnimated:YES];
}
}
}
But after the successful catch I get an
EXC_BAD_ACCESS exception in [_UITableViewUpdateSupport dealloc].
What can I do to prevent the app from crashing? What do I have to do in the catch-block to "clean" the situation? Reloading the tableview doesn't take any effect.
EDIT: numberOfRows-method looks like this:
- (NSInteger) tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [dataSourceArray count];
}
#synchronized(lock) solved my problem.
I just had to make sure that only one thread is able to manipulate the data source array at the same time. Now everything works fine, no more 'NSInternalInconsistencyException'.
Thx to trojanfoe and luk2302.
Related
Does the model change and the row animation have to be started directly from commitEditingStyle or can it be done in a later run loop iteration?
I am asking because it appears to work on iOS 10, but on iOS 11 it breaks at least the delete animation. Is it simply a bug in iOS 11 or is it a bad idea in general?
Is there a better way to trigger an asynchronous delete operation and animate the table view change on completion?
The first picture shows how it breaks on iOS 11 (The delete button overlaps the next cell). The second picture shows how it looks fine on iOS 10.
This is the interesting snipped:
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
dispatch_async(dispatch_get_main_queue(), ^{
[_model removeObjectAtIndex:indexPath.row];
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
});
}
}
If I remove the dispatch_async(..., it works as expected on iOS 10 and 11. The first picture shows iOS 11, the second iOS 10.
Here is the full code of the table view controller used to test it:
#import "TableViewController.h"
#implementation TableViewController {
NSMutableArray<NSString *>* _model;
}
- (void)viewDidLoad {
[super viewDidLoad];
_model = [[NSMutableArray alloc] init];
[self.tableView registerClass:UITableViewCell.class forCellReuseIdentifier:#"cell"];
for (NSInteger i = 0; i < 200; i++) {
[_model addObject:[NSString stringWithFormat:#"Test Row %ld Test Test Test Test Test", (long)i]];
}
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return _model.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:#"cell" forIndexPath:indexPath];
cell.textLabel.text = _model[indexPath.row];
return cell;
}
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
return YES;
}
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
dispatch_async(dispatch_get_main_queue(), ^{
[_model removeObjectAtIndex:indexPath.row];
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
});
}
}
#end
Update:
Adding this method to the table view controller fixes it for iOS 11 and allows delaying the model change and row animation. (Thanks to ChrisHaze)
- (UISwipeActionsConfiguration *)tableView:(UITableView *)tableView trailingSwipeActionsConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath NS_AVAILABLE_IOS(11_0) {
UIContextualAction* deleteAction = [UIContextualAction contextualActionWithStyle:UIContextualActionStyleDestructive title:#"Delete" handler:^(UIContextualAction * _Nonnull action, __kindof UIView * _Nonnull sourceView, void (^ _Nonnull completionHandler)(BOOL)) {
dispatch_async(dispatch_get_main_queue(), ^{
[_model removeObjectAtIndex:indexPath.row];
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
completionHandler(YES);
});
}];
UISwipeActionsConfiguration* config = [UISwipeActionsConfiguration configurationWithActions:#[deleteAction]];
return config;
}
It seems that there have been changes made to the UITableview in iOS 11.
Apple's Build release notes :
"The behavior of the delete swipe action has been changed.When implementing commitEditingStyle: to delete a swiped row, delete the row in the data source and call deleteRowsAtIndexPaths: on the table view to show the swipe delete animation."
After further research, I found that you must call the beginUpdates prior to calling the deleteRowAtIndexPath: method, along with the endUpdates method after.
According to Apple's documentation, not calling these tableView methods will result in potential data model issues and effect the deletion animations.
With that said, there is an answer that includes the required code to a question on the Apple dev forums that addresses a very similar question.
Answering your actual questions:
Model changes need to be called prior to the beginUpdates and endUpdates block, in which the UI changes will occur.
Updating the UITableView will alleviate the synchronization/animation issues.
---------------------------- additional details ----------------------------
After looking into the details within your comment, I've included a link (above) that is the latest Table View Programming Guide provided by Apple. It will take you, or anyone else with this issue, to the section of the guide that includes the details you've added to your question.
There is also a handy note that includes how to make the deleteRowAtIndexPath call from within the commitEditingStyle method if one must.
When an user deletes some table row with swipe-to-delete action,
Instruments Tool shows that the deleted UITableViewCell instance is still alive.
I used very ordinary approach that is:
-(UITableViewCellEditingStyle) tableView:(UITableView *)tableView editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath
{
return UITableViewCellEditingStyleDelete;
}
-(void) tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
if(editingStyle == UITableViewCellEditingStyleDelete){
// Do Some Processing Model things...
[self.tableView beginUpdates];
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation: UITableViewRowAnimationAutomatic];
[self.tableView endUpdates];
}
}
I can't sure but I think It is bug that is related with ARC.
May I leave this problem or should I have to find any walk around?
TableView holds on to the cell so it can reuse them later.
I prepared a TableView containing data from NSMutableArray. There is an option to edit and reorder rows. All works fine, until the phone is shut down or too many applications are running in the background - there is an error message in Xcode that the application exited unexpectedly due to memory pressure.
I would like to add some command to remain and remember previous cell order so after iPhone shut down user will have the same order as before.
Below is the code I am using for enabling reorder. Is there anything missing? Please bear with me, these are just my initial developing attempts.
Thanks in advance for any help!
Matus
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath
{
return YES;
}
- (UITableViewCellEditingStyle)tableView:(UITableView *)tableView editingStyleForRowAtIndexPath: (NSIndexPath *)indexPath {
return UITableViewCellEditingStyleDelete;
}
- (void)tableView:(UITableView *)tableView commitEditingStyle: (UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
if (editingStyle == UITableViewCellEditingStyleDelete) {
[_Title removeObjectAtIndex:indexPath.row];
[_Images removeObjectAtIndex:indexPath.row];
[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
}
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath
{
NSString *item = [self.Title objectAtIndex:fromIndexPath.row];
[self.Title removeObject:item];
[self.Title insertObject:item atIndex:toIndexPath.row];
NSString *image = [self.Images objectAtIndex:fromIndexPath.row];
[self.Images removeObject:image];
[self.Images insertObject:image atIndex:toIndexPath.row];
}
- (void)tableView:(UITableView *)tableView didEndReorderingRowAtIndexPath:(NSIndexPath *)indexPath
{
[self.tableView reloadSections:[NSIndexSet indexSetWithIndex:indexPath.section]
withRowAnimation:UITableViewRowAnimationNone];
}
- (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath
{
return YES;
}
I really wish I could comment at this point... So, I already apologize for writing this as an answer.
You really shouldn't discard the fact that your app is being shut down by the OS. This is the main issue. Not the persistence of the data.
After you fix that, you can then look into how to persist your data (using Core Data, or a simple plist file) when leaving the app (using either one of the method in the UIApplicationDelegate protocol, or one of the application notifications). And loading it back when reopening it.
You need to make sure that you're persisting the changes to self.Title and self.Images.
If you add a call to the code you use to save the order of your rows at the end of tableView:MoveRowAtIndexPath:toIndexPath: then any change in the order of the rows should always be saved.
You can test that this is working by stopping the app (either by Cmd+. or the stop sign) immediately after you move a row.
I try to manage to delete a row from an UITable which is part on an UIViewController. I use the Edit button in the navigation bar. Hitting it will put the table rows in edit mode. But when a delete button in a row is pressed I get an error ...'Invalid update: invalid number of rows in section 0…. when using the following:
- (void)setEditing:(BOOL)editing animated:(BOOL)animated {
[super setEditing:editing animated:animated];
[self.tableView setEditing:editing animated:YES];
}
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
NSMutableArray *work_array = [NSMutableArray arrayWithArray:self.inputValues];
[work_array removeObjectAtIndex:indexPath.row];
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
}
What do I miss here? The Apple documentation seems to be outdated somehow.
Thanks
The problem is simple. You are not updating your data model properly before deleting the row from the table.
All you do is create some new array and delete a row from that. That's pointless. You need to update the same array used by the other data source methods such as numberOfRowsInSection:.
The issue you are having is that you are not directly updating your table's data source. You first create a completely new array called work_array based on your data source (I'm assuming it's self.inputValues) and then you remove an item from it, and then try to delete a row, but your tableView's data source still contains the item you intended to remove.
All you need to do is ensure that self.inputValues is a mutable array and directly remove the object at the index for that array, like this:
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
[self.inputValues removeObjectAtIndex:indexPath.row];
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
}
I hope that helps!
SOLVED: See my answer (and possible explanation) below.
I'm making an app that works on iOS 5.1 devices, but not on iOS 5.0 devices. Here is the trouble code that works on 5.1 but NOT on 5.0:
- (void) expandIndexPath: (NSIndexPath *) indexPath afterDelay: (BOOL) delay
{
NSIndexPath *oldSelectedIndexPath = [NSIndexPath indexPathForRow:self.mySelectedIndex inSection:0];
self.mySelectedIndex= indexPath.row;
// [self.myTableView beginUpdates];
[self.myTableView reloadRowsAtIndexPaths:[NSArray arrayWithObjects:[NSIndexPath indexPathForRow:self.mySelectedIndex inSection:0], oldSelectedIndexPath,nil] withRowAnimation:UITableViewRowAnimationNone];
// [self.myTableView endUpdates];
}
Even more curiously, it works in iOS 5.0 if I replace the reloadRowsAtIndexPaths line with [self.myTableView reloadData];. Why is this? Did 5.0 have a bug regarding the reloadRowsAtIndexPaths line? I've tried it on 5.0 with and without begin/endUpdates lines and neither works.
EDIT: To be more specific, when I run the app on iOS 5 it crashes with the following error:
* Assertion failure in -[_UITableViewUpdateSupport _computeRowUpdates], /SourceCache/UIKit/UIKit-1912.3/UITableViewSupport.m:386
[Switching to process 7171 thread 0x1c03]
Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Invalid table view
update. The application has requested an update to the table view
that is inconsistent with the state provided by the data source.'
EDIT: Here are my UITableViewDataSource methods.
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
//I am positive that this will ALWAYS return the number (it never changes)
return self.myCellControllers.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
//I cache my table view cells, thus each time this method gets called it will
// return the exact same cell. Yes, I know that most of the time I should be dequeing
// and reusing cells; just trust me that this time, it's best for me to cache them
// (There are very few cells so it doesn't really matter)
CellController *controller = [self.myCellControllers objectAtIndex:indexPath.row];
return controller.myCell;
}
- numberOfSectionsInTableView: always returns 1;
The only interesting method is
- (CGFloat)tableView:(UITableView *)aTableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
if(indexPath.row == self.mySelectedIndex)
{
return DEFAULT_CELL_HEIGHT + self.expandSize;
}
else
{
return DEFAULT_CELL_HEIGHT;
}
}
Just as a quick overview of how this part of the program works, when a user taps a cell, the cell scrolls to the top of the screen. After it is at the top of the screen it's index gets set as self.mySelectedIndex and it expands (gets taller).
I found a solution that works. If I change the expandIndexPath method to this it works:
- (void) expandIndexPath: (NSIndexPath *) indexPath afterDelay: (BOOL) delay {
[self.myTableView beginUpdates];
NSIndexPath *oldSelectedIndexPath = [NSIndexPath indexPathForRow:self.mySelectedIndex inSection:0];
self.mySelectedIndex= indexPath.row;
if(oldSelectedIndexPath.row >= 0)
{
[self.myTableView reloadRowsAtIndexPaths:[NSArray arrayWithObjects:[NSIndexPath indexPathForRow:self.mySelectedIndex inSection:0],oldSelectedIndexPath, nil] withRowAnimation:UITableViewRowAnimationNone];
}
else
{
[self.myTableView reloadRowsAtIndexPaths:[NSArray arrayWithObjects:[NSIndexPath indexPathForRow:self.mySelectedIndex inSection:0],nil] withRowAnimation:UITableViewRowAnimationNone];
}
[self.myTableView endUpdates];
}
As far as I can tell, the problem was that oldSelectedIndexPath was sometimes being set with a negative row value. Thus when I tried to reload an indexPath with a negative row, it crashed in iOS 5.0. It appears that iOS 5.1 fixes this and does more error checking.