Adrian
Adrian

Reputation: 16725

Trials and tribulations of changing NSOrderedSet order in Core Data

I have an NSOrderedSet attached to a Core Data object that's passed into a UITableViewController. The managedObjectContext is alive and well in the UITableViewController and myNSOrderedSet displays properly my tableView. When I go to reorder myNSOrderedSet, the app crashes. I put a breakpoint in moveRowAtIndexPath to see what's going on and everything's fine up until I try to save it.

- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath {
    // BREAKPOINT: "po self.routineToEdit.exercises" shows order is 0, 1, 2
    NSMutableOrderedSet *orderedSet = [self.routineToEdit.exercises mutableCopy];

    [orderedSet exchangeObjectAtIndex:fromIndexPath.row withObjectAtIndex:toIndexPath.row];

    // BREAKPOINT: "po self.routineToEdit.exercises" shows order is 2, 1, 0
    self.routineToEdit.exercises = orderedSet;

    [self saveContext];
}

- (void)saveContext {
    if (self.managedObjectContext != nil) {
        NSError *error = nil;
        if ([self.managedObjectContext hasChanges] && ![self.managedObjectContext save:&error]) {
            NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
            abort();
        }
    }
}

Rather than passing my object into MyUITableViewController, I'm tempted to use a fetchedResultsController to grab my object and display it on MyUITableViewController. Before I go that route, I'd like to see I can re-order an NSOrderedSet with the object that's passed in from AnotherUITableViewController.

Here's the console output from my crash:

