I've got an iOS app that uses restkit to handle json responses to map things into core data. Anytime I perform a request through RKObjectManager's get/post/put/delete methods, it works great, and I never run into any issues.
The app I'm developing also supports socket updates, for which I'm using SocketIO to handle. SocketIO also is working flawlessly, and every event the server sends out I receive without fail, unless the app isn't running at that time.
The issue occurs when I try to take the json data from the socket event, and map it to core data. If the socket event comes in at the same time a a response comes back from a request I made through RKObjectManager, and they are both giving me the same object for the first time, they often both end up making 2 copies of the same ManagedObject in coredata, and I get the following warning in console:
Managed object Cache returned 2 objects for the identifier configured for the [modelObjectName] entity, expected 1.
Here is the method I've made containing the code for making the RKMapperOperation:
+(void)createOrUpdateObjectWithJSONDictionary:(NSDictionary*)jsonDictionary
{
RKManagedObjectStore* managedObjectStore = [CMRAManager sharedInstance].objectManager.managedObjectStore;
NSManagedObjectContext* context = managedObjectStore.mainQueueManagedObjectContext;
[context performBlockAndWait:^{
RKEntityMapping* modelEntityMapping = [self entityMappingInManagedObjectStore:managedObjectStore];
NSDictionary* modelPropertyMappingsByDestinationKeyPath = modelEntityMapping.propertyMappingsByDestinationKeyPath;
NSString* modelMappingObjectIdSourceKey = kRUClassOrNil([modelPropertyMappingsByDestinationKeyPath objectForKey:NSStringFromSelector(@selector(object_Id))], RKPropertyMapping).sourceKeyPath;
NSString* modelObjectId = [jsonDictionary objectForKey:modelMappingObjectIdSourceKey];
CMRARemoteObject* existingObject = [self searchForObjectOfCurrentClassWithId:modelObjectId];
RKMapperOperation* mapperOperation = [[RKMapperOperation alloc]initWithRepresentation:jsonDictionary mappingsDictionary:@{ [NSNull null]: modelEntityMapping }];
[mapperOperation setTargetObject:existingObject];
RKManagedObjectMappingOperationDataSource* mappingOperationDataSource = [[RKManagedObjectMappingOperationDataSource alloc]initWithManagedObjectContext:context cache:managedObjectStore.managedObjectCache];
[mappingOperationDataSource setOperationQueue:[NSOperationQueue new]];
[mappingOperationDataSource setParentOperation:mapperOperation];
[mappingOperationDataSource.operationQueue setMaxConcurrentOperationCount:1];
[mappingOperationDataSource.operationQueue setName:[NSString stringWithFormat:@"%@ with operation '%@'", NSStringFromSelector(_cmd), mapperOperation]];
[mapperOperation setMappingOperationDataSource:mappingOperationDataSource];
NSError* mapperOperationError = nil;
BOOL mapperOperationSuccess = [mapperOperation execute:&mapperOperationError];
NSAssert((mapperOperationError == nil) && (mapperOperationSuccess == TRUE), @"Execute mapperOperation error");
if (mapperOperationError || (mapperOperationSuccess == FALSE))
{
NSLog(@"mapperOperationError: %@",mapperOperationError);
}
NSError* contextSaveError = nil;
BOOL contextSaveSuccess = [context saveToPersistentStore:&contextSaveError];
NSAssert((contextSaveError == nil) && (contextSaveSuccess == TRUE), @"Save context error");
}];
}
In the above code, I first try and fetch the existing managed object if it currently exists to set it to the mapper request's target object. The method to find the object (searchForObjectOfCurrentClassWithId:) looks like the following:
+(instancetype)searchForObjectOfCurrentClassWithId:(NSString*)objectId
{
NSManagedObjectContext* context = [CMRAManager sharedInstance].objectManager.managedObjectStore.mainQueueManagedObjectContext;
__block CMRARemoteObject* fetchedObject = nil;
[context performBlockAndWait:^{
NSFetchRequest* fetchRequest = [self fetchRequestForCurrentClassObjectWithId:objectId];
NSError* fetchError = nil;
NSArray *entries = [context executeFetchRequest:fetchRequest error:&fetchError];
if (fetchError)
{
NSLog(@"fetchError: %@",fetchError);
return;
}
if (entries.count != 1)
{
return;
}
fetchedObject = kRUClassOrNil([entries objectAtIndex:0], CMRARemoteObject);
if (fetchedObject == nil)
{
NSAssert(FALSE, @"Should be of this class");
return;
}
}];
return fetchedObject;
}
My best guess at the issue here is that it's probably due to the managed object contexts, and their threads. I don't have the best understanding of how they should necessarily be working, as I've been able to depend on Restkit's correct usage of it. I've done my best to copy how Restkit set up these mapping operations, but am assuming I've made an error somewhere in the above code.
I'm willing to post any other code if it would be helpful. I didn't post my RKEntityMapping code, because I'm pretty sure the error doesn't lie there - after all, Restkit has been successfully mapping these objects when it does the mapper operation itself, even when there's redundant JSON objects/data to map.
Another reason I think the issue must be my doing a bad implementation of the managed object contexts and their threads, is because I'm testing on an iPhone 5c, and an iPod touch, and the issue doesn't happen on the iPod touch, which I believe only has 1 core, but the iPhone 5c does sometimes encounter the issue, and I believe it has multiple cores. I should emphasize that I'm not sure of the statements I've made in this paragraph are necessarily true, so don't assume I know what I'm talking about with the device cores, it's just something I think I've read before.