Use saved item in save completion Magical Record - core-data

I would like to use the item that has been just been saved in the completion block of Magical Record saveWithBlock method. For example:
//Get the ID of an existing NSManagedObject to use in the save block (if it exists)
NSManagedObjectID *objectRef = [self.object objectID];
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext){
//This method either loads an existing object and makes changes or creates a new entity in localContext
NSManagedObject *itemToSave = [self prepareItemInContext:localContext WithID: objectRef];
} completion:^(BOOL success, NSError *error) {
if (success) {
//here I want to get at the object 'itemToSave' that was either created in the save block (with a new objectID) or updated (with the ID objectRef)

Well, you need to have a reference to your external context to load the object with that ID:
NSManagedObjectContext *outsideContext = //...
NSManagedObjectID *objectID = //...
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext) {
} completion:^(BOOL success, NSError *error) {
NSManagedObject *newlySavedObject = [outsideContext existingObjectWithID:objectID];
//...do stuff here
}];
Generally, however, I would discourage this usage. I would instead recommend keeping any predicates or means of reloading your data set handy, and dump and refetch fresh data from the store. This will give you proper object references. Another, more precise way of updating objects in other contexts is to listen to the NSManagedObjectContextDidSaveNotification and merge this updates into your context. From there, your data will be "refreshed" and as long as you're KVO'ing a property, or using a NSFetchedResultsController with a delegate, your updates will propagate to the UI (or other destination).

Either just use self.object or, if you create a new object and insert it (presumably because objectRef is nil) then you should get the corresponding new object from the main thread context and use that.
How you do that shuffle is the interesting part. It isn't exactly clear why you're using a background context at the moment so you can also consider changing that, which removes all of the complexity.
If you need to keep the background context then you need to decide on how to get that data back to the main thread. Generally, you could use performBlockAndWait: inside your current block to get the new object from the main context and then store it into a property on your class so you can use it in the completion block. This would be setting the self.object property.

Related

What does it take for NSArrayController subclass to add Core Data objects?

Background: I built an app w/ inexperience. It uses bindings, a NSArrayController subclass and Core Data/iCloud. The experience level caused a lot of code unnecessary writing, yet the app worked ok. A button linked to the NSArrayController add: method triggered newObject and addObject:. Removing never worked and led me to look at contentArray binding . A week ago, I began cleaning the app to more thoroughly use the simplicity of bindings. (Unfortunately, complexity also entered into this problem.)
Assumptions: 1) add: and remove: methods would automagically fire the methods to create and delete objects. Many simple tutorials seem to work this way. 2) The add:, addObject:, remove: and removeObject: methods do not need to be overridden.
Problem: addObject: and removeObject: do not get called (after some point of change) unless they are explicitly called in the add: or remove: method. I have never had the functionality of deleting both the array controller and managed object with a simple click on a remove button.
Question: What is required for the Core Data managed objects to be added and removed?
Other info: The array controller pictured below has Custom Class set to CheckinArrayController. The Core Data stack is initialized in AppDelegate. The newObject method has been overridden to preset some attributes.
EDIT: Override in array controller subclass add:, newObject, addObject:, and arrangeObjects:
Code in Array Controller (NSArrayController subclass)
#implementation CheckinArrayController
- (id)newObject {
id newObject = [super newObject];
// do object set up here ....
return newObject;
}
- (void)add:(id)sender {
/* without the next 2 no object is added, although this is called
NSManagedObject *newItem = [self newObject];
[self addObject:newItem]; */
}
- (void)addObject:(id)object {
[super addObject:object];
}
- (void)remove:(id)sender {
[super remove:sender];
}
- (void)removeObject:(id)object {
[super removeObject:object];
}
#end
Current array controller connections:
I would suggest creating a convenience init in your ManagedObject class. Here is another SO question that outlines this:
Designated Init for Managed Object
Call this init method when you want to create a new Managed Object. Your arraycontroller has a managedObjectContext from the bindings you have set up.
I use it like this:
let entity = NSEntityDescription.entity(forEntityName: "yourEntityName", in: managedObjectContext!)
let object = YourManagedObjectClassName(property1: "property1 entity: entity!, insertIntoManagedObjectContext: managedObjectContext)
I use the stock removeObject methods in the NSArrayController base class to delete objects.
removeObject Documentation

A core data model object deleted in one MOC can still be used in another

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 :)

Importing with MagicalRecord + AFNetworking

