8
votes

I have a core data based app that has a one object (a list) to many objects (list items) relationship. I'm working on syncing data between devices, and as part of that I import lists from XML files in background threads (via an NSOperation subclass).

When I'm updating an existing list, I delete all of its old list items (from the NSManagedObjectContext specific to that thread) and replace them with new ones from the XML file... the delete is handled by enumerating through the items for that list:

for (ListItemCD *item in listToUpdate.listItems) {
    [self.importContext deleteObject:item];
}

However, once in a while, I get a crash during that enumeration:

* Terminating app due to uncaught exception 'NSGenericException', reason: '* Collection <_NSFaultingMutableSet: 0x4fcfcb0> was mutated while being enumerated.

I'm not sure where to start looking for the cause of that problem. I don't modify the list in any other part of the code while the enumeration is happening. There can be multiple threads at the same time, as different lists are imported/updated... would saving the context in another thread cause an issue - since it also notifies the main context (if it happened at the same time as the enumeration)?

If it helps, here's the code from the "main" function of my NSOperation subclass (where I delete the old list items from Core Data, and update the list by parsing the XML data):

- (void)main {

    // input the xml data into GDataXML
    NSData *xmlData = [[NSMutableData alloc] initWithContentsOfFile:self.filePath];
    NSError *error;
    GDataXMLDocument *doc = [[GDataXMLDocument alloc] initWithData:xmlData options:0 error:&error];



    // get the list name (so that I know which list to update)
    NSString *listName;
    NSArray *listNames = [doc.rootElement elementsForName:@"listName"];
    if (listNames.count > 0) {
        GDataXMLElement *listNameElement = (GDataXMLElement *) [listNames objectAtIndex:0];
        listName = listNameElement.stringValue;
        // NSLog(@"listName: %@", listName);




        // perform a fetch to find the old list with the same name (if there is one)
        NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
        NSEntityDescription *entity = [NSEntityDescription entityForName:@"SubListCD" inManagedObjectContext:self.importContext];
        [fetchRequest setEntity:entity];

        NSPredicate *predicate = [NSPredicate predicateWithFormat:@"%K like %@", @"listName", listName];
        [fetchRequest setPredicate:predicate];

        NSError *error;
        NSArray *fetchedObjects = [self.importContext executeFetchRequest:fetchRequest error:&error];
        // NSLog(@"fetchedObjects count: %d", [fetchedObjects count]);
        [fetchRequest release];



        /*
         // if I found the list, update its data
         */

        if ([fetchedObjects count] == 1) {
            SubListCD *listToUpdate = [fetchedObjects objectAtIndex:0];

            // get the list icon name
            NSArray *listIconNames = [doc.rootElement elementsForName:@"listIconName"];
            if (listIconNames.count > 0) {
                GDataXMLElement *listIconNameElement = (GDataXMLElement *) [listIconNames objectAtIndex:0];
                NSString *listIconName = listIconNameElement.stringValue;
                // NSLog(@"listIconName: %@", listIconName);
                listToUpdate.listIconName = [NSString stringWithString:listIconName];
            }

            // get the isChecklist BOOL
            NSArray *isChecklistBools = [doc.rootElement elementsForName:@"isChecklist"];
            if (isChecklistBools.count > 0) {
                GDataXMLElement *isChecklistElement = (GDataXMLElement *) [isChecklistBools objectAtIndex:0];
                NSString *isChecklist = isChecklistElement.stringValue;
                // NSLog(@"isChecklist: %@", isChecklist);
                listToUpdate.isCheckList = [NSNumber numberWithBool:[isChecklist isEqualToString:@"YES"]];
            }

            // get the itemsToTop BOOL
            NSArray *itemsToTopBools = [doc.rootElement elementsForName:@"itemsToTop"];
            if (itemsToTopBools.count > 0) {
                GDataXMLElement *itemsToTopElement = (GDataXMLElement *) [itemsToTopBools objectAtIndex:0];
                NSString *itemsToTop = itemsToTopElement.stringValue;
                // NSLog(@"itemsToTop: %@", itemsToTop);
                listToUpdate.itemsToTop = [NSNumber numberWithBool:[itemsToTop isEqualToString:@"YES"]];
            }

            // get the includeInBadgeCount BOOL
            NSArray *includeInBadgeCountBools = [doc.rootElement elementsForName:@"includeInBadgeCount"];
            if (includeInBadgeCountBools.count > 0) {
                GDataXMLElement *includeInBadgeCountElement = (GDataXMLElement *) [includeInBadgeCountBools objectAtIndex:0];
                NSString *includeInBadgeCount = includeInBadgeCountElement.stringValue;
                // NSLog(@"includeInBadgeCount: %@", includeInBadgeCount);
                listToUpdate.includeInBadgeCount = [NSNumber numberWithBool:[includeInBadgeCount isEqualToString:@"YES"]];
            }

            // get the list's creation date
            NSArray *listCreatedDates = [doc.rootElement elementsForName:@"listDateCreated"];
            if (listCreatedDates.count > 0) {
                GDataXMLElement *listDateCreatedElement = (GDataXMLElement *) [listCreatedDates objectAtIndex:0];
                NSString *listDateCreated = listDateCreatedElement.stringValue;
                // NSLog(@"listDateCreated: %@", listDateCreated);
                listToUpdate.dateCreated = [self dateFromString:listDateCreated];
            }

            // get the list's modification date
            NSArray *listModifiedDates = [doc.rootElement elementsForName:@"listDateModified"];
            if (listModifiedDates.count > 0) {
                GDataXMLElement *listDateModifiedElement = (GDataXMLElement *) [listModifiedDates objectAtIndex:0];
                NSString *listDateModified = listDateModifiedElement.stringValue;
                // NSLog(@"listDateModified: %@", listDateModified);
                listToUpdate.dateModified = [self dateFromString:listDateModified];
            }



            // NOTE: it's okay to get the displayOrder from index.plist here, since these update operations aren't called until after index.plist is loaded from Dropbox

            // get a reference to the documents directory
            NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
            NSString *documentsDirectory = [paths objectAtIndex:0];

            // get the file path of the index.plist file
            NSString *indexFilePath = [documentsDirectory stringByAppendingPathComponent:@"index.plist"];

            // build an array with the names of the lists in the index
            NSMutableArray *listsIndexArray = [NSMutableArray arrayWithContentsOfFile:indexFilePath];

            int listIndex = [listsIndexArray indexOfObject:listName];

            listToUpdate.displayOrder = [NSNumber numberWithInt:listIndex];





            // remove the old list items from the listToUpdate, since I'll be adding them from scratch from the XML file
            for (ListItemCD *item in listToUpdate.listItems) {
                [self.importContext deleteObject:item];
            }




            // get an array of the list items so I can add them all
            NSArray *listItems = [doc.rootElement elementsForName:@"item"];
            if (listItems.count > 0) {
                int counter = 0;
                for (GDataXMLElement *item in listItems) {

                    // create the new item
                    ListItemCD *newItem = [NSEntityDescription insertNewObjectForEntityForName:@"ListItemCD" inManagedObjectContext:self.importContext];

                    // item name
                    NSArray *itemNames = [item elementsForName:@"itemName"];
                    if (itemNames.count > 0) {
                        GDataXMLElement *itemNameElement = (GDataXMLElement *) [itemNames objectAtIndex:0];
                        NSString *itemName = itemNameElement.stringValue;
                        // NSLog(@"itemName: %@", itemName);
                        newItem.itemName = [NSString stringWithString:itemName];
                    } else continue;

                    // item note
                    NSArray *itemNotes = [item elementsForName:@"itemNote"];
                    if (itemNotes.count > 0) {
                        GDataXMLElement *itemNoteElement = (GDataXMLElement *) [itemNotes objectAtIndex:0];
                        NSString *itemNote = itemNoteElement.stringValue;
                        // NSLog(@"itemNote: %@", itemNote);
                        newItem.itemNote = [NSString stringWithString:itemNote];
                    } else continue;

                    // itemReadOnly BOOL
                    NSArray *itemReadOnlyBools = [item elementsForName:@"itemReadOnly"];
                    if (itemReadOnlyBools.count > 0) {
                        GDataXMLElement *itemReadOnlyElement = (GDataXMLElement *) [itemReadOnlyBools objectAtIndex:0];
                        NSString *itemReadOnly = itemReadOnlyElement.stringValue;
                        // NSLog(@"itemReadOnly: %@", itemReadOnly);
                        newItem.itemReadOnly = [NSNumber numberWithBool:[itemReadOnly isEqualToString:@"YES"]];
                    } else continue;

                    // TODO: check my dates.. not sure if this will hold up in other locales

                    // item creation date
                    NSArray *itemCreatedDates = [item elementsForName:@"dateCreated"];
                    if (itemCreatedDates.count > 0) {
                        GDataXMLElement *dateCreatedElement = (GDataXMLElement *) [itemCreatedDates objectAtIndex:0];
                        NSString *dateCreated = dateCreatedElement.stringValue;
                        // NSLog(@"dateCreated: %@", dateCreated);
                        newItem.dateCreated = [self dateFromString:dateCreated];
                    } else continue;

                    // item modification date
                    NSArray *itemModifiedDates = [item elementsForName:@"dateModified"];
                    if (itemModifiedDates.count > 0) {
                        GDataXMLElement *dateModifiedElement = (GDataXMLElement *) [itemModifiedDates objectAtIndex:0];
                        NSString *dateModified = dateModifiedElement.stringValue;
                        // NSLog(@"dateModified: %@", dateModified);
                        newItem.dateModified = [self dateFromString:dateModified];
                    } else continue;

                    // item completed BOOL
                    NSArray *itemCompletedBools = [item elementsForName:@"itemCompleted"];
                    if (itemCompletedBools.count > 0) {
                        GDataXMLElement *itemCompletedElement = (GDataXMLElement *) [itemCompletedBools objectAtIndex:0];
                        NSString *itemCompleted = itemCompletedElement.stringValue;
                        // NSLog(@"itemCompleted: %@", itemCompleted);
                        newItem.itemCompleted = [NSNumber numberWithBool:[itemCompleted isEqualToString:@"YES"]];
                    } else continue;

                    // item completed date
                    NSArray *itemCompletedDates = [item elementsForName:@"dateCompleted"];
                    if (itemCompletedDates.count > 0) {
                        GDataXMLElement *dateCompletedElement = (GDataXMLElement *) [itemCompletedDates objectAtIndex:0];
                        NSString *dateCompleted = dateCompletedElement.stringValue;
                        // NSLog(@"dateCompleted string: %@", dateCompleted);
                        newItem.dateCompleted = [self dateFromString:dateCompleted];
                        // NSLog(@"dateCompleted: %@", newItem.dateCompleted);
                    } else continue;


                    // display order
                    newItem.displayOrder = [NSNumber numberWithInt:counter];
                    counter++;


                    // assign the new item to the listToUpdate
                    newItem.list = listToUpdate;
                }
            }



            // the list is now imported, so set isUpdating back to NO
            listToUpdate.isUpdating = [NSNumber numberWithBool:NO];



            // Save the context.
            NSError *saveError = nil;
            if (![self.importContext save:&saveError]) {
                NSLog(@"Unresolved error %@, %@", saveError, [saveError userInfo]);
                abort();
            }
            else {
                NSLog(@"saved after UPDATING a list while syncing!");
            }


        }
        else {
            NSLog(@"UpdateOperation - couldn't find an old version of the list to update!: %@", listName);
        }
    }

    [doc release];
    [xmlData release];
}

Thanks for any advice.

1

1 Answers

30
votes

There is a bit of a clue in the error message, where you can see the class NSFaultingMutableSet listed. Indeed the set you are enumerating is really just a proxy for the to-many relationship that will potentially load data on demand. Since items in the collection are being marked as deleted during the enumeration, the possibility exists that some of the collection will 'change' while you're enumerating it and you'll see that error.

A common way to deal with this is to create a copy of the collection and enumerate the copy. The naive approach to that would just be:

NSSet *iterItems = [[list.items copy] autorelease];
for (ListItemCD *item in iterItems) { ... }

But I've found when dealing with Core Data that -copy does not actually return a copy but often just another faulting proxy. So I instead choose to copy the collection this way:

NSSet *iterItems = [NSSet setWithSet:list.items];
for (ListItemCD *item in iterItems) { ... }