Search code examples
iosobjective-ccore-datanssetnsorderedset

Core Data Relationship NSOrderedSet returning different order on each device


I have several one-to-many relationships in my data model. In each case it is important that the order of the data retrieved from the model matches the order in which it was inserted. I have ticked the 'Ordered' box in the inspector for each relationship. I add the data as follows:

NSMutableOrderedSet *set = [destinationEntity mutableOrderedSetValueForKey:relationshipKey];
[set addObject:sourceEntity];
[CoreDataFunctions saveEntity:destinationEntity];

I subsequently retrieve the set as follows:

NSOrderedSet *set = [sourceEntity valueForKey:relationshipKey];

On the device on which the data were added this works fine - I can add multiple entities and they always display in the correct order. The problem is that on devices which have received the data through iCloud synchronisation, the set is returned in a different order. Always the same order mind you, but not the same order as entered on the original device.

To get around this I have added an 'order' attribute which is populated by an incrementing integer when inserting the objects into the set. I can then sort the set by that key after retrieving it from the data model. However, I can't help but feel that something is wrong if the NSOrderedSet is not coming back in a predictable order.

I wonder whether this is related to some errors I see in the log when a device is receiving transactions from iCloud. I see the following many times, presumably once for each transaction:

*** ERROR: this process has called an NSArray-taking method, such as initWithArray:, and passed in an NSSet object. This is being worked-around for now, but will soon cause you grief.

I assume this is because the objects in the data model are of type NSSet, and Core Data is passing them to something expecting an NSArray. I have no idea what I can do about this, however, or if it is related to this problem. I assume that it is, as I suspect that Core Data is converting the NSOrderedSet to an NSArray and losing the ordering in the process. I cannot confirm this though.

Any advice gratefully received!


Solution

  • The most reliable way to do this is to create an Abstract Entity like BaseEntity from which every other class in your data model inherits. BaseEntity would have a dateCreated property that you set in awakeFromInsert in your NSManagedObject subclass.

    Then when you perform a fetch request you can specify the NSSortDescriptor to sort using the dateCreated this is pretty quick especially if you're using SQLite.

    If you do this then you can uncheck the ordered button in CoreData.

    EDIT: Here is an example of ordering relationships in a managed object subclass:

    Given an entity Entity with a CoreData generated relationship property:

    @property (nullable, nonatomic, retain) NSSet<Entity *> *toManyRelationship;
    

    You can then create your own property:

    @property (nullable, nonatomic, strong, readonly) NSArray<Entity *> *orderedToManyRelationship;
    

    This is implemented like so:

    @dynamic toManyRelationship;
    
    - (NSArray<Entity *> *)orderedToManyRelationship
    {
        NSSortDescriptor *dateSorter = [NSSortDescriptor sortDescriptorWithKey:@"dateCreated" ascending:YES];
    
        return [[self.toManyRelationship allObjects] sortedArrayUsingDescriptors:@[dateSorter]];
    }