Core Data and undo groups - ios

I'm working with Core Data for the first time and this has me stumped.
I have the following methods to handle grouping my changes and saving them:
- (void)beginUndoGrouping:(NSManagedObjectContext *)managedObjectContext {
NSLog(#"begin");
[managedObjectContext processPendingChanges];
[managedObjectContext.undoManager beginUndoGrouping];
}
- (void)endUndoGroupingAndSaveContext:(NSManagedObjectContext *)managedObjectContext
{
NSLog(#"end/save");
[managedObjectContext processPendingChanges];
[managedObjectContext.undoManager endUndoGrouping];
[self saveContext:managedObjectContext];
}
- (void)cancelUndoGrouping:(NSManagedObjectContext *)managedObjectContext {
NSLog(#"cancel");
[managedObjectContext processPendingChanges];
[managedObjectContext.undoManager endUndoGrouping];
[managedObjectContext.undoManager undoNestedGroup];
}
Aided by the NSLog statements I know this is the sequence of events:
start app in root view
enter list view
begin
leave list view
end/save
enter item detail view
enter category detail view
begin
touch Add Category button, which takes us to another view
begin
enter new data
touch Done button
end/save
touch Back to go back to item detail view
touch Cancel button
cancel
go back to list view
begin
At this point my new Category is gone and I don't understand why. It was wrapped in a group, which was ended and saved. Shouldn't it be immune from being rolled back at that point? I would have expected the cancel to only affect any changes made in the item detail view. And if the way it's behaving now is correct, then how do I make it behave the way I was expecting?
Any clarification would be appreciated!

The answer turned out to be that you need to use a second managed object context for the inner group.

Related

How to sort data in a Table View by the order in which they were saved?

I have a Table View in Xcode that is being populated by information that the user of the application is saving to iCloud. So my problem here is that whenever the user does save their data the data that is saved is being added to the every bottom of the table view of data instead of at the top which is where I want it to be seen once it has been saved.The data that is being saved is being saved to an array which then populates the Table View, but I am not sure how to sort the array due to the fact new data is being added to it every so often. Here is the code for the array in which i am working with:
- (NSArray *)notes
{
if (_notes) {
return _notes;
}
_notes = [[[NSUbiquitousKeyValueStore defaultStore] arrayForKey:#"AVAILABLE_NOTES"] mutableCopy];
if (!_notes) _notes = [NSMutableArray array];
return _notes;
}
and the data is being saved by this action here:
- (IBAction)save:(id)sender {
// Notify the previouse view to save the changes locally
[[NSNotificationCenter defaultCenter] postNotificationName:#"New Note" object:self userInfo:[NSDictionary dictionaryWithObject:self.finalScore.text forKey:#"Note"]];
[self dismissViewControllerAnimated:YES completion:nil];
}
Thanks!
You can store an object to the array that includes a date/time (along with your note data) - you can then sort the array by that value to get them in created order or reverse created order.
The other option might be since items are always added at the end, simply read the array in reverse order when displaying in the table. This may be helpful:
https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSArray_Class/NSArray.html#//apple_ref/occ/instm/NSArray/reverseObjectEnumerator
In fact, look at all the methods on NSArray - some really good and useful stuff there.
Hope this helps.

Core Data - many ways to add an object

i'm doing some testing of Core Data, let's say i have a mainViewController with a navigationBar and addButton.
Clicking on the addButton will open a detailViewController. When i press save to insert a new Object the detailVieController will close and show the table with the new data inserted.
I can think two different way to do that.
FIRST METHOD - Passing the ManagedObjectContext
In the action of the add button i create an instance of the new detailViewController and i pass the managedObjectContext to it. So will be the save button of the detailViewController that will take care of saving the context and then pop the controller.
This is the method called by the addButton in the MainViewController
-(void)addNewObject{
DetailViewController *detVC = [DetailViewController alloc]initWhit:self.managedObjectCOntext];
[self.navigationcontroller pushViewController:detVC animated:YES];
}
This method is called by the save button in the IngredientViewController
-(void)saveObject{
NSError *error;
if (![self.managedObjectContext save:&error]){
NSLog(#"Error");
}
}
SECOND METHOD - Using a delegate
In the action of addButton i create an instance of DetailViewController, i set it as delegate, so when i press the save button in the DetailViewCOntroller will call the delegate that will pass data to the main controller.
This is the method called by the addButton in the MainViewController
(void)addNewObject{
DetailViewController *detVC = [DetailViewController alloc]init];
detVC.delegate = self;
[self.navigationcontroller pushViewController:detVC animated:YES];
}
This method is called by the save button in the IngredientViewController
-(void)saveObject{
[self.delegate detailVCdidSaveObject];
}
This is the delegate implemented in the mainViewController
detailVCdidSaveObject{
NSError *error;
if (![self.managedObjectContext save:&error]){
NSLog(#"Error");
}
}
------------------------------ Passing the object
Is it best to pass raw data to the DetailViewController and create there the object or it's best to pass the instance of the object to DetailViewController that will take care of settin its data?
For Example
This way i link the object instance of the mainVC to the one DetailVC so i can easilly set its value
-(void)addObject{
DetailViewController *detailVC =[[DetailViewController alloc]init];
detailVC.delegate = self;
self.object = [NSEntityDescription insertNewObjectForEntityForName:#"Object" inManagedObjectContext:self.managedObjectContext];
detailVC.object = self.object;
[self.navigationController pushViewController:detailVC animated:YES];
}
this way i pass raw data and let the detailVC create the instance
-(void)addObject{
DetailViewController *detailVC =[[DetailViewController alloc]initWithName:#"objname"];
[self.navigationController pushViewController:detailVC animated:YES];
}
those code are just pseudocode for educational purpose. all ways works, i just want to know which do you think it's the most correct and why. thanks
I have used the first two methods and in my opinion they are both equally valid (though I personally prefer delegation). However, the third method caused problems if you give the user the option to cancel or go back in a navigation controller. If that happens, you will have an object that you never needed to create.
This sounds like a perfect use case for a NSFetchedResultsController. A NSFetchedResultsController is an object makes displaying data from core data in a UITableView a lot easier. It even tells you when the objects in core data matching a predicate change (insert, delete, update, move).
So the way I would do it is that MainViewController would have a NSFetchedResultsController that provides the data to the UITableView. When you press the add button, it would do what you have in the first method. The DetailViewController will create the new instance, set the values on it then save the managedObjectContext.
Since the MainViewController has the NSFetchedResultsController, it will automatically know that a new object have been created and it can update the UITableView to show it.
The NSFetchedResutsController documentation and the NSFetchedResutsControllerDelegate documentation show you exactly how to use it with a UITableView including code you can copy into your view controller that do the majority of the work.
The actual answer depends on your preference. In my project, I have implemented the first two methods. A definite No for the third method from my side because of same reasons as Kevin mentioned. If the user cancels the operation or some error occurs, then you will have to take care of removing the change (Perhaps write the following code in your didMoveToParentViewController method and cancel method):-
[self.managedObjectContext rollback]
Assuming of course that you do not have any other process modifying that managedObjectContext at the same time.
Now, I prefer the first two methods because :-
The first method allows me to write additional code in saveObject method. Lets say that you want to validate some properties before saving the object. These properties are only present in detailViewController. So, you cannot use a delegate in that situation without explicitly passing each and every property back to delegate function (which can get messy).
Now, assume that you are creating a object in your mainViewController and the detailViewController is only used to populate a field of the object that was created in mainViewController. In such a situation, I would use the delegate method and pass the field back to the mainViewController so that when the user saves the object in mainViewController, then the field values are saved along with it. If the user cancels mainViewController, then the field values are also not saved.

PFQueryTableViewController reload table when back btn pressed

Using Parse, after I'm logged in I am presented PFQueryTableViewController that displays a list of tasks and another detail view controller that allows me to edit the task detail and segue back. The issue right now is that the PFQueryTableViewController does not reflect the new changes after I finished editing and popping the task detail view off the stack. However the table view list does get updated when I go back to the login screen(view before the PFQueryTableViewController) and re-enter the table view again. I've tried the following:
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self.tableView reloadData];
}
and also
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self loadObjects];
[self.tableView reloadData];
}
Yet they don't seem to take effect. My guess is that the data is updated after the view is popped off and the table view appears. I'm just wondering if anyone has any insight on this while I'm investigating. Thanks!
You could try re-querying the queryForTable method in viewDidAppear (this would naturally use an API request on every view appearance however)
- (void)viewDidAppear:(BOOL)animated {
[self queryForTable];
}
This answer assumes that you are using Local Datastore and want to see changes made in it be reflected in a PFQueryTableViewController.
Because the ParseUI classes do not implement any form of caching though the local datastore, changes made in the detail view will not appear in the PFQueryTableViewController until the save operation has completed and the tableView has fetched the new items from Parse.
One solution to your problem would be adding a category to the PFQueryTableViewController that modifies how it fetches data to include what is in the Local Datastore as well.
You should make sure the data is saved before popping your view controller.
Use Parse's save method with completion handler.
[request saveInBackgroundWithBlock:^(BOOL succeeded, NSError *error) {
if (succeeded) {
[self.navigationController popViewControllerAnimated:YES];
}
}];
You can use [self loadObjects] to trigger a refresh for all objects in the PFQueryTableViewController.

Auto-save not working with NSUndoManager on UIManagedDocument

Resolution
NSUndoManager must only be used in a child NSManagedObjectContext (when used with Core Data). This is because the UIManagedDocument may auto-save at any point in time, after which an undo will have no effect. Therefore there is no point using NSUndoManager to just achieve save/cancel functionality, since a child context will give you the same result.
Bit sad really, because NSUndoManager is a lot easier to implement than a child context (for the latter I have to call existingObjectWithID to copy objects from the parent to the child - painful). Personally I would have thought the document should not auto-save if groupingLevel != 0. Rant finished.
Original Question
I have a table view controller that loads data using Core Data into a UIManagedDocument. It segues to a view controller to edit each row in the table. In that view controller I have cancel and save buttons. I am implementing the cancel capability using NSUndoManager through a category on my NSManaged object (self.list below).
- (void)viewDidLoad
{
[super viewDidLoad];
[self.list beginEdit];
}
- (IBAction)cancel:(id)sender
{
[self.list cancelEdit];
[self close];
}
- (IBAction)save:(id)sender
{
[self.list endEdit];
[self close];
}
The category implements beginEdit, endEdit and cancelEdit which is intended to handle the NSUndoManager stuff. In the code below, useUndo is a constant that I set to NO or YES to see the impact of using NSUndoManager.
- (void)beginEdit
{
if (useUndo)
{
NSUndoManager *undoManager = [[NSUndoManager alloc] init];
self.managedObjectContext.undoManager = undoManager;
[undoManager beginUndoGrouping];
}
}
- (void)endEdit
{
[self.managedObjectContext save:nil];
if (useUndo)
{
NSUndoManager *undoManager = self.managedObjectContext.undoManager;
[undoManager endUndoGrouping];
self.managedObjectContext.undoManager = nil;
}
}
- (void)cancelEdit
{
if (useUndo)
{
NSUndoManager *undoManager = self.managedObjectContext.undoManager;
[undoManager endUndoGrouping];
[undoManager undo];
}
}
I can see the Core Data debug messages showing it is committing the changes if I save an object and click the Home button when useUndo = NO. However, with useUndo = YES, it does not auto-save when I click on the Home button. I have waited a couple of minutes, and it still doesn't autosave. Is there some way I can force an auto-save?
Can anybody explain why using undoManager causes this change in behaviour?
I suspect either I am going about this the wrong way, or have some simple problem in the code. Any help would be appreciated.
I'm not sure if it's correct but other answers on stackoverflow have mentioned that an NSUndoManager clears the undo stack when the context saves. That means that using an undo manager with auto-save would at most be useful for a couple of seconds (whatever the auto-save interval is). There might be a connection there, I'm trying to find out more...

Master-Detail Application with Core Data

From Xcode 4.2 Master-Detail template (for iPad) with Core Data, I modified the data model and added additional text view objects to the nib file.
Code for moving data from managed object to interface objects is in ConfigureView in DetailViewController and it's working fine.
I'm now trying to auto save the interface object data to managed object data when I move from one item to another in the popover.
I added the code for save in viewWillDisappear in DetailViewController, but this doesn't seem to fire. Am I missing something?
- (void)configureView { // Update the user interface for the detail item.
if (self.detailItem) {
self.sname.text = [self.detailItem valueForKey:#"sname"];
self.saddress.text = [self.detailItem valueForKey:#"saddress"];
}
}
- (void)viewWillDisappear:(BOOL)animated {
[self.detailItem setValue: self.sname.text forKey:#"sname"];
[self.detailItem setValue: self.saddress.text forKey:#"saddress"];
NSError *error; if (![self.detailItem.managedObjectContext save:&error]) {
NSLog(#"Unresolved error %#, %#",error,[error userInfo]);
exit(-1); //fail
}
[super viewWillDisappear:animated];
}
First, in a MasterDetail application the detailViewController is usually always be visible and not disappear. So that is why viewWillDisappear is not being called. Of course I'm not sure about the particulars of your app architecture, so I may be wrong.
Secondly, consider the use case if the user changes some data then switches to another application. Then while in the other application, the system terminates your app. The changes that your user made will be lost and will run counter to what they expect.
Unless you are saving a lot of data for the interface in detailViewController, consider saving the data after the user changes data in the interface rather than when the user switches from managedObject to managedObject in the popoverViewController. i.e. when the user edits some data in a textView or textfield, perform a save on the managedObjectContext.
Good Luck!

Resources