I'm using AFNetworking and MagicalRecord (the current develop branch) and I'm trying to figure out how to import a lot of objects which are dependent on each other. Each resource/entity has multiple pages worth of downloads. I have a class managing the downloads for a given entity and saving them using MagicalDataImport (which has been amazing).
I believe my issue is that the imports aren't happening on the same thread. So I think what is happening is:
In one thread, EntityA is getting saved properly and propagated to the parent entity.
Then in another thread, EntityB is being saved, and along with it it's relationship to EntityA is built. That means a blank (fault?) object is being created. Then when it gets propagated to the parent entity, I believe EntityA is overwriting the EntityA that is there. Thus I'm left with some objects that don't have all of the attributes.
At least, I think that is what is happening. What I'm seeing via the UI is actually that the relationships between entities aren't always built correctly.
My end goal is to get the entire download/import process to be done in the background, not effecting the UI at all.
Here is my AFJSONRequest:
AFJSONRequestOperation *operation = [AFJSONRequestOperation
JSONRequestOperationWithRequest:request
success:^(NSURLRequest *request, NSHTTPURLResponse *response, id JSON)
{
[self saveResources:[JSON objectForKey:#"data"]];
}
failure:^(NSURLRequest *request, NSHTTPURLResponse *response, NSError *error, id JSON)
{
DLog(#"%#",error.userInfo);
[self.webService command:self didFail:error.localizedDescription];
}];
[operation setQueuePriority:self.priority];
And it calls saveResources::
- (void)saveResources:(NSArray*)resources {
BOOL stopDownloads = [self stopDownloadsBasedOnDate:resources];
if ([resources count] > 0 && !stopDownloads){
self.offset = #([offset intValue] + [resources count]);
[self send];
}
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *blockLocalContext) {
[self.classRef MR_importFromArray:resources inContext:blockLocalContext];
} completion:^(BOOL success, NSError *error) {
if (error){
// ... handle errors
}
else {
// ... handle callbacks
}
}];
}
This kicks off another download ([self send]) and then saves the objects.
I know by default AFNetworking calls the callback in the main queue, and I've tried setting the SuccessCallbackQueue/FailureCallbackQueue to my background thread, but that doesn't seem to solve all the issues, I still have some relationships going to faulted objects, though I think I do need to do that to keep everything going in a background thread.
Is there anything else I need to call in order to properly propagate these changes to the main context? Or is there a different way I need to set this up in order to make sure that all the objects are saved correctly and the relationships are properly built?
Update
I've rewritten the issue to try to give more clarification to the issues.
Update
If you need more code I created a gist with (I believe) everything.
I ended up having this exact same issue a few days ago. My issue was I had received a customer record from my API with AFNetworking. That customer could have pets, but at this point I didn't have the petTypes to correspond to the customers pet record.
What I did to resolve this was create a transformable attribute with an NSArray which would temporarly store my pets until my petTypes were imported. Upon the importation of petTypes I then triggered an NSNotificationCenter postNotification (or you can just do the pet import in the completion).
I enumerated through the temporary transformable attribute that stored my pet records and then associated the with the petType
Also I see you are doing your import inside of a save handler. This is not needed. Doing your MR_importFromArray will save automatically. If you are not using an MR_import method then you would use the saveToPersistentStore methods.
One thing is I don't see where you are associating the relationships. Is EntityB's relationship to EntityA being sent over via JSON with the EntityA objecting being in EntityB?
If so then this is where the relationship is getting messed up as it is creating / overwriting the existing EntityA for the one provided in EntityB. My recommendation would be to do something like this.
NSArray *petFactors = [responseObject valueForKeyPath:#"details.items"];
NSManagedObjectContext *currentContext = [NSManagedObjectContext MR_context];
Pets *pet = [Pets MR_findFirstByAttribute:#"id" withValue:petId inContext:currentContext];
pet.petFactors = nil;
for (id factor in petFactors) {
[pet addPetFactorsObject:[PetFactors MR_findFirstByAttribute:#"id" withValue:[factor valueForKey:#"factorId"]]];
}
[currentContext MR_saveToPersistentStoreWithCompletion:^(BOOL success, NSError *error) {
if (success) {
NSLog(#"SAVED PET FACTORS");
[[NSNotificationCenter defaultCenter] postNotificationName:kPetFactorsSavedSuccessfully object:nil];
} else {
NSLog(#"Error: %#", error);
}
}];
I'm putting this as an answer, though I'm not 100% sure if this is your issue or not. I think the issue stems from your localContext. Here is a sample web request method from an app we wrote that uses data importing, you may be able to use it as an example to get yours working.
Note that the AFNetworking performs its completion block on the main thread, then the MagicalRecord saveInBackground method switches back to a background thread to do the importing and processing, then the final MR completion block performs the handler block on the main thread again. The localContext that's used to import is created/managed by the saveInBackground method. Once that method is complete the context is saved and merged with the app's main context and all the data can then be accessed.
- (void)listWithCompletionHandler:(void (^)(BOOL success))handler{
[[MyAPIClient sharedClient] getPath:#"list.json" parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject){
NSString *statusString = [responseObject objectForKey:#"status"];
// Handle an error response
if(![statusString isKindOfClass:[NSString class]] || ![statusString isEqualToString:#"success"]){
// Request failure
NSLog(#"List Request Error: %#", statusString);
NSLog(#"%#", [responseObject objectForKey:#"message"]);
if(handler)
handler(NO);
return;
}
NSArray *itemsArray = [responseObject objectForKey:#"items"];
[MagicalRecord saveInBackgroundWithBlock:^(NSManagedObjectContext *localContext){
// Load into internal database
NSArray *fetchedItems = [Item importFromArray:itemsArray inContext:localContext];
NSLog(#"Loaded %d Items", [fetchedItems count]);
} completion:^{
if(handler)
handler(YES);
}];
} failure:^(AFHTTPRequestOperation *operation, NSError *error){
NSLog(#"Fail: %#", error);
if(handler)
handler(NO);
}];
}

Getting indexPath of an object in FetchedResultsController

I am creating a table view controller for an app that manages position assignments for a team. The sections with headers for defense, center, and offense for example will have names in them if a position Entity exists with that positionProperty. If that person is removed though through swiping, they become an alternate entity with same positionProperty.
I am trying to have the alternates for each position display when the edit button is tapped. Much like extra contact details appear when you edit a contact in the contacts app.
I have a fetchedResultsController returning the parent entity for alts/positions keyed by the positionProperty to define sections. (This may be the wrong way to do this... I am new to Core Data).
In setEditing:WithAnimation I have done the following. Attempting to search my fetched results and if any objects are of type Alternate, display that row. So in the enumeration, if it is type alternate I tried to call IndexPathForObjects:alt. This just returned nil...
if(editing){
[self.tableView beginUpdates];
for (MCAlternate *alt in fetchedResultsController.fetchedObjects) {
if ([alt isKindOfClass:[MCAlternate class]]) {
NSLog(#"The alternate is: %#", alt);
// This is where the error is trying to get indexPathForObject
NSIndexPath *index = [fetchedResultsController indexPathForObject:alt];
[self.tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:[index] withRowAnimation:UITableViewRowAnimationLeft];
}
}
[self.tableView endUpdates];
}
I have checked if the object exists in the results. It does. I have also tried called the getindexpath in a place with the object was just created by calling objectAtIndexPath and it still came back nil.
any suggestions are appreciated, Thanks!
Maybe you are not using your fetched results controller as it is foreseen. Your frc should fetch all the data to populate the table view.
You can still achieve the dynamic table content by tweaking your table view datasource methods. Suppose you are fetching the entity "Position" and the position object in question has a to-many attribute of entity "MCAlternate" called "alternates". You would then expand a certain section as follows:
-(void)expandSectionForPosition:(Position *)position {
int row = 0;
int section = [[frc indexPathForObject:position] section];
for (MCAlternate *alt in position.alternates) {
// update your datasource - e.g. by marking the position as
// "expanded"; make sure your numberOfRowsInSection reflects that
[_tableView insertRowsAtIndexPaths:#[[NSIndexPath
indexPathForRow:i++ inSection:section]]
withRowAnimation:UITableViewRowAnimationLeft];
}
}
Your class check is not logical - you are casting as MCAlternate in the for loop anyway. Your call or getting the index path fails, because you typically do not have a separate fetched results controller.

Fill CoreData ManagedObject property based on another object property

I have app that stores tree structure in CoreData.
There is an ManagedObject, "Item", and it has attributes:
itemId (string)
List item
title (string)
parentId (string)
parent (relationship to Item)
parentTitle (string)
parentId points to another Item object.
How do I make property parentTitle to be filled automatically with title of parent Item ?
While Martin's suggestion is a good solution for derived values, my question on yours is, why would you want this? You are not manipulating the value from the parent at all, ever. Since you are just accessing it, access the parent directly via KVC such as:
Item *item = ...;
NSString *title = [item valueForKeyPath:#"parent.title"];
//Do something with title
The only time you would want to use the keyPathsForValues... functionality is if you are changing something based on that value. If you are just accessing it, use KVC directly.
This is a possibility to achieve the desired functionality:
// implement in Item.m
// manages KVO notifications
+ (NSSet *)keyPathsForValuesAffectingParentTitle
{
return [NSSet setWithObjects:#"parent.title", nil];
}
// getter for parentTitle
- (NSString*) parentTitle
{
return [self valueForKeyPath:#"parent.title"];
}
additionally declare the property for parentTitle as readonly in Item.h
There is no need to declare a Core Data attribute "parentTitle".
The only problem I see with this solution is the following:
Item A is parent of item B
A gets turned into fault
B is still active and some View is bound to B.parentTitle
The view gets a notification because of the dependency declared with keyPathsForValuesAffecting, still object A is already faulted (and on shutdown unable to be unfaulted again) does Core Data manage such faulting&observation problems automatically?

Resources