Here is my first post on stackoverflow. Any of your help will be greatly appreciated!
I am totally new to ios development. As a practice, I am trying to develop a simple app that keeps track of diffrent items in my lab. Each item has a to-one relationship to its location and each location has a to-many relationship to different items.
I have a UITableViewController(LocationListTableViewController) that uses NSFectchedResultController as its data source for the tableview. The tableview controller is also conformed to NSFetchedResultController Delegate to keep all the NSManagedObject updated.
Once a location on the LocationListTableViewController is tapped, a DetailedTableViewController will be pushed and shows the items at that location. This DetailedTableViewController has an NSUndoManager instance variable to support canceling of changes. I use a NSMutableArray as the data source of the DetailedTableViewController:
NSSet *items = self.location.items;
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"itemName" ascending:YES];
NSArray *sortDescriptors = [NSArray arrayWithObject:sortDescriptor];
NSArray *array = [[items allObjects]sortedArrayUsingDescriptors:sortDescriptors];
// itemList is the data source for the table view
self.itemList = [array mutableCopy];
When the "Edit" button is tapped, Users can delete items and those item's location is set to nil. The deleted Items are transferred to a "changedItem" NSMutableArray so that the change can be canceled:
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
if (!changedItems)
changedItems = [[NSMutableArray alloc] init];
// Set the company attribute of selected company to nil
Item *editingItem = [itemList objectAtIndex:indexPath.row];
editingItem.company = nil;
// Add to changedItem array to support canceling
[changedItems addObject:editingItem];
// Remove from the data source array
[itemList removeObject:editingItem];
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
I put a Cancel button as the left bar button item, and when it's tapped:
[undoManager endUndoGrouping];
NSManagedObjectContext *moc = self.location.managedObjectContext;
[moc rollback];
// Add item from changedItem array back to itemList array (data source array)
if ([changedItems count] > 0) {
[itemList addObjectsFromArray:changedItems];
[itemList sortUsingDescriptors:[NSArray arrayWithObject:sortDescriptor]];
}
Here comes the problem. When I delete more than one item and try to cancel these change, the app does not crash but I get the error message:
*** Assertion failure in -[_UITableViewUpdateSupport _setupAnimationsForNewlyInsertedCells], /SourceCache/UIKit_Sim/UIKit-1448.89/UITableViewSupport.m:599
Serious application error. An exception was caught from the delegate of NSFetchedResultsController during a call to -controllerDidChangeContent:. Attempt to create two animations for cell with userInfo (null)
When only deleting one item, the app seems fine.
This is very anoying. Is there anybody could help me with this?
Many thanks!
The cell probably doesn't exist - try NSLogging its value or making sure it's created properly.
Just have a try:
[tableView beginUpdates];
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[tableView endUpdates];
Related
Update for code: Following on Matthew's answer I tried correcting my code to be more correct. Now the code does delete the cell but also crashes and gives an error:
* Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '* -[__NSPlaceholderArray initWithObjects:count:]: attempt to insert nil object from objects[0]'
The code below is from an action called checkboxTapped which is in my CustomCell code. Once action is fired it gives the error. I figured out that my indexPath is equal to NULL, and thats most likely the issue. But I don't know how to fix it.
[self.textLabel setTextColor:[UIColor grayColor]];
[self.detailTextLabel setTextColor:[UIColor grayColor]];
parent = [[ViewController alloc] init];
db = [[DataObject alloc] init];
NSIndexPath *indexPath = [[parent tableView] indexPathForSelectedRow];
[[parent array] removeObjectAtIndex:[indexPath row]];
[db deleteTaskAtIndex:[indexPath row]];
[[parent tableView] deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
[db release];
[parent release];
Old: I looked through my code and I printed my array I was using and it appears to be fine, yet this error still persists.
* Terminating app due to uncaught exception 'NSRangeException', reason: '* -[__NSArrayM removeObjectAtIndex:]: index 1 beyond bounds [0 .. 0]'
My guess was that it had something to do with my indexPath but it doesn't make much different how much I change it.
-(void)checkboxTapped:(id)sender
{
[sender setSelected:YES];
[self.textLabel setTextColor:[UIColor grayColor]];
[self.detailTextLabel setTextColor:[UIColor grayColor]];
parent = [[ViewController alloc] init];
UITableView *tableView = parent.tableView;
NSMutableArray *array = [[NSMutableArray alloc] initWithArray:parent.array];
[parent release];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow:[array count] inSection:1];
[array removeObjectAtIndex:[indexPath row]];
[db deleteTaskAtIndex:[indexPath row]];
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationTop];
[array release];
[tableView endUpdates];
[tableView reloadData];
}
In your code [indexPath row] is going to return the value of [array count]. That's unlikely to be what you want. If your array has zero objects in it, you are going to attempt to remove the object at index 0. But there will be no objects and you'll get an error. If your array has 1 object in it, you're going to attempt to remove the object at index 1. Again, that will fail, because there is no object at index 1, just one object at index 0.
If you want to remove the last object in an array you need to use an index that is count-1. You may also need to check to see if the array is empty, if that case can occur.
Updated in response to follow up in comment
You don't want to do anything indexPathWithIndex. As a first step, try modifying your code along the following lines:
-(void)checkboxTapped:(id)sender
{
[sender setSelected:YES];
[self.textLabel setTextColor:[UIColor grayColor]];
[self.detailTextLabel setTextColor:[UIColor grayColor]];
parent = [[ViewController alloc] init]; // looks very odd - is an instance of this viewController active when the checkBox is tapped? If so, you don't want to create a new one, you want to access the existing one
UITableView *tableView = parent.tableView;
[parent release]; // this looks very dicey - when you release the parent, won't it release the tableView too?!
int lastRow = [array count] - 1;
if (lastRow == 0)
{
return; // bail if there are no rows in the table
}
NSMutableArray *array = [[NSMutableArray alloc] initWithArray:parent.array];
[array removeObjectAtIndex: lastRow]; // not clear this will do anything as the reference to array is discarded later
[db deleteTaskAtIndex: lastRow];
NSIndexPath *indexPath = [NSIndexPath indexPathForRow: lastRow inSection:1];
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationTop];
[array release];
// [tableView endUpdates]; // there's no matching beginUpdates and you're only do one change operation anyway - leave this out
// [tableView reloadData]; // if you leave this line in, you won't see the delete animation - if you just want to delete one row, you wouldn't normally use reloadData, at least not if you want the animation
}
All this said, it looks as if there are other things going on here.
What's happening with array? You create this, remove an item from it and the discard the pointer to it. Is that what you really want to do. A more common pattern would be to get the pointer to the array from the other object and remove the item at the end of it here.
It's not clear from your code how you are updating the table's data source. When using deleteRowsAtIndexPaths:withRownAnimation you need to make sure the table's data source will return one row less than it did last time it was asked with tableView:numberOfRowsInSection:. From your code it's not clear how the tableView dataSource is going to know there's one less item, unless, perhaps, it's looking at whatever it is that db is pointing to in order to find this out.
More fundamentally, with a typical design pattern the tableView is going to be released when you release the parent view, so whatever it points to after `[parent release]' is going to do something undefined and is likely to crash at least some of the time.
After days of research and re-coding I am pretty much stumped. My goal is to get a test app running with a single tableview populated from two separate fetchedResultControllers.
I have a series of items on a shopping list, each with a department and a boolean 'collected' flag. Uncollected items should be listed by department, followed by a single section containing all collected items (regardless of department). As a user checks off uncollected items, they should move down to the 'collected' section. If s/he un-checks a collected item, it should move back into its correct department.
To achieve the first part (uncollected items), I set up a simple fetchedResultsController that fetches all items where collected = NO, and sectioned the results by department:
- (NSFetchedResultsController *)firstFRC {
// Set up the fetched results controller if needed.
if (firstFRC == nil) {
// Create the fetch request for the entity.
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// fetch items
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Item" inManagedObjectContext:managedObjectContext];
[fetchRequest setEntity:entity];
// only if uncollected
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"(collected = NO)"];
[fetchRequest setPredicate:predicate];
// sort by name
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
// fetch results, sectioned by department
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:managedObjectContext sectionNameKeyPath:#"department" cacheName:nil];
aFetchedResultsController.delegate = self;
self.firstFRC = aFetchedResultsController;
}
return firstFRC;
}
I set the number of rows, sections, and section headers as follows:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
// Return the number of sections.
return firstFRC.sections.count;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
// Return the number of rows in the section.
return [[firstFRC.sections objectAtIndex:section] numberOfObjects];
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
return [[[firstFRC sections] objectAtIndex:section] name];
}
I also use the boilerplate controllerWillChangeContent, didChangeObject, and controllerDidChangeContent to add/remove cells as the FRC's change.
For brevity, I won't include the code for displaying the cell, but I essentially pull the correct item based on the cell's index path, set the text/subtitle of the cell, and attach one of two checkmark images, depending on whether the item is collected or not. I also wire up this image (which is on a button) to toggle from checked to unchecked when it is touched, and update the item accordingly.
This part all works fine. I can view my list of items by department, and when I mark one as collected, I see it drop off the list as expected.
Now I attempted to add the bottom section, containing all of the collected items (in a single section). First I set up a second fetchedResultsConroller, this time to fetch only uncollected items, and without sectioning. I also had to update the following:
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
// Return the number of sections - add one for 'collected' section
return firstFRC.sections.count + 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
// Return the number of rows in the section
if (section < firstFRC.sections.count) {
return [[firstFRC.sections objectAtIndex:section] numberOfObjects];
}
else {
return secondFRC.fetchedObjects.count;
}
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
if (section < firstFRC.sections.count) {
return [[[firstFRC sections] objectAtIndex:section] name];
}
else{
return #"Collected";
}
}
I then updated the cellForRowAtIndexPath in a similar fashion, so that the item I retrieve comes from the right FRC:
Item *item;
if (indexPath.section < firstFRC.sections.count) {
item = [firstFRC objectAtIndexPath:indexPath];
}
else {
item = [secondFRC objectAtIndexPath:[NSIndexPath indexPathForRow:indexPath.row inSection:0]];
}
[[cell textLabel] setText:[item name]];
…rest of cell configuration
This works great when I launch. The tableview displays exactly as anticipated:
The Problem (finally)
The (first) problem comes when I select the checkmark for an uncollected item. I expect the item to be removed from the department it is listed under, and moved to the 'collected' section. Instead, I get:
CoreData: error: Serious application error. An exception was caught
from the delegate of NSFetchedResultsController during a call to
-controllerDidChangeContent:. Invalid update: 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 (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). with userInfo (null)
If I attempt the opposite, then I receive a different error:
* Terminating app due to uncaught exception 'NSRangeException', reason: '* -[__NSArrayM objectAtIndex:]: index 2 beyond bounds [0 ..
1]'
I suspect that in both cases there is a problem with consistency with the number of sections/rows in the FRCs and the tableview when an item moves from one FRC to the other. Although that second error makes me think there is maybe a simpler problem related to my retrieval of items.
Any direction or ideas would be appreciated. I can provide more of my code if it would help, and have also created a small test app with a single view to demonstrate the issue. I can upload it if necessary, but mostly I wanted to test the issue in a small scale sandbox.
Update - additional code requested
As requested, this is what happens when a checkmark is touched:
- (void)checkButtonTapped:(id)sender event:(id)event
{
NSSet *touches = [event allTouches];
UITouch *touch = [touches anyObject];
CGPoint currentTouchPosition = [touch locationInView:self.tableView];
NSIndexPath *indexPath = [self.tableView indexPathForRowAtPoint: currentTouchPosition];
if (indexPath != nil)
{
[self tableView: self.tableView accessoryButtonTappedForRowWithIndexPath: indexPath];
}
}
- (void)tableView:(UITableView *)tableView accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)indexPath
{
Item *item;
if (indexPath.section < firstFRC.sections.count) {
item = [firstFRC objectAtIndexPath:indexPath];
}
else {
item = [secondFRC objectAtIndexPath:[NSIndexPath indexPathForRow:indexPath.row inSection:0]];
}
UITableViewCell *cell = [tableView cellForRowAtIndexPath:indexPath];
UIButton *button = (UIButton *)cell.accessoryView;
if (![item collected]) {
[item setCollected:YES];
[button setBackgroundImage:[UIImage imageNamed:#"checked.png"] forState:UIControlStateNormal];
}
else if ([item collected]){
[item setCollected:NO];
[button setBackgroundImage:[UIImage imageNamed:#"unchecked.png"] forState:UIControlStateNormal];
}
NSError *error = nil;
if (![item.managedObjectContext save:&error]) {
NSLog(#"Error saving collected items");
}
}
Well, I think I might have cracked it. As is often the case, stripping it down and reading just a few comments pointed me in the right direction.
When I get to the delegate method controllerDidChangeObject, I attempt to insert a row at the indexPath provided (for the 'checked' item). Except that when inserting into my additional section, this indexPath has no awareness of the fact that there are a bunch of other sections before it. So it receives section 0 and attempts to insert there. Instead, if the indexPath comes from the second FRC, I should be incrementing the section number by the number of sections in the first FRC's table. So, I replaced:
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
UITableView *tableView = self.tableView;
switch(type) {
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
with
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
UITableView *tableView = self.tableView;
switch(type) {
case NSFetchedResultsChangeInsert:
if ([controller.sectionNameKeyPath isEqualToString:#"department"]) {
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
}
else {
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:[NSIndexPath indexPathForRow:newIndexPath.row inSection:firstFRC.sections.count]] withRowAnimation:UITableViewRowAnimationFade]; }
break;
I will need to do this for each of insert, delete, update etc. I will mark this as the answer after I have validated this and to allow time for other comments.
The errors you are seeing mean that your UITableView reloads itself before BOTH your NSFetchedResultsControllers do. The codes you posted are probably correct. I suspect that the problem is in one of the NSFetchedResultsControllerDelegate methods.
I know this is going to be one of those head-smacker moments.
I have a UITableView from which I need to delete cells. Problem is, didSelectRowAtIndexPath is only called if I first tap the row and then swipe to show the delete button. If I swipe the row without explicitly tapping the row, then I get an EXC_BAD_ACCESS error. I put an NSLog into didSelectRowAtIndexPath and can see the indexPath array when I tap the cell, but nothing if I just swipe without clicking.
I've done my share of searching and I'm 99% sure my delegate is hooked up correctly (usually problem #1) and didn't type didDeselectRowAtIndexPath (usually problem #2). I also have in my .h file (problem #3).
Thanks in advance.
EDIT:
I should have said: If I swipe the row without explicitly tapping the row, then I get an EXC_BAD_ACCESS error when I tap the Delete button. If I tap the cell and then swipe, I can tap Delete and it deletes as expected.
EDIT:
(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath
{
static NSString *CellIdentifier = #"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier] autorelease];
}
NSSortDescriptor *aSortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:YES];
[bandList sortUsingDescriptors:[NSArray arrayWithObject:aSortDescriptor]];
NSDictionary *dict = [bandList objectAtIndex:indexPath.row];
cell.textLabel.text = [dict objectForKey:#"name"];
return cell;
}
- (void)tableView: (UITableView *)tableView didSelectRowAtIndexPath: (NSIndexPath *)indexPath
{
NSLog(#"IndexPath: %#", [indexPath description]);
}
-(void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
if (editingStyle == UITableViewCellEditingStyleDelete)
{
NSDictionary *selectedBand = [bandList objectAtIndex:indexPath.row];
NSString *selectedBandId = [selectedBand objectForKey:#"id"];
[self deleteMyBand:selectedBandId];
bandList = [self getSavedBands];
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
}
}
These lines jump out at me:
NSDictionary *selectedBand = [bandList objectAtIndex:indexPath.row];
NSString *selectedBandId = [selectedBand objectForKey:#"id"];
I don't understand your reasoning for taking a value from an array, sticking it into a dictionary, then getting a string out of that dictionary. Shouldn't the value returned from
[bandList objectAtIndex:indexPath.row];
Be the same as what you get in selectedBandId?
Also, I can't analyze it any further when I don't know what "deleteMyBand" and "getSavedBands" do. Try commenting out those calls and seeing if the visible row itself can be deleted. That may tell you where the problem is.
Not exactly sure why, but the fact that I was using a sorted NSMutableArray for my source data seems to have been the problem. When I was deleting the row from the table and updating the MutableArray, I got the crash. However, if I created my source data as an NSArray, made a mutableCopy of the array into a temporary MutableArray, removed the row, resorted the array, and then wrote the MutableArray back out to an NSArray, everything is peachy.
I don't think this explanation makes sense, but it's the pattern I'm seeing. If anybody can think of why this might be working, please enlighten me. My gut tels me it was the sort, but my gut hasn't been correct much lately.
First off, thank you very much if you find time to help me!
I have been learning about NSFetchedResultsControllerand im trying to understand it as much as i can, however, some things about it very much confuse me. This code if from a tutorial i've found online. Basically i have the simplest fetched controller set up with really fancy methods like this
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller {
// The fetch controller is about to start sending change notifications, so prepare the table view for updates.
[self.tableView beginUpdates];
}
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
UITableView *tableView = self.tableView;
switch(type) {
case NSFetchedResultsChangeInsert:
[tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeUpdate:
[self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
break;
case NSFetchedResultsChangeMove:
[tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
// Reloading the section inserts a new row and ensures that titles are updated appropriately.
[tableView reloadSections:[NSIndexSet indexSetWithIndex:newIndexPath.section] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
(NSFetchedResultsController *)fetchedResultsController {
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription
entityForName:#"FailedBankInfo" inManagedObjectContext:_context];
[fetchRequest setEntity:entity];
NSSortDescriptor *sort = [[NSSortDescriptor alloc]
initWithKey:#"details.closeDate" ascending:NO];
[fetchRequest setSortDescriptors:[NSArray arrayWithObject:sort]];
[fetchRequest setFetchBatchSize:20];
NSFetchedResultsController *theFetchedResultsController =
[[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:_context sectionNameKeyPath:nil
cacheName:#"Root"];
self.fetchedResultsController = theFetchedResultsController;
_fetchedResultsController.delegate = self;
[sort release];
[fetchRequest release];
[theFetchedResultsController release];
return _fetchedResultsController;
}
Do i need to create a new fetched controller everytime for different view controllers? For instance if i have a list of businesses in one list view. then i click on one of them and brings me to a list of employees in that ocmpany, i have to use a different fetchedViewController since i need to respecify the entity key right? See above my implementation.
With my current implementation, it works great for listing items. Let's say I have a view controller called VC. init i fully implemented NSFetchedResultsController. I have a barbutton add item and a method called addButtonPressed which when pressed modally adds a view (from bottom up). In this same method i have
Entity myEntity = [NSEntityDescription insertNNewObjectForEntityForName:#"Entity" inManagedObjectContext:context_]; Then i tell the code the go into another view. HOWEVER, when the animation is moving the new navigation controller up, the new cell already shows itself mid animation. From my understanding this these two methods
- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath AND - (UITableViewCell *)tableView:(UITableView *)tableView get called in conjuction with the top two methods in the code snippet above. how can i fix it? i dont want anything showing up on the current view until i added a new Entity by going to another view. The second another object gets called into the context, the update methods must get called or something
What does this snippet of code do? Particularly the part with id < stuff >
(void)controller:(NSFetchedResultsController *)controller didChangeSection:(id NSFetchedResultsSectionInfo)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type {
switch(type) {
case NSFetchedResultsChangeInsert:
[self.tableView insertSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
case NSFetchedResultsChangeDelete:
[self.tableView deleteSections:[NSIndexSet indexSetWithIndex:sectionIndex] withRowAnimation:UITableViewRowAnimationFade];
break;
}
}
Edit: Holy $#$, formatting this frustrating. there should be didChangeSection:(id (LESS THAN EQUAL SIGN) NSFetchedResultsSectionInfo (GREATER THEN EQUAL SIGN)) in the code above
4 Where can i learn more about this. i.e what the individual content updating methods do and when they are called etc. Also Does the NSFetchedResultcontroller act as a single array? Meaning that if i want to have a tableview with multiple sections i need more ViewControllers? Once again THANKS!
I second Brad's comment, but I'll give you some pointers:
Basically yes, you need a new NSFetchedResultsController for every entity you want to query for. However, you don't need to use and NSFetchedResultsController at all, this is just a nice class to let you do fancy things with your tableView as you are doing in question #2.
Your problem here is that you are calling [NSEntityDescription insertNNewObjectForEntityForName:#"Entity" inManagedObjectContext:context_]; from your tableViewController and as soon as you call that your NSFetechedResultsController will get a message stating that a row was inserted, triggering the NSFetchedResultsChangeInsert path in your controller:didChangeObject:atIndexPath:forChangeType:newIndexPath: method. NSFetchedResultsController is an object that watches the data that is fetches from CoreData for any changes and then gives you the ability to do stuff as the changes happen. To fix your display issue, just move the insertNewObjectForEntityName call into your next viewController, and magically when you dismiss that viewController you'll have a new row.
The didChangeSection: delegate method of NSFetchedResultsController is informing you if a section has been added or deleted from your dataset. See my answer to #4 and this should be clear.
NSFetechedResultsController allows you to "section" the data it fetches using the sectionNameKeyPath: part of the init call. By supplying the name of a data point in your entity you can group your data into sections. So going back to #3 & #2 if you happen to insert a new entity that has a new value in the "section" keypath that you specified when initing the NSFetchedResultsController you'd get the didChangeSection: delegate called.
I recommend reading Apples documentation on NSFetchedResultsController and CoreData and probably their TableView Programming Guide. Don't forget to download and play with the sample code, it's easier to understand in most cases than the theories in the documentation.
I know this question has been asked before, and I took a look at the answer to this question. However, I'm still confused as to how to implement reordering with a UITableView in a Core Data project. What I know is that I need to have a "displayOrder" attribute in my Entity to track the order of items, and I need to enumerate through all the objects in the fetched results and set their displayOrder attribute.
In the given code in the question I linked to, the table view delegate method calls a method like this [self FF_fetchResults];, and the code for that method is not given so its hard to tell what exactly it is.
Is there any sample code that demonstrates this? That would be simpler to look at than sharing large chunks of code.
Thanks
I'm not sure which part you are having trouble with (based on the comments)... but here is my suggestion. The displayOrder is just a simple attribute on a NSManagedObject class. If you can save a managed object, then you will be able finish this feature. Lets first take a simple NSManagedObject:
#interface RowObj : NSManagedObject
{
}
#property (nonatomic, retain) NSString *rowDescription;
#property (nonatomic, retain) NSNumber *displayOrder;
Next, we need to have local copy of the data being displayed in the tableview. I have read through the comments you have made and I'm not really sure if you are using the FetchedResultsController or not. My suggestion would be to start simple and just use a normal tableviewcontroller where you update the row data whenever a user changes the display order... then save the order when the user is done editing.
The interface for this tableviewcontoller would look like this:
#interface MyTableViewController : UITableViewController {
NSMutableArray *myTableViewData;
}
#property(nonatomic,retain) NSMutableArray *myTableViewData;
#end
Next, we need to load the the table view data in the viewWillAppear method:
- (void)viewWillAppear:(BOOL)animated {
myTableViewData = [helper getRowObjects]; // insert method call here to get the data
self.navigationItem.leftBarButtonItem = [self editButtonItem];
}
There are 2 things going on here... (I'll explain the editButtonItem later) the first is that we need to get our data from CoreData. When I have to do this I have some sort of helper(call it what you want) object do the work. A typical find method would look like this:
- (NSMutableArray*) getRowObjects{
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"RowObj" inManagedObjectContext:[self managedObjectContext]];
[request setEntity:entity];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"displayOrder" ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
[request setSortDescriptors:sortDescriptors];
[sortDescriptors release];
[sortDescriptor release];
NSError *error;
NSMutableArray *mutableFetchResults = [[managedObjectContext executeFetchRequest:request error:&error] mutableCopy];
if (mutableFetchResults == nil) {
// Handle the error.
}
[request release];
return mutableFetchResults;
}
Now that you have your data, you can now wait for the user to edit the table. That is where the [self editButtonItem] comes into play. This is a built in feature that returns a bar button item that toggles its title and associated state between Edit and Done. When the user hits that button, it will invoke the setEditing:animated: method:
To update the display order you need to override the setEditing method on the UITableViewController class. It should look something like this:
- (void)setEditing:(BOOL)editing animated:(BOOL)animated {
[super setEditing:editing animated:animated];
[myTableView setEditing:editing animated:animated];
if(!editing) {
int i = 0;
for(RowObj *row in myTableViewData) {
row.displayOrder = [NSNumber numberWithInt:i++];
}
[helper saveManagedObjectContext]; // basically calls [managedObjectContext save:&error];
}
}
We don't have to do anything when the user is in edit mode... we only want to save once they have pressed the "Done" button. When a user drags a row in your table you can update your display order by overriding the canMoveRowAtIndexPath and moveRowAtIndexPath methods:
- (BOOL)tableView:(UITableView *)tableView canMoveRowAtIndexPath:(NSIndexPath *)indexPath {
return true;
}
(void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath {
RowObj *row = [myTableViewData objectAtIndex:sourceIndexPath.row];
[myTableViewData removeObjectAtIndex:sourceIndexPath.row];
[myTableViewData insertObject:row atIndex:destinationIndexPath.row];
}
Again, the reason I don't update the displayOrder value here is because the user is still in edit mode... we don't know if the user is done editing AND they could even cancel what they've done by not hitting the "Done" button.
EDIT
If you want to delete a row you need to override tableView:commitEditingStyle:forRowAtIndexPath and do something like this:
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {
if (editingStyle == UITableViewCellEditingStyleDelete) {
// Delete the managed object at the given index path.
RowObj *row = [myTableViewData objectAtIndex:indexPath.row];
[helper deleteRow:row];
// Update the array and table view.
[myTableViewData removeObjectAtIndex:indexPath.row];
[myTableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:YES];
}
}