I've got an NSFetchedResultsController as my data source and and I implement NSFetchedResultsControllerDelegate in my custom UITableViewController. I'm using sectionNameKeyPath to break my result set into multiple sections.
In one of my methods, I'm adding a couple of objects to the context, all of which are in a new section. At the moment where I save the the objects, the delegate methods are called properly. The order of events:
// -controllerWillChangeContent: fires
[self.tableView beginUpdates]; // I do this
// -controller:didChangeSection:atIndex:forChangeType: fires for section insert
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex]];
// -controller:didChangeObject:atIndexPath:forChangeType:newIndexPath fires many times
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITavleViewRowAnimationFade]; // for each cell
// -controllerDidChangeContent: fires after all of the inserts
[self.tableView endUpdates]; // <--- Where things go terribly wrong!!!
On the last call, "endUpdates", the application always crashes with:
Serious application error. Exception was caught during Core Data change processing:
[NSCFArray objectAtIndex:]: index (5) beyond bounds (1) with userInfo (null)
It seems that the table updates are not in sync with the NSFetchedResultsController data in some way, and things blow up. I'm following the docs on NSFetchedResultsControllerDelegate, but it's not working. What's the right way to do it?
UPDATE: I've created a test project that exhibits this bug. You can download it at: NSBoom.zip
Tracing through the app, I note didChangeSection is called first, which inserts a whole section - and then didChangeObject is called repeatedly.
The problem is that in didChangeSection you insert a whole section, then right after before the table view is updated you are adding objects to that same section. This is basically a case of overlapping updates... (not allowed even in a begin/end updates block).
If you comment out the individual object insert, it all works:
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath]
withRowAnimation:UITableViewRowAnimationFade];
break;
If you comment out the section insert:, it doesn't work - but I have had less luck with insertRowsInSections working all the time, and it may well be because there is no section yet (which I'm sure is why you were inserting the section to begin with). You may have to detect either case to do inserts with the right granularity.
In general I've had a lot more luck reloading and inserting whole sections than rows, the table view seems very fiddly to me around those working. You can also try UITableViewRowAnimationNone which seems to operate successfully more often.
I'm having the same problem. I find that didChangeSection fires off twice. Once when you create the object for insert, and once when you actually save it. To me, it shouldn't call didChangeSection until save is called. Or at the very least, willChangeSection would be called on creation of the object, and didChangeSection called when it is saved.
Now I'm looking into the NSManagedObjectContextDidSaveNotification observer method. This isn't part of the NSFetchedResultsControllerDelegate protocol, but you can register to receive it. Perhaps that will only be called when I actually call save and not before.
Related
I am updating my UITableView with the following code:
int index = [self.guests indexOfObject:_guests];
[[self tableView] beginUpdates];
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForRow:index inSection:0]] withRowAnimation:UITableViewRowAnimationLeft];
[[self tableView] endUpdates];
But after calling [[self tableView] endUpdates], its not calling its datasource methods. In case of adding row, it calls its datasource methods. I think in case of deletion it does not need to ask anything to its datasource, but in case of adding a row it needs to ask its datasource about pretty much everything about the new row added like cellForRow, height etc etc. I just want to make sure that is it right if deleteRowsAtIndexPaths is not calling any of its datasource methods??
deleteRowsAtIndexPaths:indexPaths doesn't call delegate methods in all situation. It just simply deletes rows, If the rows being deleted are so many that they are occupying screen area, then it may call your delegate method. You have to explicitly call reloadData so that it refreshes its rows.
But calling reloadData immediately will spoil your animation or produce weird errors, since its rows are being deleted and you call reload method. (It may go crazy). The alternate solution is to call reloadData after a slight delay like 0.2 or greater like this:
[self.tableView deleteRowsAtIndexPaths:indexPaths withRowAnimation:UITableViewRowAnimationLeft];
[self.tableView performSelector:#selector(reloadData) withObject:nil afterDelay:0.2]; //calling reloadData after a short delay. 0.2 or whatever suits you.
Don't forget about deleting data from your datasource's array or whatever you are using to hold data.
The answer is a question " why should it call all the datasource and delegate methods if the view is already popuated and all cells are readily available"?
well it Should be the behavior
iOS 5 finally added a moveRowAtIndexPath:toIndexPath: method to UITableView. Problem is, calling it only moves the row, it doesn't update/redraws it. The only way to do so seems to be by finding the cell and calling setNeedsDisplay directly. But this is tricky since you never know whether the cell already got moved or not.
I can't call reloadRowsAtIndexPaths: either, since I'm not allowed to have 2 animations applied to a single row inside the same beginUpdates-endUpdates block.
There are may workarounds I can think of, like splitting the whole thing into 2 animation blocks. But I'm wondering whether I'm missing something, as the whole behavior seems odd to me.
Finally received an official answer from Apple regarding this issue:
Engineering has determined that this issue behaves as intended based
on the following information:
Only insert and update cause the cell to be requested from the data
source (the first because it's new and the second because you've
explicitly told the table view to update the cell). Delete and move
don't update the contents because that's not implicit in the action
being performed. If you need to change the contents of the cell
during the update, just reach into the cell and change the contents.
They like making it difficult for us don't they ;-)
case NSFetchedResultsChangeUpdate:
[self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
break;
case NSFetchedResultsChangeMove:
// old API
//[tableView deleteRowsAtIndexPaths:#[indexPath] withRowAnimation:UITableViewRowAnimationFade];
//[tableView insertRowsAtIndexPaths:#[newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
// new API
[self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:newIndexPath]; // note use of newIndexPath
[tableView moveRowAtIndexPath:indexPath toIndexPath:newIndexPath]; // NS_AVAILABLE_IOS(5_0);
break;
Note in the ChangeMove, configureCell has different params compared to its use in ChangeUpdate so be careful if copy pasting. Rather tricky to get that right.
I'm using core data and NSFetchedResultsController in an app, for feeding a UIViewTable with Car objects. I have a button that, when tabbed, takes the app to another view when the user can add a new Car. The problem I have is that even if the user doesn't create the car in the second view, it's added to the table. If I restart the application, the Car wasn't added to the DB.
This is related to the fact that I create an instance of the Car in the second view, in the viewDidLoad method, using something like this:
car = [NSEntityDescription insertNewObjectForEntityForName:#"Car"
inManagedObjectContext:context];
This is added even if I don't save the context.
I tried to delete the object when the second view is about to be closed, using this:
[context deleteObject:car];
This partially works. The car is not added to the table in the first page, but looks like the indexes of the data source are altered. If I scroll all the way down I got this error:
*** Terminating app due to uncaught exception 'NSRangeException', reason: '*** -[_PFBatchFaultingArray objectAtIndex:]: index (9) beyond bounds (9)'
Any ideas will be appreciated.
Try creating your car object by passing a nil context:
car = [NSEntityDescription insertNewObjectForEntityForName:#"Car"
inManagedObjectContext:nil];
From there, if the user decides to save it you can move the object to the main context.
The other alternative is to have a separate MOC for this view (which adds unnecessary complications) and then again move the object between contexts if/when required.
Cheers,
Rog
PS: if you're wondering whether you can pass nil as the managedObjectContext, this is straight from Apple's docs on NSManagedObject:
...If context is not nil, this method
invokes [context insertObject:self]
(which causes awakeFromInsert to be
invoked)...
More details here
[EDIT]
In addition to this, I just came across something interesting when looking at the NSFetchRequest documentation and thought you might want to have a go (I haven't tried myself). It appear that you can tell the fetchRequest whether to include pending changes (i.e. not saved) or not when fetching objects:
- (void)setIncludesPendingChanges:(BOOL)yesNo
FYI the default value is YES - more details here
First, try delaying the insertion of the new object when the user commits the change.
Only if it's not appropriate:
When the context has changed, NSFetchedResultsController is automatically informed to modify its data passed to the table view controller. However, the table view itself is not modified, so you have to update the table view by yourself.
That's why NSFetchedResultsController has a delegate object (conforming to NSFetchedResultsControllerDelegate protocol) which is responsible to update the table view.
For example, in CoreDataBooks sample project, you will see delegate methods of this protocol in RootViewController class. The most related delegate method is controller: didChangeObject: atIndexPath: forChangeType: newIndexPath:. You may probably want to do something like the following:
if (changeType == NSFetchedResultsChangeDelete) {
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
My app allows user to reorder rows in a table view, but I want to reloadData after moving is done to change something (add some text to the first row).
I tried to reloadData in moveRowAtIndexPath: method, but it makes the app hang because this method is called many time (according number of rows need to be moved) and the table view is reload many time.
So, I just want to know when the moving behavior is done then I reloadData in just one time. Does anyone know about this? Please help me. Thanks you so much!
- (void)doReload
{
[myTable reloadData];
}
You can do [self performSelector:selector(doReload) withObject:nil afterDelay:0.1] in the moveRowAtIndexPath method but you can't call [myTable reloadData] directly because as you found it causes a loop. Basically you need to allow the table manipulation to finish and the run loop to get around to calling your method which then causes a reload of your table. This is a bit of a hack but it works well. Ordinarily you don't need reloadData at this point but you may be trying to do something out of the ordinary.
-(int)table:(UITable*)table movedRow: (int)row toRow: (int)dest
I have a table which I am manipulating with a tableViewController (no nib, and the controller is creating the table behind the scenes)
I'm trying to delete a row from the table based on its row number; I can delete it from the array I use to create the cell in cellForRowAtIndexPath, but I get a strange error if I try to do the following, which is the same code as in tableView:commitEditingStyle:forRowAtIndexPath:
where it works fine
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:i+1 inSection:1]
[self.tableView deleteRowsAtIndexPaths:[NSArray
arrayWithObject:indexPath]
withRowAnimation:UITableViewRowAnimationFade];
It gives an error
-[_WebSafeForwarder forwardInvocation:]
and then jumps out of the method but does not crash the app
Can anyone help?
You need to do that in this block.
[self.tableView beginUpdates];
///
[self.tableView endUpdates];
do notice one thing that when the the block reaches its end
- (void) numberOfRowsInSection:(NSInteger)section
will be called again. so u need to update the number of rows in table view also.
Hope this helps.
Thanks,
Madhup
Well you are missing a semi-colon on your first line.
When in doubt, clean up your syntax...
As a point of observation, most of the programmers I have worked with and talked to really hate UITableViewController. It really adds nothing to the functionality for the user and only obfuscates things that developers might really like to control... such as the position of the table via a XIB.
It's just a convenience class and in my experience, causes more issues than it prevents.