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

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

Related

When to init the NSManagedObjectContext?

I have an iOS app with master and detail layout.
In master, I managed my own NSManagedObjectContext, and also detail is, by this way:
NSPersistentStoreCoordinator *psc = ((AppDelegate *)UIApplication.sharedApplication.delegate).persistentStoreCoordinator;
_context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_context setPersistentStoreCoordinator:psc];
In master, I display list that can be clicked by user to show the detail in detail layout.
Upon filling the detail by user, user can save the detail by clicking on button there.
However, I am trying to understand this:
Since there is a save button in detail, the save method will save the detail with detail's context and call the load list in master
Load list will remove all the NSMutableArray of the list [_activities removeAllObjects]; and re-fetch the data from Core Data and reload the tableview
Done that but the the re-fetch function seems to use old data and not the latest.
Why does re-fetch the data doesn't work if I use same context?
You are using outdated APIs to create your managed object contexts. After creating a context, instead of assigning the persistent store, you should set the parentContext.
If you display a "list", you should be using a context that is of type NSMainQueueConcurrencyType. The best way is a NSFetchedResultsController which will also help you manage memory and performance, and greatly simplify the updates you need for your UI. (You would avoid the verbosity and complexity of merging the data "manually" via NSManagedObjectContextDidSaveNotification.)
NB: Resetting the context is a crude and inefficient method for the task you are trying to accomplish. Due to the fact that it affects the entire context, it could change the behavior in other parts of your app.
You have two contexts going on here which is why the data is not being refreshed. If you call [context reset] then you will find that the data will refresh.
Explanation:
An NSManagedObjedctContext is a "scratchpad" which manages the Objective-C representation of SQLite (or XML) data in memory. When you fetch data the context retains the managed objects that are created by the fetch. If you then fetch from another context that context reads the data from the persistent store and creates new managed objects based on what it finds.
ALSO: When you perform a fetch the context checks to see if a managed object representation is ALREADY in existence and then returns it if it is. This means that two contexts can get out of sync quite quickly.
There are several ways around this:
1) Calling reset on a context returns it to it's "baseState" which means all the managed objects are "forgotten" or released by the context. Therefore, another fetch will force the context to read the data directly from the store.
2) Implementing the NSManagedObjectContextDidSaveNotification (see here) will allow you to incorporate changes between contexts.
I have found this article very useful in the past.

Force save NSManagedObject with NSUndoManager

I have an application listening to server events through a websocket. When the server sends a specific event, I create a Notification which is a subclass of NSManagedObject. I later display it in my main view controller.
In a view controller (let's call it ObjectViewController), I have this code :
- (void)viewDidLoad {
[super viewDidLoad];
[((AppDelegate *)UIApplication.sharedApplication.delegate).managedObjectContext.undoManager beginUndoGrouping];
}
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
AppDelegate * delegate = ((AppDelegate *)UIApplication.sharedApplication.delegate);
if (something) {
[delegate saveContext];
} else {
[delegate.managedObjectContext undo];
}
}
It allows me to undo all operation done to several NSManagedObjects of different types when I click the cancel button.
Now the problem is, when I am in this view and I receive a notification, the Notification object is removed from CoreData if I cancel the object changes.
Is there a way to force CoreData to save ONE notification while the other NSManagedObjects remain in the undo group ?
When you save a context, it saves everything in the context.
IMO, a better approach would be to use a separate NSManagedObjectContext as a "scratchpad".
Basically, your view controller will create its own context, either as a child of the main context, or connected directly to the main context's persistent store coordinator (if the latter, you need to merge saved changed).
This use case, however, is probably best served by creating a child context.
This way, your "editing context" is separate from the main context. When the view controller goes away, you can save the context, or simply do nothing and let it dealloc.
Thus, you can still have changes going on in your "main context" and anything done in the "editing context" only happens if you choose to save the context.
You can then just not even use the undo manager, as the temporary context is doing that work.
EDIT
After looking at the apple doc, to create a new context as a child of
the main context, I just have to set its parentContext attribute ? I
don't know how I lived without knowing this... so useful ! – Sunder
To create it, yes, that's all you have to do. There are some drawbacks to using it, but there are usually corner cases. As long as you are not creating new objects in the child context and passing their object-ids to another MOC, you should be fine.
Simply make your changes, and if you want to share them with the parent, just save to the parent. Note, however, that saving from a child to a parent just "copies" the object changes into the parent. The parent context must still save its context for the changes to make it to the store.

Keeping a CoreData NSManagedObject fresh

