I am attempting to use a NSManagedObject as a response class in my requests, but I have relationships that I need saturated as well that I do not want managed because they change far more frequently. I have attempted to use just plain NSObject's along with transient relationships to NSManagedObject's, but either approach ends up with empty relationships (the transient or NSObjects don't get saturated).
Here is the object I want saturated from the request and stored in SQLite:
#interface Car : NSManagedObject
#property (nonatomic, retain) NSString *brand
#property (nonatomic, retain) MLocation *location //don't want persisted
#end
Here is the object I don't want persisted, but saturated:
//I have tried this with NSObject and NSManagedObject
#interface MLocation : NSObject
#property (nonatomic, assign) double latitude;
#property (nonatomic, assign) double longitude;
#end
Here is the response descriptor:
[manager addResponseDescriptor:[RKResponseDescriptor responseDescriptorWithMapping:self.buildCarMapping
method:RKRequestMethodGET
pathPattern:#"/v1/cars/near/:latitude/:longitude"
keyPath:nil
statusCodes:RKStatusCodeIndexSetForClass(RKStatusCodeClassSuccessful)] ];
Here is the object mapping:
- (RKObjectMapping *)buildCarMapping {
RKEntityMapping *carMapping = [RKEntityMapping mappingForEntityForName:#"Car" inManagedObjectStore:self.managedObjectStore];
[carMapping addAttributeMappingsFromDictionary:#{
#"brand" : #"brand"
}];
[carMapping addRelationshipMappingWithSourceKeyPath:#"location" mapping:self.buildLocationMapping];
return carMapping;
}
//I have tried this with NSObject and a transient NSManagedObject relationship
- (RKObjectMapping *)buildLocationMapping {
RKObjectMapping *locationMapping = [RKObjectMapping mappingForClass:[MLocation class]];
[locationMapping addAttributeMappingsFromDictionary:#{
#"longitude": #"longitude",
#"latitude": #"latitude"
}];
return locationMapping;
}
Here is an example response:
{
"brand": "Chevy",
"location": { //This is ignored by RestKit using my mappings
"latitude": 42.8,
"longitude": -98.6
}
}
I don't think there is going to be a way to do that because the managed objects are created on a background thread and the only way to get them back to you is by destroying those instances (with your temporary data) and generating the corresponding instances from the persistent data on the main thread.
Possibly if you create and execute the mapping operation yourself on the current thread it could work, but there would be a number of other requirements for you to manage / issues to deal with.
I would think about using 2 response descriptors. The first, like you have, would process the Core Data related content into the persistent store. The second would be used to create the plain (temporary) object, in an appropriate instance hierarchy, which contained the unique identifiers (as required) such that you could fetch the associated managed objects when you need them. You wouldn't have direct relationships, but you would have all the information you needed.
Related
I have created CKQuerySubscriptions to monitor remote insertion, modification and deletion of CKRecords. For inserted and modified records this works well because I can query CloudKit for the affected CKRecord, get the associated NSManagedObject and then handle the insertion and modification from there.
For deleted CKRecords, this is a problem because by the time the notification has been fired, the CKRecord has already been removed from CloudKit. This means the fetch request to get the now deleted CKRecord fails, so I have no way to know which NSManagedObject was associated with the deleted CKRecord.
I don't know if I'm going about this all the wrong way and if there is an easier way to handle all of this!
This works, but it feels a bit clunky. There must be an easier way! But if not, if this code is useful to anybody else, feel free to comment if you want the code used in any of the helper methods not shown (e.g. in the +[CoreDataFunctions fetchRecordsForEntityType: withCloudIDs: completion:] method);
//Array to hold all cloudIDs of existing NSManagedObject instances
NSMutableArray *cloudIDs = [NSMutableArray array];
//Populate cloudIDs array with the IDs of the existing NSManagedObject instances
for (NSManagedObject *item in self.items) {
NSUUID *cloudID = [item valueForKey:#"cloudID"];
[cloudIDs addObject:cloudID];
}
//Array to hold remaining NSManagedObject instances (i.e. the ones which were not deleted)
NSMutableArray *remainingItems = [NSMutableArray array];
//Fetch all remaining CKRecords (i.e. the ones which were not deleted
[CoreDataFunctions fetchRecordsForEntityType:[self managedObjectMonitoringClass] withCloudIDs:cloudIDs completion:^(NSArray<CKRecord *> *results) {
//For each local NSManagedObject instance
for (NSManagedObject *item in self.items) {
//The cloudID for the local NSManagedObject instance
NSString *localCloudID = [[item valueForKey:#"cloudID"] UUIDString];
//For each CKRecord in CloudKit
for (CKRecord *record in results) {
//The cloudID for the remote CKRecord object
NSString *remoteCloudID = [record valueForKey:#"CD_cloudID"];
//If the local and remote cloudIDs match, the local NSManagedObject entity represents a CKRecord which still exists in CloudKit
//Add the NSManagedObject entity to the remainingItems array
if ([remoteCloudID isEqualToString:localCloudID]) {
[remainingItems addObject:item];
break;
}
}
}
//Array to hold NSIndexPath objects to be removed from the collectionView
NSMutableArray *indexPaths = [NSMutableArray array];
//For each NSManagedObject stored locally
for (NSManagedObject *item in self.items) {
//If the remainingItems array does not contain this NSManagedObject, it has been deleted from CloudKit
//Create and indexPath for this item and add it to the array
if (![remainingItems containsObject:item]) {
NSInteger index = [self.items indexOfObject:item];
[indexPaths addObject:[NSIndexPath indexPathForItem:index inSection:0]];
}
}
dispatch_async(dispatch_get_main_queue(), ^{
[[self TBcollectionView] performBatchUpdates:^{
//Set the local items array to whatever is remaining in CloudKit
self.items = remainingItems;
//Delete the indexPaths for the items which were deleted
[[self TBcollectionView] deleteItemsAtIndexPaths:indexPaths];
} completion:nil];
});
}];
I use subscriptions with remote notifications and CKFetchRecordZoneChangesOperation.
If the "application(didReceiveRemoteNotification:)" method is called, if build and fire a "CKFetchRecordZoneChangesOperation".
There are several completion handlers. One is for the updated records (add / modified) and there is a special one for deleted records.
This handler is called "recordWithIDWasDeletedBlock" and is called for each single record, which has been deleted, providing the recordID of the deleted record. With this information you should be able to process what you need.
This is a test on part of Apple's Core Data PG, which I quote here
You started with a strong reference to a managed object from another object in your application.
You deleted the managed object through the managed object context.
You saved changes on the object context.
At this point, the deleted object has been turned into a fault. It isn’t destroyed because doing so would violate the rules of memory management.
Core Data will try to realize the faulted managed object but will fail to do so because the object has been deleted from the store. That is, there is no longer an object with the same global ID in the store.
So I setup a test project to see if it is the real case.
I'm using MagicalRecord to save some troubles creating MOCs, the code is based on a Core data model Class named "People"
#interface People : NSManagedObject
#property (nonatomic) int64_t userID;
#property (nullable, nonatomic, retain) NSString *name;
#end
In the test part, I wrap the MOCs MagicalRecord created into backgroundMOC and UIMOC so that those who are not familiar with MagicalRecord won't be confused.
UIMOC is BackgroundMOC's child and will merge backgroundMOC's changes by listening to NSManagedObjectContextDidSaveNotification backgroundMOC send out.
The "saveWithBlockAndWait" is just a wrapper around "performBlockAndWait". So here comes,
[[self backgroundMOC] MR_saveWithBlockAndWait:^(NSManagedObjectContext * _Nonnull localContext) {
People *people = [People MR_createEntityInContext:localContext];
people.userID = 1;
people.name = #"Joey";
}];
People *peopleInMainThread = [People MR_findFirstInContext:[self UIMOC]];
NSLog(#"Before delete, name = %#", peopleInMainThread.name);
[[self backgroundMOC] MR_saveWithBlockAndWait:^(NSManagedObjectContext * _Nonnull localContext) {
People *people = [People MR_findFirstInContext:localContext];
NSLog(#"Deleting, name = %#", people.name);
[localContext deleteObject:people];
}];
NSLog(#"After delete, name = %#", peopleInMainThread.name);
[[self UIMOC] save:nil];
NSLog(#"After save UIMOC, name = %#", peopleInMainThread.name);
The NSLog result is
Before delete, name = Joey //As expected
Deleting, name = Joey //As expected
After delete, name = Joey //Shouldn't it be nil already?
After save UIMOC, name = null //As expected
This result seems to state that Merge from parent MOC won't make model objects fault, which could lead to some hard-to-find bugs or instead tedious checking codes everywhere.
Again with the people object. I'll have to do things like this
- (void)codesInSeriousApp
{
[[self backgroundMOC] MR_saveWithBlockAndWait:^(NSManagedObjectContext * _Nonnull localContext) {
People *people = [People MR_createEntityInContext:localContext];
people.userID = 1;
people.name = #"Joey";
}];
__block People *people = nil;
[[self UIMOC] performBlockAndWait:^{
people = [People MR_findFirstInContext:[self UIMOC]];
}];
[self sendHttpRequestViaAFNetworking:^{
//this block is executed on main thread, which is AFNetworking's default behavior
if ([[self UIMOC] existingObjectWithID:people.objectID error:NULL])//[people isFault] would be NO here, and people's properties stay still.
{
//do something
}
else
{
//the people object is gone
//maybe some codes on another thread deleted it and save to the backgroundMOC
//the UIMOC merge the changes sent by notification, but the people object is still NOT FAULT!
}
}];
}
As far as I can tell, for any model non-fault object in a specific MOC, say MOCA, the object won't be fault until [MOC save:&error] called all the way down to the persistent store.
What really confuse me is, if Another MOC, already know that the object is fault by doing the saving chain, and MOCA merged changes that very MOC send out, how come the object in it is still non-fault?
Am I misunderstood or anything? Any reply would be appreciated.
Thx in advance :)
Currently, I am developing an app to book cars. All booking related data are stored in an entity 'Bookings'. As some attributes of 'Bookings' or relationships between 'Bookings' and other enties are mandatory I decided to add all managedObjects of entity 'Bookings' to their own managedObjectContext. This context will also be stored in a separate variable to avoid losing it. This works fine unless I'll sign (enterprise store or adhoc) my app and deploy it. ARC is enabled.
Class Bookings interface
#interface Bookings : NSManagedObject {
#private
NSManagedObjectContext *mContext;
}
#end
Class Bookings implementation
#implementation Bookings {
+ (Bookings *)booking {
NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:concurrencyType];
[context setPersistentStoreCoordinator:[self persistentStoreCoordinator]];
Bookings *object = (Bookings*)[[NSManagedObject alloc] initWithEntity:[self entityForName:pName] insertIntoMarenagedObjectContext:context];
[object setSomeReservationData:...];
...
// here I store the context in an ivar of my booking object
[object->mContext = context];
return object;
}
}
At this state the Booking object will not be saved!
Class BookingsVC
Bookings *booking = [Bookings booking];
NSLog(#"Context: %#", [booking managedObjectContext]);
Nothing saved or altered but context is null.
Console output on device (adhoc signed and deployed via iPhone-Configurator or Testflight)
... Context: (null)
Console output on simulator or device (adhoc signed but installed via Xcode)
... Context: <NSManagedObjectContext: 0x893c520>
So why does an unsaved managedObject lose its managedObjectContext and how can this be avoided? Is it a bug or the expected behavior?
Your context is nullified at the end of your function. see here
Your object is disowned by the context making all its properties null, in debug mode there exists an autorelease pool keeping the context from being deallocated.
i have multiple NSmanagedObject from the same entity ( we call PersonEntity).
This entity have a relationship "to-many" for another entity (we call BusinessEntity).
when i populate my store, i create the BusinessEntity managedObject.
After i add this BusinessEntity managedObject to my first PersonEntity managedObject.
[(Person *)entity1 addBusinessObject:businessEntity];
[(Person *)entity2 addBusinessObject:businessEntity];
the addBusinessObject function ( generate by XCode ) :
- (void)addBusinessObject:(NSManagedObject *)value {
NSSet *changedObjects = [[NSSet alloc] initWithObjects:&value count:1];
[self willChangeValueForKey:#"business" withSetMutation:NSKeyValueUnionSetMutation usingObjects:changedObjects];
[[self primitiveValueForKey:#"business"] addObject:value];
[self didChangeValueForKey:#"business" withSetMutation:NSKeyValueUnionSetMutation usingObjects:changedObjects];
[changedObjects release];
}
this work but only just after the populate.
If i save the store, only the relationShip between the entity1 and the businessEntity exist.
I have no relationShip between entity2 and businessEntity.
Really strange
PS: my two Entity are subclass of NSManagedObject for use with undefined property, and transient property.
Thanks for your help
On the apple documentation Core Data Programming Guide
i have read this important text :
Important: You must define
many-to-many relationships in both
directions—that is, you must specify
two relationships, each being the
inverse of the other. You can’t just
define a to-many relationship in one
direction and try to use it as a
many-to-many. If you do, you will end
up with referential integrity
problems.
Now it's work
I have an application where I would like to exchange information, managed via Core Data, between two iPhones.
First turning the Core Data object to an NSDictionary (something very simple that gets turned into NSData to be transferred).
My CoreData has 3 string attributes, 2 image attributes that are transformables.
I have looked through the NSDictionary API but have not had any luck with it, creating or adding the CoreData information to it.
Any help or sample code regarding this would be greatly appreciated.
I would recommend that you convert the Core Data objects to an intermediate format like JSON before pushing it over the wire. I have written up the code on how to transform NSManagedObject instances into and out of JSON in this other post:
JSON and Core Data on the iPhone
NSManagedObject doesn't conform to the NSCoding Protocol so you can't convert a managed object straight to data.
Instead, you just need to add a method to the managed object subclass that returns a dictionary with the instance attributes and then on the receiver side, use those to create a new managed object in the local context.
Edit:
From comments:
Currently I have for the sending
side..
NSData* data;
NSString *str0 = [NSString stringWithFormat:#"%#",[[person valueForKey:#"PersonName"] description]];
NSString *str1 = [NSString stringWithFormat:#"%#",[[person valueForKey:#"alias"] description]];
NSMutableDictionary *taskPrototype = [NSMutableDictionary dictionary];
[taskPrototype setObject:str0 forKey:#"PersonName"];
[taskPrototype setObject:str1 forKey:#"alias"];
data = ?????;
//I do not know what to put here... [self mySendDataToPeers:data];
on the receiving side I have...
NSMutableDictionary *trial = [[NSMutableDictionary alloc] initWithData:data];
NSString *str0a = ???? NSString *str1a = ????
//I dont know what to put after this to retrieve the values and keys from the dictionary
You would simply reverse the process to create a managed object on the receiver.
NSMutableDictionary *trial = [[NSMutableDictionary alloc] initWithData:data];
NSManagedObject *person=[NSEntityDescription insertNewObjectForEntityForName:#"PersonEntity" inManagedObjectContext:moc];
[person setValue:[trial objectForKey:#"PersonName"] forKey:#"PersonName"];
[person setValue:[trial objectForKey:#"alias"] forKey:#"alias"];
.. and you're done.