I'm having some trouble getting my Core Data entities to play nice and order when using an UITableView.
I've been through a number of tutorials and other questions here on StackOverflow, but there doesn't seem to be a clear or elegant way to do this - I'm really hoping I'm missing something.
I have a single Core Data entity that has an int16 attribute on it called "displayOrder". I use an NSFetchRequest that has been sorted on "displayOrder" to return the data for my UITableView. Everything but reordering is being respected. Here is my (inefficient) moveRowAtIndePath method:
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath {
NSUInteger fromIndex = fromIndexPath.row;
NSUInteger toIndex = toIndexPath.row;
FFObject *affectedObject = [self.fetchedResultsController.fetchedObjects objectAtIndex:fromIndex];
affectedObject.displayOrderValue = toIndex;
[self FF_fetchResults];
for (NSUInteger i = 0; i < [self.fetchedResultsController.fetchedObjects count]; i++) {
FFObject *otherObject = [self.fetchedResultsController.fetchedObjects objectAtIndex:i];
NSLog(@"Updated %@ / %@ from %i to %i", otherObject.name, otherObject.state, otherObject.displayOrderValue, i);
otherObject.displayOrderValue = i;
}
[self FF_fetchResults];
}
Can anyone point me in the direction of a good bit of sample code, or see what I'm doing wrong? The tableview display updates OK, and I can see through my log messages that the displayOrder property is being updated. It's just not consistently saving and reloading, and something feels very "off" about this implementation (aside from the wasteful iteration of all of my FFObjects).
Thanks in advance for any advice you can lend.
I took a look at your code and this might work better:
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath {
NSUInteger fromIndex = fromIndexPath.row;
NSUInteger toIndex = toIndexPath.row;
if (fromIndex == toIndex) {
return;
}
FFObject *affectedObject = [self.fetchedResultsController.fetchedObjects objectAtIndex:fromIndex];
affectedObject.displayOrderValue = toIndex;
NSUInteger start, end;
int delta;
if (fromIndex < toIndex) {
// move was down, need to shift up
delta = -1;
start = fromIndex + 1;
end = toIndex;
} else { // fromIndex > toIndex
// move was up, need to shift down
delta = 1;
start = toIndex;
end = fromIndex - 1;
}
for (NSUInteger i = start; i <= end; i++) {
FFObject *otherObject = [self.fetchedResultsController.fetchedObjects objectAtIndex:i];
NSLog(@"Updated %@ / %@ from %i to %i", otherObject.name, otherObject.state, otherObject.displayOrderValue, otherObject.displayOrderValue + delta);
otherObject.displayOrderValue += delta;
}
[self FF_fetchResults];
}
(This is intended as as comment on gerry3's answer above, but I am not yet able to comment on other users' questions and answers.)
A small improvement for gerry3's - very elegant - solution. If I'm not mistaken, the line
otherObject.displayOrderValue += delta;
will actually perform pointer arithmetic if displayOrderValue
is not of primitive type. Which may not be what you want. Instead, to set the value of the entity, I propose:
otherObject.displayOrderValue = [NSNumber numberWithInt:[otherObject.displayOrderValue intValue] + delta];
This should update your entity property correctly and avoid any EXC_BAD_ACCESS crashes.
Here a full solution how to manage an indexed table with core data. Your attribute is called displayOrder
, I call it index
.
First of all, you better separate view controller and model. For this I use a model controller, which is the interface between the view and the model.
There are 3 cases you need to manage that the user can influence via the view controller.
The first two cases Adding and Deleting are pretty straightforward. Delete calls a routine called renewObjectIndicesUpwardsFromIndex
in order to update the indices after the deleted object.
- (void)createObjectWithTitle:(NSString*)title {
FFObject* object = [FFObject insertIntoContext:self.managedObjectContext];
object.title = title;
object.index = [NSNumber numberWithInteger:[self numberTotalObjects]];
[self saveContext];
}
- (void)deleteObject:(FFObject*)anObject {
NSInteger objectIndex = [anObject.index integerValue];
[anObject deleteObject];
[self renewObjectIndicesUpwardsFromIndex:objectIndex];
[self saveContext];
}
- (void)renewObjectIndicesUpwardsFromIndex:(NSInteger)fromIndex {
NSFetchRequest* fetchRequest = [[NSFetchRequest alloc] init];
[fetchRequest setEntity:[NSEntityDescription entityForName:@"Object" inManagedObjectContext:self.managedObjectContext]];
NSPredicate* predicate = [NSPredicate predicateWithFormat:@"(index > %d)", fromIndex];
[fetchRequest setPredicate:predicate];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"index" ascending:YES];
NSArray *sortDescriptors = [NSArray arrayWithObjects:sortDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
NSError* fetchError = nil;
NSArray* objects = [self.managedObjectContext executeFetchRequest:fetchRequest error:&fetchError];
NSInteger index = fromIndex;
for (FFObject* object in objects) {
object.index = [NSNumber numberWithInteger:index];
index += 1;
}
[self saveContext];
}
Before I come to the controller routines for the re-order, here the part in the view controller. I use a bool isModifyingOrder
similar to this answer. Notice that the view controller calls two functions in the controller moveObjectOrderUp
and moveObjectOrderDown
. Depending on how you display the objects in the table view - newest first or newest last - you can switch them.
- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)sourceIndexPath toIndexPath:(NSIndexPath *)destinationIndexPath {
isModifyingOrder = YES;
NSUInteger fromIndex = sourceIndexPath.row;
NSUInteger toIndex = destinationIndexPath.row;
if (fromIndex == toIndex) {
return;
}
FFObject *affectedObject = [self.fetchedResultsController.fetchedObjects objectAtIndex:fromIndex];
NSInteger delta;
if (fromIndex < toIndex) {
delta = toIndex - fromIndex;
NSLog(@"Moved down by %lu cells", delta);
[self.objectController moveObjectOrderUp:affectedObject by:delta];
} else {
delta = fromIndex - toIndex;
NSLog(@"Moved up by %lu cells", delta);
[self.objectController moveObjectOrderDown:affectedObject by:delta];
}
isModifyingOrder = NO;
}
And here the part in the controller. This can be written nicer, but for understanding this is maybe best.
- (void)moveObjectOrderUp:(FFObject*)affectedObject by:(NSInteger)delta {
NSInteger fromIndex = [affectedObject.index integerValue] - delta;
NSInteger toIndex = [affectedObject.index integerValue];
if (fromIndex < 1) {
return;
}
NSFetchRequest* fetchRequest = [[NSFetchRequest alloc] init];
[fetchRequest setEntity:[NSEntityDescription entityForName:@"Object" inManagedObjectContext:self.managedObjectContext]];
NSPredicate* predicate = [NSPredicate predicateWithFormat:@"(index >= %d) AND (index < %d)", fromIndex, toIndex];
[fetchRequest setPredicate:predicate];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"index" ascending:YES];
NSArray *sortDescriptors = [NSArray arrayWithObjects:sortDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
NSError* fetchError = nil;
NSArray* objects = [self.managedObjectContext executeFetchRequest:fetchRequest error:&fetchError];
for (FFObject* object in objects) {
NSInteger newIndex = [object.index integerValue] + 1;
object.index = [NSNumber numberWithInteger:newIndex];
}
affectedObject.index = [NSNumber numberWithInteger:fromIndex];
[self saveContext];
}
- (void)moveObjectOrderDown:(FFObject*)affectedObject by:(NSInteger)delta {
NSInteger fromIndex = [affectedObject.index integerValue];
NSInteger toIndex = [affectedObject.index integerValue] + delta;
NSFetchRequest* fetchRequest = [[NSFetchRequest alloc] init];
[fetchRequest setEntity:[NSEntityDescription entityForName:@"Object" inManagedObjectContext:self.managedObjectContext]];
NSPredicate* predicate = [NSPredicate predicateWithFormat:@"(index > %d) AND (index <= %d)", fromIndex, toIndex];
[fetchRequest setPredicate:predicate];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"index" ascending:YES];
NSArray *sortDescriptors = [NSArray arrayWithObjects:sortDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
NSError* fetchError = nil;
NSArray* objects = [self.managedObjectContext executeFetchRequest:fetchRequest error:&fetchError];
for (FFObject* object in objects)
{
NSInteger newIndex = [object.index integerValue] - 1;
object.index = [NSNumber numberWithInteger:newIndex];
}
affectedObject.index = [NSNumber numberWithInteger:toIndex];
[self saveContext];
}
Don't forget to use a second BOOL
in your view controller for the delete action to prevent the move notification to do anything. I call it isDeleting
and put it here.
- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject
atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type
newIndexPath:(NSIndexPath *)newIndexPath {
if (isModifyingOrder) return;
...
switch(type) {
...
case NSFetchedResultsChangeMove:
if (isDeleting == false) {
[self.tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:localIndexPath] withRowAnimation:UITableViewRowAnimationFade];
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:localNewIndexPath]withRowAnimation:UITableViewRowAnimationFade];
}
break;
...
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With