How to delete new objects from NSManagedObjectContext - objective-c

Ok, lets start with the code. I am looping through a returned array of dictionaries and creating (or updating) objects based on them. In this method I'm trying find or create a new entity. And then if the object is supposed to be deleted, I'd like to do so and not waste time updating it with the new information.
- (void)updateOrCreateObjectWith:(NSDictionary*)dictionary {
RKManagedObjectStore *objectStore = ((MyAppDelegate*)[[UIApplication sharedApplication] delegate]).objectStore;
id updateObject = (NSManagedObject*)[objectStore findOrCreateInstanceOfEntity:[resource entity] withPrimaryKeyAttribute:#"myID" andValue:[dictionary objectForKey:#"id"]];
[updateObject setMyID:[dictionary objectForKey:#"id"]];
// if marked for deletion, delete it now
if ([[dictionary objectForKey:#"deleted_at"] isKindOfClass:[NSString class]]) {
if ([updateObject isNew]){
NSError *error = nil;
[objectStore.managedObjectContext save:&error];
if (error) {
NSLog(#"error saving before delete: %#",error);
return;
}
// [objectStore.managedObjectContext deleteObject:updateObject];
// [objectStore.managedObjectCache delete:updateObject];
}
else {
[objectStore.managedObjectContext deleteObject:updateObject];
}
return;
}
[updateObject updateWith:dictionary];
}
The part to be aware of is the deleted_at section with the (1) save section, (2) delete object from context, and (3) delete object from cache. I have tried a few combinations of those three but I don't get the desired results.
If I delete it from the cache (just #3):
The objects get saved but they have no attributes.
If I delete it from the managed context (just #2) I get:
NSUnderlyingException=Cannot update object that was never inserted.
Since it was never inserted, I thought I'd save it and then delete it (#1 and #2), but then I get:
*** Terminating app due to uncaught exception 'NSObjectInaccessibleException', reason: 'CoreData could not fulfill a fault for '0xed27810 <x-coredata://4EE6AD5A-CC34-460A-A97A-0909454126A4/User/p166>''
So what is the proper way to remove a "new" object from NSMangedObjectContext?

It's easier to get the RKManagedObjectStore instance with [[RKObjectManager sharedManager] objectStore] (assuming you want the shared one, which it seems like you do since you're calling your app delegate).
Check for the deleted_at key before you ever create an NSManagedObject. This code assumes you're converting to type Resource, which is a subclass of NSManagedObject. It's untested but should give you an idea of what you should be doing.
- (void)updateOrCreateObjectWith:(NSDictionary*)dictionary {
RKManagedObjectStore *objectStore = [[RKObjectManager sharedManager] objectStore];
//get a reference to the object
Resource *resource = [Resource findFirstByAttribute:#"myID" withValue:[dictionary objectForKey:#"id"]];
//see if "deleted_at" exists in dictionary
if ([[dictionary objectForKey:#"deleted_at"] isKindOfClass:[NSString class]])
{
//check to see if object exists in the context
if(resource)
{
//if it exists, delete it
[objectStore.managedObjectContext deleteObject:resource];
}
} else {
//no "deleted at", so create the object
if (!resource) {
//resource is nil (it doesn't exist in the context), so we need to create it
resource = [Resource object];
}
[resource updateWith:dictionary];
}
NSError *error = nil;
[objectStore.managedObjectContext save:&error];
if (error) {
NSLog(#"error saving before delete: %#",error);
}
}

You want to avoid creating managed objects unless necessary. The best strategy is to follow this pseudo code:
NSManagedObject *existingObject = ...; // fetch the object
if (existingObject) {
if (deleted) {
[self.managedObjectContext deleteObject: existingObject];
}
} else {
if (!deleted) {
// create the object, insert it into the MOC, set the object properties
}
}

Related

Objective C - Firebase - How to add completion handler to FDataSnapshot

I'm experimenting with Firebase's FDataSnapshot to pull in data and I would like it to write its data to my core data using MagicalRecord.
According to Firebases "best practice" blog I need to keep a reference to the "handle" so it can be cleaned up later on. Further, they mention to put the FDSnapshot code in viewWillAppear.
I am wanting a callback so that when its finished doing its thing to update core data.
But I'm really note sure how to do that; its doing two things and giving a return at the same time.
// In viewWillAppear:
__block NSManagedObjectContext *context = [NSManagedObjectContext MR_context];
self.handle = [self.ref observeEventType:FEventTypeValue withBlock:^(FDataSnapshot *snapshot) {
if (snapshot.value == [NSNull null])
{
NSLog(#"Cannot find any data");
}
else
{
NSArray *snapshotArray = [snapshot value];
// cleanup to prevent duplicates
[FCFighter MR_truncateAllInContext:context];
for (NSDictionary *dict in snapshotArray)
{
FCFighter *fighter = [FCFighter insertInManagedObjectContext:context];
fighter.name = dict[#"name"];
[context MR_saveToPersistentStoreWithCompletion:^(BOOL contextDidSave, NSError *error){
if (error)
{
NSLog(#"Error - %#", error.localizedDescription);
}
}];
}
}
}];
NSFetchRequest *fr = [[NSFetchRequest alloc] initWithEntityName:[FCFighter entityName]];
fr.sortDescriptors = #[[NSSortDescriptor sortDescriptorWithKey:#"name" ascending:YES]];
self.fighterList = (NSArray *) [context executeFetchRequest:fr error:nil];
[self.tableView reloadData];
In the above code, the core data reading does not wait for the firebase to complete.
Thus, my query -- how would I best combine a completion handler so that when it is complete to update core data, and reload the tableview.
Many thanks
This is a common issue when working with Asynchronous data.
The bottom line is that all processing of data returned from an async call (in this case, the snapshot) needs to be done inside the block.
Anything done outside the block may happen before the data is returned.
So some sudo code
observeEvent withBlock { snapshot
//it is here where snapshot is valid. Process it.
NSLog(#"%#", snapshot.value)
}
Oh, and a side note. You really only need to track the handle reference when you are going to do something else with it later. Other than that, you can ignore the handles.
So this is perfectly valid:
[self.ref observeEventType:FEventTypeValue withBlock:^(FDataSnapshot *snapshot) {
//load your array of tableView data from snapshot
// and/or store it in CoreData
//reload your tableview
}

Use NSManagedObject's own context property to delete it?

It may be kinda naive but I was wondering if it is correct to use the following statement to delete a managed object from the persistent store of Core Data:
[managedObject.managedObjectContext deleteObject:managedObject];
I ran the above in Xcode debugger - it didn't complain but the object's content was still there. Could it be that context was referenced through the object to be deleted, and thus causing a memory lock preventing deletion of the object?
In regards to your content persisting, you still need to call save: on the context after deleting the object.
I can't answer specifically if you will have an issue by referencing the managedObjectContext in the managedObject as I usually use a 'DataManager' to manage my managedObjectContext. Below is an example of a delete method that I used in one of my dataManagers:
- (void)deleteReport:(Report*)aReport inContext:(NSManagedObjectContext*)context {
if (aReport != nil) {
if (context == nil) {
context = self.managedObjectContext;
}
context.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy;
[context deleteObject:aReport];
NSError *error = nil;
[context save:&error];
if (error) {
//NSLog(#"%#", error);
}
}}
EDIT: For clarification, the Report in this method is an instance of NSManagedObject, and the method takes NSManagedObjectContext as a parameter, because the application that it was pulled from supports the use of multiple contexts.

Parse.com -- PFFile is null

So I'm retrieving an object that has a file as one of it's fields. In the Parse.com data browser the file is there and downloads. However, when I retrieve the object, PFFile *wordlistFile = [object objectForKey:kWSWordlistFilesFileKey]; is returning null, and so getDataInBackgroundWithBlock does nothing.
Here's the log of the object I'm retrieving. There is no reference to the file:
2013-12-03 12:07:10.635 WSPhoto[24958:a0b] object = <WordlistFiles:lRHFmHaPRg:(null)> {
ACL = "<PFACL: 0xd445670>";
language = Spanish;
}
And here's the full code. It appears I'm doing everything correctly based on some examples I've seen:
PFQuery* wordlistFilesQuery = [PFQuery queryWithClassName:kWSWordlistFilesClassKey];
[wordlistFilesQuery whereKey:kWSWordlistFilesLanguageKey equalTo:language];
[wordlistFilesQuery includeKey:kWSWordlistFilesFileKey];
[wordlistFilesQuery setCachePolicy:kPFCachePolicyNetworkOnly];
[wordlistFilesQuery getFirstObjectInBackgroundWithBlock:^(PFObject *object, NSError *error) {
if (!error) {
PFFile *wordlistFile = [object objectForKey:kWSWordlistFilesFileKey];
NSLog(#"******* wordlistFile = %#",wordlistFile);
// Show HUD view
AppDelegate *appDel = (AppDelegate *)[[UIApplication sharedApplication]delegate];
[appDel showGlobalProgressHUDWithTitle:#"Loading wordlist. This may take a while."];
[wordlistFile getDataInBackgroundWithBlock:^(NSData *data, NSError *error) {
if (!error) {
// Super private stuff here
}
// The data didn't load
else {
NSLog(#"loadWordlistFromDBByFile -- wordlist does not exist, loading by querying");
[self loadWordlistFromDBByQuery:language];
}
} progressBlock:^(int percentDone) {
}];
}
// The object didn't load
else {
NSLog(#"loadWordlistFromDBByFile -- wordlist does not exist, loading by querying");
[self loadWordlistFromDBByQuery:language];
}
}];
Make sure your kWSWordlistFilesFileKey is exactly the same as the name of the column seen on the data browser on Parse.com.
So if the column is called "wordlistfile", make sure:
kWSWordlistFilesFileKey = #"wordlistfile";
OK, this was a silly error. I reloaded the WordlistFiles class in the Parse.com Data Browser, and the file disappeared. Not sure how it happened. I swear to you guys I was staring it in the face. I re-uploaded and now it's retrieving, and there doesn't seem to be any weird behavior where it's deleting it.
Operator error.

EXC_BAD_ACCESS during NSFileVersion call to removeOtherVersionsOfItemAtURL: inside coordinated write block

I'm using what seems to be a simple invocation of the NSFileVersion class method removeOtherVersionsOfItemAtURL: inside a coordinated writing block for some iCloud conflict resolution.
When my devices go into 'spaz mode', which is a technical term for repeatedly opening and closing the application on a few devices, an EXC_BAD_ACCESS exception is thrown internally. Code snippet:
- (void)compareVersionChanges:(NSFileVersion *)version {
if (![DataLoader iCloudPreferenceEnabled]) {
NSLog(#"Ignoring iCloud changes (version comparison) based on user preference");
return;
}
NSLog(#"compareVersionChanges");
dispatch_queue_t aQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(aQueue, ^(void) {
NSError *readError = nil;
NSFileCoordinator *coordinator = [[NSFileCoordinator alloc] initWithFilePresenter:(id)self];
[coordinator coordinateReadingItemAtURL:[version URL] options:0 error:&readError byAccessor:^(NSURL *newURL) {
DataContext *loadedContext = nil;
NSData *data = [NSData dataWithContentsOfURL:newURL];
NSError *e = nil;
loadedContext = [self convertXmlDataToContext:data error:&e];
if (e) {
NSLog(#"Done loading, error: %#", e);
[[DataLoader applicationDelegate] displayError:e];
loadedContext = nil;
}
if (!loadedContext) {
return;
}
id appDelegate = [DataLoader applicationDelegate];
DataContext *inMemoryContext = nil;
if (appDelegate != nil && [appDelegate respondsToSelector:#selector(context)]) {
inMemoryContext = [appDelegate performSelector:#selector(context)];
}
if (inMemoryContext) {
NSLog(#"Performing iCloud context synchronizating...");
DataContextSynchronizer *synchronizer = [[DataContextSynchronizer alloc] init];
ChangeSet *changes = [synchronizer compareLocalContext:inMemoryContext andRemoteContext:loadedContext];
if ([[changes changes] count] > 0) {
[SelectionManager disable];
#synchronized(appDelegate) {
NSLog(#"Applying synchronization changes...");
[synchronizer applyChangeSet:changes toDataContext:inMemoryContext];
NSLog(#"Synchronization changes applied");
}
[SelectionManager enable];
if ([appDelegate respondsToSelector:#selector(setSkipRefreshSave:)]) {
[appDelegate performSelector:#selector(setSkipRefreshSave:) withObject:[NSNumber numberWithBool:YES]];
}
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_async(mainQueue, ^(void) {
[SelectionManager notifyListeners];
});
if ([appDelegate respondsToSelector:#selector(setSkipRefreshSave:)]) {
[appDelegate performSelector:#selector(setSkipRefreshSave:) withObject:[NSNumber numberWithBool:NO]];
}
[self save:[[DataLoader applicationDelegate] context]];
} else {
NSLog(#"No sync changes applicable.");
}
NSError *coordinateWriteRemoveError = nil;
[coordinator coordinateWritingItemAtURL:newURL options:NSFileCoordinatorWritingForDeleting error:&coordinateWriteRemoveError byAccessor:^(NSURL *theURL) {
theURL = [theURL copy];
NSError *removeOtherVersionsError = nil;
[NSFileVersion removeOtherVersionsOfItemAtURL:theURL error:&removeOtherVersionsError];
if (removeOtherVersionsError) {
NSLog(#"Error removing other versions: %#", removeOtherVersionsError);
}
}];
if (coordinateWriteRemoveError) {
NSLog(#"Error occurred coordinating write for deletion of other file versions: %#", coordinateWriteRemoveError);
}
}
}];
if (readError) {
NSLog(#"Done loading (outside block) error: %#", readError);
}
});
}
I thought a little syntax highlighting might make this easier to examine:
Link to image of code snippet and failure stack in Xcode
The error actually occurs on line 1404, and as you can see from the below screenshot, it's deep in Apple code territory.
Link to image of debugger
Before submitting a radar, I thought I'd check here to see if there's something I'm doing wrong? The extra [... copy] on line 1402 was just a quick check to make sure I'm not losing the reference to the block-provided argument, and will be removed.
Edit: An important note! I'm using ARC.
Edit 2: I've noticed that when calling:
[NSFileVersion otherVersionsOfItemAtURL:theURL]
The return value is nil, which indicates (via the documentation):
...or nil if there is no such file. The array does not contain the version object returned by the currentVersionOfItemAtURL: method.
So by checking the return value of this method before I make the call to removeOtherVersionsOfItemAtURL:, it has alleviated the issue. But I still find it strange that an EXC_BAD_ACCESS is thrown, rather than that method handling it properly.
I've noticed that when calling:
[NSFileVersion otherVersionsOfItemAtURL:theURL]
immediately prior to the call to removeOtherVersionsOfItemAtURL:, the return value is nil, which indicates (via the documentation):
Returns: An array of file version objects or nil if there is no such
file. The array does not contain the version object returned by the
currentVersionOfItemAtURL: method.
So by checking the return value of this method before I make the call to removeOtherVersionsOfItemAtURL:, it has alleviated the issue. But I still find it strange that an EXC_BAD_ACCESS is thrown by removeOtherVersionsOfItemAtURL:, rather than that method simply returning NO, or simply populating the provided NSError object.
I'll be filing a Radar and will update here when I hear back.

Core Data unique attributes

Is it possible to make a Core Data attribute unique, i.e. no two MyEntity objects can have the same myAttribute?
I know how to enforce this programatically, but I'm hoping there's a way to do it using the graphical Data Model editor in xcode.
I'm using the iPhone 3.1.2 SDK.
Every time i create on object I perform a class method that makes a new Entity only when another one does not exist.
+ (TZUser *)userWithUniqueUserId:(NSString *)uniqueUserId inManagedObjectContext:(NSManagedObjectContext *)context
{
TZUser *user = nil;
NSFetchRequest *request = [[NSFetchRequest alloc] init];
request.entity = [NSEntityDescription entityForName:#"TZUser" inManagedObjectContext:context];
request.predicate = [NSPredicate predicateWithFormat:#"objectId = %#", uniqueUserId];
NSError *executeFetchError = nil;
user = [[context executeFetchRequest:request error:&executeFetchError] lastObject];
if (executeFetchError) {
NSLog(#"[%#, %#] error looking up user with id: %i with error: %#", NSStringFromClass([self class]), NSStringFromSelector(_cmd), [uniqueUserId intValue], [executeFetchError localizedDescription]);
} else if (!user) {
user = [NSEntityDescription insertNewObjectForEntityForName:#"TZUser"
inManagedObjectContext:context];
}
return user;
}
From IOS 9 there is a new way to handle unique constraints.
You define the unique attributes in the data model.
You need to set a managed context merge policy "Merge policy singleton objects that define standard ways to handle conflicts during a save operation" NSErrorMergePolicy is the default,This policy causes a save to fail if there are any merge conflicts.
- (NSManagedObjectContext *)managedObjectContext {
// Returns the managed object context for the application (which is already bound to the persistent store coordinator for the application.)
if (_managedObjectContext != nil) {
return _managedObjectContext;
}
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (!coordinator) {
return nil;
}
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_managedObjectContext setPersistentStoreCoordinator:coordinator];
[_managedObjectContext setMergePolicy:NSOverwriteMergePolicy];
return _managedObjectContext;
}
The various option are discussed at Apple Ducumentation Merge Policy
It is answered nicely here
Zachary Orr's Answer
and he has kindly also created a blogpost and sample code.
Sample Code
Blog Post
The most challenging part is to get the data Model attributes editable.The Secret is to left click and then right click, after you have clicked the + sign to add a constraint.
I've decided to use the validate<key>:error: method to check if there is already a Managed Object with the specific value of <key>. An error is raised if this is the case.
For example:
- (BOOL)validateMyAttribute:(id *)value error:(NSError **)error {
// Return NO if there is already an object with a myAtribute of value
}
Thanks to Martin Cote for his input.
You could override the setMyAttribute method (using categories) and ensure uniqueness right there, although this may be expensive:
- (void)setMyAttribute:(id)value
{
NSArray *objects = [self fetchObjectsWithMyValueEqualTo:value];
if( [objects count] > 0 ) // ... throw some exception
[self setValue:value forKey:#"myAttribute"];
}
If you want to make sure that every MyEntity instance has a distinct myAttribute value, you can use the objectID of the NSManagedObject objects for that matter.
I really liked #DoozMen approach!!
I think it's the easiest way to do what i needed to do.
This is the way i fitted it into my project:
The following code cycles while drawing a quite long tableView, saving to DB an object for each table row, and setting various object attributes for each one, like UISwitch states and other things: if the object for the row with a certain tag is not present inside the DB, it creates it.
NSFetchRequest *request = [[NSFetchRequest alloc] init];
request.entity = [NSEntityDescription entityForName:#"Obiettivo" inManagedObjectContext:self.managedObjectContext];
request.predicate = [NSPredicate predicateWithFormat:#"obiettivoID = %d", obTag];
NSError *executeFetchError = nil;
results = [[self.managedObjectContext executeFetchRequest:request error:&executeFetchError] lastObject];
if (executeFetchError) {
NSLog(#"[%#, %#] error looking up for tag: %i with error: %#", NSStringFromClass([self class]), NSStringFromSelector(_cmd), obTag, [executeFetchError localizedDescription]);
} else if (!results) {
if (obbCD == nil) {
NSEntityDescription *ent = [NSEntityDescription entityForName:#"Obiettivo" inManagedObjectContext:self.managedObjectContext];
obbCD = [[Obiettivo alloc] initWithEntity:ent insertIntoManagedObjectContext:self.managedObjectContext];
}
//set the property that has to be unique..
obbCD.obiettivoID = [NSNumber numberWithUnsignedInt:obTag];
[self.managedObjectContext insertObject:obbCD];
NSError *saveError = nil;
[self.managedObjectContext save:&saveError];
NSLog(#"added with ID: %#", obbCD.obiettivoID);
obbCD = nil;
}
results = nil;
Take a look at the Apple documentation for inter-property validation. It describes how you can validate a particular insert or update operation while being able to consult the entire database.
You just have to check for an existing one :/
I just see nothing that core data really offers that helps with this. The constraints feature, as well as being broken, doesn't really do the job. In all real-world circumstances you simply need to, of course, check if one is there already and if so use that one (say, as the relation field of another item, of course). I just can't see any other approach.
To save anyone typing...
// you've download 100 new guys from the endpoint, and unwrapped the json
for guy in guys {
// guy.id uniquely identifies
let g = guy.id
let r = NSFetchRequest<NSFetchRequestResult>(entityName: "CD_Guy")
r.predicate = NSPredicate(format: "id == %d", g)
var found: [CD_Guy] = []
do {
let f = try core.container.viewContext.fetch(r) as! [CD_Guy]
if f.count > 0 { continue } // that's it. it exists already
}
catch {
print("basic db error. example, you had = instead of == in the pred above")
continue
}
CD_Guy.make(from: guy) // just populate the CD_Guy
save here: core.saveContext()
}
or save here: core.saveContext()
core is just your singleton, whatever holding your context and other stuff.
Note that in the example you can saveContext either each time there's a new one added, or, all at once afterwards.
(I find tables/collections draw so fast, in conjunction with CD, it's really irrelevant.)
(Don't forget about .privateQueueConcurrencyType )
Do note that this example DOES NOT show that you, basically, create the entity and write on another context, and you must use .privateQueueConcurrencyType You can't use the same context as your tables/collections .. the .viewContext .
let pmoc = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
pmoc.parent = core.container.viewContext
do { try pmoc.save() } catch { fatalError("doh \(error)")}