Editing/Adding a Core Data entity with the same view? - ios

First, let me explain what I'm trying to accomplish. I've got a master-detail application with a MasterViewController and a EditViewController. The MasterViewController contains an Add button and a table listing Core Data entities. When the user taps a table row or the Add button, the Edit view should pop up. I'm confused about how I should handle editing and adding differently.
Here's how I'm currently doing it: my app uses Storyboards, so I have editEntity and addEntity segues from Master to Edit. Both segues pass an entity to the EditViewController, but editEntity finds an existing entity based on the row tapped whereas addEntity creates a new one. Both segues set the isNew transient property on the entity.
The EditViewController doesn't know anything about Core Data--it simply edits the entity it's given. It in turn has done and cancel unwind actions. MasterViewController looks at the isNew property when considering cancel--if the entity is new, it deletes it, and if it already exists, it simply doesn't apply the changes.
This works, but it has a couple problems. Firstly, it seems a tad messy to add extra properties to the entity. Secondly, if the user closes the app on the Edit view while editing a new entity, that entity won't be deleted, which is certainly unexpected. Most of all, this seems like a problem that Core Data itself must have a solution to--I just don't know how. Thanks a bunch!

The simplest improvement would be to replace the isNew flag on the entity description with a flag on your edit view controller. The edit VC might not know anything about Core Data, but it's OK to let it know if the object it's editing is new or pre-existing. Set the flag there, and have the master VC check the value before deciding how to proceed. Don't put this in your entity description, it's not data you need to keep around.
What I've done in this situation is create the new instance but don't insert it in the managed object context yet. Something like
NSEntityDescription *entityDescription = [NSEntityDescription entityForName:#"Entity" inManagedObjectContext:[self managedObjectContext]];
NSManagedObject *myObj = [[NSManagedObject alloc] initWithEntity:entityDescription insertIntoManagedObjectContext:nil];
Passing nil for the second argument when creating the instance gives you an instance that hasn't been inserted yet. Pass that to the edit view controller.
If the user taps on the save button, you can insert it later with something like:
if ([myObj managedObjectContext] == nil) {
[[self managedObjectContext] insertObject:myObj];
}
Since the object hasn't been inserted, it has no managed object context, so checking that property tells you whether to insert it. Don't use the isInserted property here, it won't do what you need. Save changes in either case.
If the user taps "cancel", just don't insert it. The object gets deallocated just like any other object, and never makes it to the persistent store. Since you never inserted it, you don't need to bother deleting it.

Related

Core Data: A clever way to implement Add/Edit functionality

I've created a couple of Apps that use Core Data and a lot of experiments, but I've never found the "perfect" way to implement a simple Add/Edit viewController.
I just want to implement a single controller able to manage both the edit and add functionalities, I don't want to create two different Controllers.
At the moment I'm working whit this approach (let's take the classic Person NSManagedObject as example)
1) In the addEditViewController I add a currentPerson property
var currentPerson:Person?
2) When I present the controller in Add-Mode this property is nil
3) When I present the controller in Edit-Mode this property is a reference to the Person to edit
4) When I need to save user operations I just check if the currentPerson is set and I understand if I need to create a new object in the context or just save the one I need to edit.
Ok, this approach works but I want to follow another approach that seems to be more secure for the edit action. Check this terrible error!
Let's say that the person has Address property that needs a different viewController to be edited.
1) Following my previous logic I can pass the currentPerson property to the addressViewController that I'm going to present:
addressVC.currentPerson = currentPerson
presentAddressVC()
2) Now when the user has completed the edit operation and he/she taps on "save"
the addressVC calls the saveContext function.
Where is the problem? well... if the user starts editing the currentPerson in the addEditViewController an then just goes back to a previous controller, the currentPerson still stores the edit of the user and as soon as the context will be saved in any other controller the not-really-wanted data get stored and becomes persistent.
Probably I can perform a rollback in case the user taps the back button on the addEditViewController, but I really don't like this behaviour it seems so poor.
I think to work with multiple contexts or inserting NSManagedObjects in a nil context and just move them to the main context only at the end of the operations but I'm not sure about this choice too.
I know it's a kind of a complex and long (an tedious) question, but I hope you can give me some lights on this issue.
How to you treat this kind of situation? what do you think about my approach and about my proposed approaches?
It seems to me that your problem is maintaining a connection to a single NSManagedObjectContext when instead what you really want is to establish a tree. The construction of a context is fairly cheap so you should be creating a context per ViewController.
So when you show the addEdit controller you can simply create it with a new context:
NSManagedObjectContext *context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
context.mergePolicy = NSMergeByPropertyStoreTrumpMergePolicy;
context.parentContext = //parentContext
context.undoManager = nil;
Think of these new contexts as scratch pads for editing your managed objects. The only thing to bear in mind is that when you call save, it saves to the parent context and not all the way to the store. For that you will need a recursive call all the way to the parent for saves. Heres a basic recursive save:
- (void)saveChangesToDiskForManagedObjectContext:(NSManagedObjectContext *)context {
if (context && [context hasChanges]) {
[context save:nil];
NSManagedObjectContext *parentContext = context.parentContext;
[parentContext performBlock:^{
[self saveChangesToDiskForManagedObjectContext:parentContext];
}];
}
}
Its not really great practice to retain managed objects in an app where they could be deleted on other screens. So what you should do is perhaps fetch these objects in your view controllers view will appear method. Either that or call refreshObject(mergeChanges:) to synchronize your managed object with the changes made by another screen.
I really don't like the idea of calling save when the user navigates back, there should be a save button, when i press back I'm trying to close the screen, i would expect to select "done" if i want my changes saved.
Don't forget you can also use an NSUndoManager to track all your changes, thats why the context has an undoManager:)