Imagine a social photo app like Instagram. You see one of your friend's photos in your feed. That photo is persisted in CoreData as something like this:
Photo (NSManagedObject)
Attributes: id, imageURL
Relationships: comments (set of Comment objects), likes (set of Like objects)
I have a view controller that has a reference to the Photo object. This view controller also handles actions for liking and commenting on the photo.
So, when someone likes a photo the flow is this: Send the like to the server. If the API post is successful, update the Photo object in CoreData with any new information received from the server, which will include the new like. At this point the Photo should have one more Like object related to it in CoreData than before I pressed the like button.
Now here is where I'm having a problem. The view controller needs to update the number of likes on the photo. In a success block, I'm doing this:
self.likesLabel.text = [NSString stringWithFormat:#"%d likes", self.photo.likes.count];
The problem is that self.photo.likes.count is reporting 0 at this point (it was non-zero before I pressed the like button). If I navigate elsewhere and come back to this screen, the count will update properly. So it seems that the Photo managedObject becomes "dirty" when I update it. The update probably happens in another context (I use Magical Record). The update looks something like this:
NSManagedObjectContext* context = [NSManagedObjectContext MR_contextForCurrentThread];
Photo* photo = [Photo MR_findFirstWithPredicate:somePredicate inContext:context];
...
photo.likes = likes;
photo.comments = comments;
[photo save:context];
So the question is, how do I properly keep the Photo object updated in the view controller so that I can always reliably query the number of likes and comments? In general, how can you always keep a fresh version of an NSMangagedObject, given that it may be updated in other threads/contexts?
I have tried listening for the NSManagedObjectContextObjectsDidChangeNotification, but I run into the same problem where the Photo reports 0 likes and comments when I get that notification.
One solution for this scenario I frequently use is KVO. In your case, I would observe the "likes" property on the managed object. You can look up standard KVO articles on how to do that properly. However, this is only half the story. So, in order for those values to change for you properly, the NSManagedObjectContext that is "managing" that property needs to get updates itself. This is where either merging saves on NSManagedOjbectContextDidSaveNotification events in the case of a thread isolation context, or the parent/child merge rules come into play. The point is, you could be making the change to the actual data on any context within your app. It's up to you to merge in those changes into the context you currently care about (probably what's displaying on the screen).
All that said, MagicalRecord should have some build in methods to already handle that half of the problem for you. If not, make sure your context that is displaying use information is being updated properly when a background context is being saved. The thing to remember is that you have to update the context, and once the context merges in the new changes on update, then your KVO methods will fire with the proper values.
listen and apply merges made in other contexts
#pragma mark - Core Data stack
- (void)mergeChanges:(NSNotification *)notification {
// Merge changes into the main context on the main thread
dispatch_async(dispatch_get_main_queue(), ^{
[self.mainManagedObjectContext mergeChangesFromContextDidSaveNotification:notification];
[[NSNotificationCenter defaultCenter] postNotificationName:MyMainContextMergedChangesNotification object:nil];
});
}
after the merge you can refresh the GUI
alternativly/additionally you can force the context to refresh the object from the db calling refreshObject:

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

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.

iOS - Core data - NSManagedObjectContext - not sure if it is saved

Overview
I have an iOS project in which I am using Core data
I am inserting an object, then I want to save it.
I am not sure if save works.
Save seems to be working when app goes into background
When using Simulator, If I click on Stop button on Xcode, save doesn't seem to be working.
Question
Is the save actually happening ?
Am I facing a problem because I created a view based app (the core data checkbox was not available) ?
Steps Followed
I am using the simulator to test it.
Insert an object (code is in the next section)
Save the inserted object (code is in the next section)
I press the Stop button on Xcode to stop running the app
Output noticed
setBeforeSave.count = 1
setAfterSave.count = 0
Before saving, The NSManagedObjectContext method insertedObjects returns 1 object
Before saving, The NSManagedObjectContext method insertedObjects returns 0 objects
When Xcode Stop button is pressed, and when the app is relaunched, the previous data is not available (is it because I clicked on stop on xcode)
managedObjectContext is NOT nil
The NSManagedObjectContext method save: returns YES.
Code to Insert Object
Test *test = [NSEntityDescription insertNewObjectForEntityForName:#"Test" inManagedObjectContext:self.database.managedObjectContext];
Code to Save:
//database is a property of the type UIManagedDocument
NSSet *setBeforeSave = [self.database.managedObjectContext insertedObjects];
NSLog(#"setBeforeSave.count = %i", setBeforeSave.count);
NSError *error = nil;
if(![self.database.managedObjectContext save:&error])
NSLog(#"error = %#", error);
NSSet *setAfterSave = [self.database.managedObjectContext insertedObjects];
NSLog(#"setAfterSave.count = %i", setAfterSave.count);
According to the UIManagedDocument documentation, you should not call save on either of the internal managed contexts. Instead, if you want data saved, you should do one of two things.
Use the undoManager, as it will mark the context dirty, and ready to be saved.
Call [document updateChangeCount:UIDocumentChangeDone];
Thus, in your case, you should replace that save call with:
[self.database updateChangeCount:UIDocumentChangeDone];
And your data will get saved.
EDIT
To provide additional detail. A UIManagedDocument has two MOCs., in a parent/child relationship. The child is the one you get when calling document.managedObjectContext. Now, when a NSManagedObjectContext has a parent, the normal way to propagate changes to the parent is to call save:. However, the UIManagedDocuememt does other stuff, and its documentation specifically says NOT to call save on either the parent or child context.
Well, how does stuff get saved, then? Well, you tell the UIManagedDocument that it is "dirty" and needs to be saved. The two ways you can do that are by either using the undoManager, or calling updateChangeCount:.
When doing either of those, the internals of UIManagedDocument will make sure that the parent context is notified of the change. At some point in the future, the parent will effect the change to the actual backing store (i.e., file(s) on disk).
Furthermore, when a context is "saved" it may or may not keep references to the objects that were changed. You can set a property which tells it to retain objects that have been saved, or to release them.
Hopefully, that addresses your problems.
to summarize, though, see the original answer.
BTW, you can actually see a log of what the SQL store is doing underneath by adding "-com.apple.CoreData.SQLDebug 1" to your command line arguments. You do that in the "Edit Scheme" dialog.

Resources