2015-07-12 06:39:53.176 MyApp[26505:1105589] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[__NSCFString appendString:]: nil argument'
*** First throw call stack:
(
    0   CoreFoundation                      0x00000001060dec65 __exceptionPreprocess + 165
    1   libobjc.A.dylib                     0x0000000105663bb7 objc_exception_throw + 45
    2   CoreFoundation                      0x00000001060deb9d +[NSException raise:format:] + 205
    3   CoreFoundation                      0x00000001060afabf mutateError + 159
    4   CoreData                            0x000000010591ec26 -[_NSSQLGenerator prepareMasterReorderStatementPart2ForRelationship:] + 118
    5   CoreData                            0x000000010598cfb8 -[NSSQLAdapter newCorrelationMasterReorderStatementPart2ForRelationship:] + 72
    6   CoreData                            0x00000001059a5671 -[NSSQLiteConnection writeCorrelationMasterReordersFromTracker:] + 817
    7   CoreData                            0x00000001059a5f81 -[NSSQLiteConnection writeCorrelationChangesFromTracker:] + 65
    8   CoreData                            0x0000000105997627 -[NSSQLCore writeChanges] + 1351
    9   CoreData                            0x00000001058d32c7 -[NSSQLCore saveChanges:] + 423
    10  CoreData                            0x00000001058a3e74 -[NSSQLCore executeRequest:withContext:error:] + 484
    11  CoreData                            0x000000010597ee3f __65-[NSPersistentStoreCoordinator executeRequest:withContext:error:]_block_invoke + 4335
    12  CoreData                            0x0000000105987c30 gutsOfBlockToNSPersistentStoreCoordinatorPerform + 192
    13  libdispatch.dylib                   0x0000000108890614 _dispatch_client_callout + 8
    14  libdispatch.dylib                   0x0000000108876002 _dispatch_barrier_sync_f_invoke + 365
    15  CoreData                            0x0000000105979245 _perform + 197
    16  CoreData                            0x00000001058a3a58 -[NSPersistentStoreCoordinator executeRequest:withContext:error:] + 504
    17  CoreData                            0x00000001058cd52d -[NSManagedObjectContext save:] + 1213
    18  MyApp                           0x000000010511083e -[EditRoutineTableViewController saveContext] + 222
    19  MyApp                           0x0000000105110673 -[EditRoutineTableViewController tableView:moveRowAtIndexPath:toIndexPath:] + 371
    20  UIKit                               0x0000000106817822 -[UITableView _endReorderingForCell:wasCancelled:animated:] + 565
    21  UIKit                               0x000000010682b89d -[UIControl touchesEnded:withEvent:] + 462
    22  UIKit                               0x0000000106767958 -[UIWindow _sendTouchesForEvent:] + 735
    23  UIKit                               0x0000000106768282 -[UIWindow sendEvent:] + 682
    24  UIKit                               0x000000010672e541 -[UIApplication sendEvent:] + 246
    25  UIKit                               0x000000010673bcdc _UIApplicationHandleEventFromQueueEvent + 18265
    26  UIKit                               0x000000010671659c _UIApplicationHandleEventQueue + 2066
    27  CoreFoundation                      0x0000000106012431 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
    28  CoreFoundation                      0x00000001060082fd __CFRunLoopDoSources0 + 269
    29  CoreFoundation                      0x0000000106007934 __CFRunLoopRun + 868
    30  CoreFoundation                      0x0000000106007366 CFRunLoopRunSpecific + 470
    31  GraphicsServices                    0x000000010a12ba3e GSEventRunModal + 161
    32  UIKit                               0x00000001067198c0 UIApplicationMain + 1282
    33  MyApp                           0x000000010510a05f main + 111
    34  libdyld.dylib                       0x00000001088c4145 start + 1

Thank you for reading. I welcome your input.

I found this issue that's in the "same neighborhood" of the problem I'm encountering, however the order of my set is not static like the example given in the example.

Core Data ordering with a UITableView and NSOrderedSet

Upvotes: 1

Views: 635

Answers (3)

Adrian
Adrian

Reputation: 16725

The StackOverflow consensus seems to be there's a bug with how NSOrderedSet relationships are handled by Core Data. I was able to manipulate the order of my NSOrderedSet using the information contained in Dmitry Makarenko's post on the accessors generated by Xcode:

Exception thrown in NSOrderedSet generated accessors

I also cleaned up (at least I think so) the code I used for moving rows in the UITableView:

- (void)tableView:(UITableView *)tableView moveRowAtIndexPath:(NSIndexPath *)fromIndexPath toIndexPath:(NSIndexPath *)toIndexPath {
    NSMutableOrderedSet *orderedSet = [self.routineToEdit mutableOrderedSetValueForKey:@"stretches"].mutableCopy;

    // This is necessary, as exchangeObjectAtIndex:withObjectAtIndex doesn't work passing in indexPath.row
    NSInteger fromIndex = fromIndexPath.row;
    NSInteger toIndex = toIndexPath.row;

    // Swap objects at indexes
    [orderedSet exchangeObjectAtIndex:fromIndex withObjectAtIndex:toIndex];
    NSLog(@"ending orderedSet = /n%@", orderedSet);

    // Assign NSMutableOrderdSet to the object's NSOrderedSet
    self.routineToEdit.stretches = orderedSet;

    // Added performBlockAndWait so the save is complete before doing anything else
    [self.persistentStoreCoordinator performBlockAndWait:^{
        [self saveContext];
    }];

}

Saving the object to the managedObjectContext right after the UITableView rows/NSObjects are swapped is critical--I discovered this when I saw not all my changes were being saved when I clicked a Done IBAction w/ the save method invoked there:

[self.persistentStoreCoordinator performBlockAndWait:^{
            [self saveContext];
        }];

...but the app still crashed.

Finally, I changed the relationship between BOTH entities to ordered and that did the trick:

crash on coredata ios8

For my purposes, this is sufficient. I tried each of these fixes independently and none of them worked on a stand-alone basis. At this point, my app works the way I want it to, so I'm going to leave it alone for now.

Upvotes: 1

John Estropia
John Estropia

Reputation: 17500

You are already halfway there by calling mutableOrderedSetValueForKey:, don't call mutableCopy on it! Also, do not reassign this mutable ordered set back to exercises. just save your context, call refreshObject:mergeChanges: on the context, and exercises should reflect your changes.

Upvotes: 0

Mundi
Mundi

Reputation: 80273

I think the fetch results controller does not give you any advantage here (mainly due to the sorting requirement which does not make sense here).

I would just use the ordered set (the to-many relationship of the object being edited) directly to inform your table view data source. And I would check that assigning the newly ordered ordered set back to the object is working as expected.

Upvotes: 1

Related Questions