Update NSManagedObject into Core Data

So I have a tabbed application. The first tab allows a user to enter information in ~20 fields that describe a NSManagedObject. They are then able to save this into core data, and that works just fine.
The second tab is a TableView of all of the existing submissions. Now when a user clicks on a cell in the TableView, it will open up the first tab and repopulate all of the fields that were originally saved into core data. When the user clicks save again, I want the existing submission in core data to be updated, instead of a new insertion into core data.
I have found a lot of information saying that I should make a fetch request and then update it like that. But that seems redundant to me because I already have the object that was saved passed to the first tab/ViewController.
If you could point me to some code that would help my situation or describe a way you might accomplish this scenario, I would greatly appreciate it!
Since you have a reference to the NSManagedObject in the first tab, you can update its properties to the new values when the user saves. You can then save the changes to your NSManagedObject (let's call it myObject for simplicity) by calling [[myObject managedObjectContext] save:&error] where error is an NSError *.

unattached (disconnect) Core Data entity from context

I'm using magical record for all my core data work.
everything works great, with the exception at times when I'm doing updates in the background I need to detach or disconnect the entity from the context.
for example
ButtonList = [Buttons MR_findAllSortedBy:#"listOrder" ascending:YES];
How would I keep the entity, but remove the reference to the context for the array ButtonList?
Thanks
This will only happen when you don't use a NSFetchedResultsController, or code that observe context changes and remove deleted objects from the UI to reflect the store state.
If you like the deleted objects to be removed from view as soon as your context finds out about the deletion, you would need to listen for "context did change notification" on your main context and look at the deleted objects set, if any of the deleted objects are part of your display array you will need to update your view accordingly (remove from array and update table. a NSFetchedResultsController also listen for context changes).
Another option:
Since you manage your tableview state by yourself (and not a fetched results controller) and
If you like the "buttons" to remain in view including their properties, you could:
Change your request to return dictionaries instead of managed objects (does not nullify on deletion):
NSFetchRequest* r = [Buttons MR_requestAllSortedBy:#"listOrder" ascending:YES];
[r setResultType:NSDictionaryResultType];
//This is your link to the data store and managed object (if you later need to fetch by or update if still exist)
NSExpressionDescription* objectIdDesc = [[NSExpressionDescription new] autorelease];
objectIdDesc.name = #"objectID";
objectIdDesc.expression = [NSExpression expressionForEvaluatedObject];
objectIdDesc.expressionResultType = NSObjectIDAttributeType;
[r setPropertiesToFetch:#[objectIdDesc,#"buttonName",#"buttonIcon"/*, and any other property you need for display*/]];
Now all is left to do is execute this request on any context you like (even in background) and get the array back to your table view controller.
The difference here is you get back dictionaries and not NSManagedObject array.
In Core Data there's no such concept of "detaching" an entity from a given context. You would have tons of problems in attempting to do something like that (e.g. move objects between contexts/persistent stores), especially when dealing with relationships.
I think you should refactor and design your application in a way that reacts proactively to any changes in the object(s) you are representing, e.g by removing the related table view cells (with NSFetchedResultsControllerDelegate callbacks), by automatically popping the detail view controller if present, etc...
I would not recommended recommend it but, if there are states for the object you're representing which are "transitory" and shouldn't be reflected in the UI, you could temporarily nil the NSFetchedResultsController delegate (I assume you're using that to display your managed objects) so that it doesn't receive updates and doesn't update the table view (for example). When the objects are ready to be displayed again, you can set the delegate back and perform a new fetch, so that the tableView gets updated (with automatic cell insertions, removals and updates).
If an object has been fetched from a managed object context, the only way to break its connection to the context is to delete the object and then save changes. There is no way to convert a fetched object into something that's not associated with its context. You could copy the data to some other object, but the one that you fetched is and will always be associated with the context that you got it from.
In your case, if you're deleting these objects in a background queue, you should not be using them any more. Or if you do need to use them, then you should not be deleting them. I can't tell from your question just what you're trying to accomplish here, but what you've described makes no sense.

How to delete a temporary object on a child managed object context?

I have a CodeData model [Phone]<<--->>[Forwarding]. So the Phone object has a Forwardings set, and vice versa.
I have a list of Phones and want to add one of them to a new Forwarding.
In the ForwardingViewController I do:
// Create a new managed object context; set its parent to the fetched results controller's context.
managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[managedObjectContext setParentContext:[fetchedResultsController managedObjectContext]];
self.forwarding = (ForwardingData*)[NSEntityDescription insertNewObjectForEntityForName:#"Forwarding"
So this creates a child MOC and a now temporary Forwarding.
Then I pass self->forwarding to my PhonesViewController which shows all Phones (in a table). This view controller is simply navigation-pushed.
When the user taps on one of the Phones in the table I do:
[self.forwarding addPhonesObject:phone];
The addPhonesObject is a CoreData generated accessor.
Now, when the user is back at the ForwardingViewController and taps the Cancel button (because he decides he does not want to create a new Forwarding after all), it is dismissed, which cleans up this child managedObjectContext and also self.forwarding.
After doing the above, I get a database error (Cocoa error 1550). When trying to understand the console output, my guess is that the Forwarding was indeed deleted, but that the Phone object (which of course is still there), now has a null reference to this deleted Forwarding.
My question. How should I handle this case correctly: Having a temporary object created on a child MOC, link it to another object (on the parent MOC), and then delete this temporary object again.
What is the actual error you are getting?
From your description, I am guessing that your PhonesViewController is listing phones from a different NSManagedObjectContext than the one that you created the ForwardingData entity from. This violates the relationship rule with Core Data. The rule is simple, to create a relationship between two entities they must both be from the same NSManagedObjectContext instance.
I question why you are creating a temporary NSManagedObjectContext in this situation. Since you are retaining the ForwardingData entity and you know when you are being cancelled, it seems cleaner to just delete the temporary entity when cancel is pressed instead of standing up another NSManagedObjectContext.
Update
If you need to use the child (per your comment), then you should change your PhonesViewController to accept a NSManagedObjectContext via dependency injection. Then you can send it the same NSManagedObjectContext instance as the one you used to create the new entity. With that change everything will work as you expect it to.

Core Data Object Injection (with Dependency) Storyboard

I'm trying to use segues for passing core data MOC and Entities to other View Controllers.
So I'm using the prepareForSegue method and doing something like this
SecondViewController *svc = (SecondViewController *)[segue destinationViewController];
//passing the current managed object context to the other view controller
svc.managedObjectContext = managedObjectContext
I then want the pass the currentEntity to the same view controller
//rootEntity is -- TheManagedObject * rootEntity in the second view controller
svc.rootEntity = currentEntity
I'm not sure if the above svc.rootEntity is the right way to do it but it feels like the right way to do it to inject the currentEntity in the next view controller.
In the Second View Controller I want to insert a new object for the entity based on the rootEntity injection above.
I know I would normally create a new Managed Object by doing this:
NSManagedObject *newObject = [NSEntityDescription insertNewObjectForEntityForName:#"TheNewObject" in ManagedObjectContext:managedObjectContext //MOC injected from the First View Controller
My issue is that I want to do the above newObject but I want it to be dependent (relationship) to the first passed entity (the above rootEntity).
I've come close but I keep getting unassociated NewObjects (should be one to many)
The next step would the be to repeat the above and insert another level in the next view controller based on the the NewObject in the second view Controller.
I've read Zarra's book and a few others but they all use init methods that don't seem to work with segues.
Thanks,
you are doing everything right. Once you are in your new view controller, just proceed as you would originally when inserting new entities and relationships. After all, you are referring to the same managed object context.
So for example, if you want to insert an new entity which is a relationship you would do something like this:
NSManagedObject *newObject = [NSEntityDescription
insertNewObjectForEntityForName:#"SubEntity"
inManagedObjectContext:managedObjectContext];
newObject.rootEntity = self.rootEntity;
The newObject of kind "SubEntity" is now associated to the rootObject.
I don't think storyboards or segues have anything to do with your problem.
Where is the code where you are establishing the relationship? You should be able to simply go
[self.rootEntity addNewObjectsObject:newObject];
or, simpler to do from the many end of the relationship:
newObject.rootEntity = self.rootEntity;
(note I have assumed the relationship names here).
As a bonus, you don't need to pass in the managed object context. You can obtain this from the rootEntity object- all managed objects have a reference to their context - rootEntity.managedObjectContext

Resources