Undo Management with Core Data - iphone

I'm trying to implement undo support using Core Data on the iPhone and I ran into a few problems.
I currently have a couple of managed objects set up but when I make changes to their properties, these changes don't get recorded by the undo manager. From my understanding, Core Data is supposed to have this automatically set up and I should be able to have basic undo and redo support for changes, creation and deletion of managed objects.
Is there special way of making changes to the objects so that they get recorded by the undo manager? Or should I be registering undo actions for each change?
Also, suppose the application slides into a detailed view for editing a specific object. I would like to be able to undo all changes made when say, the cancel button is hit. Would undo grouping be applicable here? What is the difference between committing a group and have another undo manager manage the finer actions in the detailed view versus using just having one undo manager (the one included with the managed object context)?
Thanks!

While the undo features will work pretty much out of the box, you do need to allocate an NSUndoManager for the NSManagedObjectContext for which you want undo support.
The easiest way to do this is to set up the undo support when something asks your appDelegate for the NSManagedObjectContext
This is the default method that apple gives you:
- (NSManagedObjectContext *) managedObjectContext {
if (managedObjectContext != nil) {
return managedObjectContext;
}
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil) {
managedObjectContext = [[NSManagedObjectContext alloc] init];
[managedObjectContext setPersistentStoreCoordinator: coordinator];
}
return managedObjectContext;
}
Modify it to look like this:
- (NSManagedObjectContext *) managedObjectContext {
if (managedObjectContext != nil) {
return managedObjectContext;
}
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil) {
managedObjectContext = [[NSManagedObjectContext alloc] init];
//Undo Support
NSUndoManager *anUndoManager = [[NSUndoManager alloc] init];
[managedObjectContext setUndoManager:anUndoManager];
[anUndoManager release];
[managedObjectContext setPersistentStoreCoordinator: coordinator];
}
return managedObjectContext;
}

Related

Using core data with and with out a nsfetchedResultsController

In core data, you initially add objects/set their attributes values using:
-(IBAction)save{
if (self.managedObjectContext == nil)
{
self.managedObjectContext = [(RootAppDelegate *)[[UIApplication sharedApplication] delegate] managedObjectContext];
}
Frame *f = [NSEntityDescription insertNewObjectForEntityForName:#"Frame" inManagedObjectContext:self.managedObjectContext];
f.typeLabel = self.textFieldtext.text;
[self dismissViewControllerAnimated:YES completion:nil];
NSError *error;
if (![self.managedObjectContext save:&error]) {
NSLog(#"Error");
}
}
And you would typically edit the values using:
-(IBAction)save{
[self.f setValue:self.newTextfield.text forKey:#"typeLabel"];
[self dismissViewControllerAnimated:YES completion:nil];
NSError *error;
if (![self.managedObjectContext save:&error]) {
//Handle Error
}
}
It's obviously a bit different using A NSFetchedResultsController
I guess my question would be, how can I set properties and edit them using a NSFetchedResultsController?
A fetched results controller acts as a link between a fetch request and a table view. The useful part is that if you make any changes to the managed object context that would affect the results of the fetch request, the FRC automatically picks up on these and sends various delegate methods which you can tie in to your table view datasource code to keep the table up to date. See "Implementing the Table View Datasource Methods" here.
Your code above isn't really relevant to this, unless it is contained within a modal view controller that is called from a table view displaying the results of a fetch request, and is used for adding new items. In that case, the code above would be identical, but when you returned to the table view, it would already contain your new data.

How to ensure NSManagedObjectContext when opened asynchronously through UIManagedDocument

I have an application with different controllers that all operate on the same NSManagedObjectContext.
My approach was to initialize the NSManagedObjectContext in my AppDelegate and inject it into all the controllers.
I am initializing my NSManagedObjectContext by opening a UIManagedDocument like this:
UIManagedDocument* databaseDoc = [[UIManagedDocument alloc] initWithFileURL:url];
if (![[NSFileManager defaultManager] fileExistsAtPath:[databaseDoc.fileURL path]]) {
[databaseDoc saveToURL:databaseDoc.fileURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {
myController.managedObjectContext = databaseDoc.managedObjectContext;
}];
} else if (databaseDoc.documentState == UIDocumentStateClosed) {
[databaseDoc openWithCompletionHandler:^(BOOL success) {
myController.managedObjectContext = databaseDoc.managedObjectContext;
}];
} else if (databaseDoc.documentState == UIDocumentStateNormal){
myController.managedObjectContext = databaseDoc.managedObjectContext;
}
Now my problem is, that opening the UIManagedDocument happens asynchronously and the NSManagedObjectContext is only available in the completion block.
How do I ensure that the controllers always have a valid NSManagedObjectContext to work with? Of course the problems happen at startup i.e. when a controller wants to use the NSManagedObjectContext in his "viewDidLoad" method, and the completion block in the AppDelegate has not yet run ...
One approach would probably be to "wait" in the AppDelegate until the UIDocument has opened, but as far as I gather this is not recommended ...
I would like to avoid to "pollute" my controllers with code that deals with the asynchronous nature of opening a NSManagedObjectContext... but maybe this is a naive wish?
In your appDelegate:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
MyWaitViewController* waitController = [[MyWaitViewController new] autorelease];
self.window.rootViewController = waitController;
// then somewheres else, when you get your context
[databaseDoc saveToURL:databaseDoc.fileURL forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {
myContextController.managedObjectContext = databaseDoc.managedObjectContext;
self.window.rootViewController = myContextController;
// note that at this point when the viewDidLoad method will get called
// it will have his managedObjectContext and his view already available.
// you can change your rootController, or push another viewController into the
// stack. Depending on what u want from the GUI side
}];
return YES;
}
Note that you dispose the GUI logic into the MyWaitViewController + AppDelegate side. But you keep your "myContextController" away from that logic control, since he get called / created only when a context exist.
I was struggling with the same issue, and I came up with it by using NSNotificationCenter.
When initializing your NSManagedObjectContext in the success handler, add send a notification.
Then, add a listener to to the viewDidLoad of whatever your first ViewController is.
I used that listener to call a reloadData method. In a heavy app, this could be a problem, as the viewcontroller loads blank, and then reloads the data, but this is a lite one, and it's noticeable at all - the viewController loads instantaneously with the managedObjectContext.

Edit and Delete Existing EKEvent?

I am using the Kal calendar in my app, (hopefully that doesn't change too much) but I get an EKEvent object from that depending on the user selection on the calendar.
Anyway, how can I edit and delete an event which already exists? Namely the EKEvent that I receive?
I need to do this all programatically, I am not using any of Apple's pre-made EKEventViewController's.
I can successfully create and save new events, but Im unsure of how to edit or delete existing ones, any help would be appreciated, thanks.
A complete answer would almost require a demo project.
Other approach would be simply giving you a link to Event Kit Programming Guide.
Since you did not provide any code (what you have tried already) i hope this extract from a working app will push you to the right track.
Note that i sublassed EKEventViewController due to app's specifics - this is not neccessary. I had to sublass it simply because original EKEventViewController
didn't spawn with black navigationBar (this was reported as a bug also, don't now if it's
fixed already).
You know how to add an event to calendar, so there's no need to write about getting a reference to EKEventStore and EKCalendar.
You're also not asking about how to retreive an event from calendar so let's assume you already have some kind of mechanism to select (receive) the event and you want to edit it. Let's say it is:
EKEvent *selectedEvent = (EKEvent *)[events objectAtIndex: selectedIndex];
I create this viewController as a property of appDelegate - you probably have better solution. appDelegate also holds eventStore and defaultCalendar reference - your approach could differ.
appDelegate.detailViewController = [[MPEventViewController alloc] initWithNibName:nil bundle:nil];
appDelegate.detailViewController.event = selectedEvent;
appDelegate.detailViewController.eventStore = appDelegate.eventStore;
appDelegate.detailViewController.defaultCalendar = appDelegate.defaultCalendar;
appDelegate.detailViewController.allowsEditing = NO;
[appDelegate.navigationController pushViewController:appDelegate.detailViewController animated:YES];
Sublcassing (again, this is not neccessary but it might come useful) goes like this:
MPEventEditViewController.h
#import <Foundation/Foundation.h>
#import <EventKitUI/EventKitUI.h>
#interface MPEventViewController : EKEventViewController <EKEventEditViewDelegate>
#property (nonatomic, strong) EKEventStore *eventStore;
#property (nonatomic, strong) EKCalendar *defaultCalendar;
- (void)editEvent:(id)sender;
#end
MPEventEditViewController.m
#import "MPEventViewController.h"
#import "----------AppDelegate.h"
#implementation MPEventViewController
#synthesize eventStore;
#synthesize defaultCalendar;
- (void)viewDidLoad {
[super viewDidLoad];
[self setTitle: [self.event title]];
self.allowsEditing = NO;
self.navigationController.navigationBar.barStyle = UIBarStyleBlack;
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:
UIBarButtonSystemItemEdit target:self action:#selector(editEvent:)];
//this has nothing to do with the answer :)
//[[self.navigationController navigationBar] setTintColor: [UIColor colorWithHexString: NAVBAR_TINT_COLOR]];
}
- (void)editEvent:(id)sender {
EKEventEditViewController *addController = [[EKEventEditViewController alloc] initWithNibName:nil bundle:nil];
//this has nothing to do with the answer :)
//[addController.navigationBar setTintColor: [UIColor colorWithHexString: NAVBAR_TINT_COLOR]];
addController.eventStore = self.eventStore;
addController.event = self.event;
addController.navigationBar.barStyle = UIBarStyleBlack;
addController.editViewDelegate = self;
[self presentModalViewController:addController animated:YES];
}
- (void)eventEditViewController:(EKEventEditViewController *)controller
didCompleteWithAction:(EKEventEditViewAction)action {
NSError *error = nil;
EKEvent *thisEvent = controller.event;
switch (action) {
case EKEventEditViewActionCanceled:
break;
case EKEventEditViewActionSaved:
[controller.eventStore saveEvent:controller.event span: EKSpanFutureEvents error:&error];
break;
case EKEventEditViewActionDeleted:
[controller.eventStore removeEvent:thisEvent span: EKSpanFutureEvents error:&error];
break;
default:
break;
}
//here would be the place to reload data if you hold it in some kind of UITableView
[controller dismissModalViewControllerAnimated:YES];
}
- (EKCalendar *)eventEditViewControllerDefaultCalendarForNewEvents:(EKEventEditViewController *)controller {
EKCalendar *calendarForEdit = self.defaultCalendar;
return calendarForEdit;
}
- (void)dealloc {
eventStore = nil;
defaultCalendar = nil;
}
#end
Only after writing all this i remembered there is a great sample code SimpleEKDemo. In fact some of this posted code is probably originates from there.
EDIT:
After the question was edited the above answer became off-topic.
In that case you should take a look at EKCalendarItem and EKevent.
You can change allmost all properties programatically (most of them are inherited from EKCalendarItem).
Maybe you were distracted for example becaues hasNotes is readonly. That is because hasNotes is kind of a function and not a real property. Properties like notes, atendees, startDate, endDate etc. are perfectly editable.
For saving modified event you can still use:
NSError error = nil;
[self.eventStore saveEvent:event span: EKSpanFutureEvents error:&error];
And for deleting it:
NSError error = nil;
[self.eventStore removeEvent:event span: EKSpanFutureEvents error:&error];
EDIT2: for deleting all events try this:
//assuming self.eventStore is already properly set in code
//identifierArray would be your NSMutableArray holding event identifiers
//change the name according to your code
NSError error = nil;
for (NSString *eventIdentifier in removeAllObjects) {
EKEvent *event = [self.eventStore eventWithIdentifier:eventIdentifier];
[self.eventStore removeEvent:event span:EKSpanFutureEvents error:&error];
}
//now you can also clear identifiers
[removeAllObjects removeAllObjects];
Note: there's no guarantee you'll be able to delete all events - only events from
default calendar which is set by usert in settings app.

How to synchronise two NSManagedObjectContext

I'm working on an ipad application that use coredata. It download information on a database that is on the web, and record them in coredata. The application is based on a split view. My problem was to make the download and the record of the data in background. Here is how I've done :
- I've create an NSOperation, that does the download and the record of the data.
- This NSOperation use a different NSManagedObjectContext than the context of the appDelegate, return by this function, that is in the appDelegate :
(NSManagedObjectContext*)newContextToMainStore {
NSPersistentStoreCoordinator *coord = nil;
coord = [self persistentStoreCoordinator];
NSManagedObjectContext *moc = [[NSManagedObjectContext alloc] init];
[moc setPersistentStoreCoordinator:coord];
return [moc autorelease];
}
- I've had an observer in the NSOperation, that will call this function in the appDelegate when I save the context, to modify the context of the delegate too :
- (void)mergeChangesFromContextSaveNotification:(NSNotification*)notification {
[[self managedObjectContext]mergeChangesFromContextDidSaveNotification:notification];
}
But I've a problem, the synchronisation doesn't work, because the data on the rootViewController (that is a UITableViewController), that have a NSManagedObjectContext initialised with the context of the appDelegate and use as datasource a NSFetchedResultsController, don't actualise automatically the informations, as it normaly must do.
So I ask you : What did I do wrong ? Is it the good way to use two different context and synchonise them ?
What you have here looks correct. You do want to make sure you implement the NSFetchedResultControllerDelegate methods in the rootViewController so the changes will appear in the UI.

CoreData weird behavior when data are loaded on background thread

I have very strange problem, when I don't understand what's going on at all, so I'm looking for explanation of it. Situation is as following:
I have a view controller with scrollview with three subviews in it. Those three subviews have method
-(void)loadContent
which loads content from database using CoreData in the background thread, creates subviews which represent loaded items and add them as own subviews calling [self addSubview: itemView]; That method is invoked as
[self performSelectorInBackground: #selector(loadContent) withObject: nil];
To load data from DB I'm using a singleton service class. Everything worked fine, but when those three views are loading their portions of data, it sometimes crashes the app.
I guessed it's because it shares one NSManagedObjectContext instance for all read operations, so I rewrote the class so it shares only NSManagedObjectModel and NSPersistentStoreCoordinator instances and creates it's own NSManagedObjectContext instance.
Suddenly, very strange thing happened. Data are loaded ok, subviews are created and added to the view hierarchy, but it get never displayed on the screen. When I switch back to the old singleton service class (sharing one managedObjectContext), it works again like a charm! (but with risk of crashing the app, though).
I absolutely don't get the point how loading data from DB is related to displaying items on the screen. More on that - when subviews are created and added to the view hierarchy, why the hell it don't get displayed?
The source looks like this:
- (void) loadContent {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSArray *results = [(WLDataService *)[WLDataService service] loadItemsForGDView];
NSUInteger channelPosition = 0;
CGFloat position = 0.0;
CGFloat minuteWidth = ((self.superview.frame.size.width / 2.0) / 60.0);
for(Item *it in results) {
/// On following lines size and position of the view is computed according to item setup - skipping here...
/// Create item; it's simple subclass of UIView class
WLGDItemView *item = [[WLGDItemView alloc] init];
/// Variables used here are declared above when size and position is computed
item.frame = CGRectMake(itemX, itemY, itemWidth, itemHeight);
[self performSelectorOnMainThread: #selector(addSubview:) withObject: item waitUntilDone: NO];
/// This is just helper macro to release things
WL_RELEASE_SAFELY(item);
}
[pool drain];
}
The basic service class (non-singleton one) implementation is as follows (just interesting parts):
#import "WLLocalService.h"
static NSPersistentStoreCoordinator *sharedPSC = nil;
static NSManagedObjectModel *sharedMOM = nil;
#implementation WLLocalService
#synthesize managedObjectContext;
/// This is here for backward compatibility reasons
+ (WLLocalService *) service {
return [[[self alloc] init] autorelease];
}
#pragma mark -
#pragma mark Core Data stack
- (NSManagedObjectContext *) managedObjectContext {
if (managedObjectContext == nil) {
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil) {
managedObjectContext = [[NSManagedObjectContext alloc] init];
[managedObjectContext setPersistentStoreCoordinator: coordinator];
}
[managedObjectContext setUndoManager: nil];
[managedObjectContext setMergePolicy: NSMergeByPropertyStoreTrumpMergePolicy];
}
return managedObjectContext;
}
- (NSManagedObjectModel *) managedObjectModel {
if(sharedMOM == nil) {
sharedMOM = [[NSManagedObjectModel mergedModelFromBundles: nil] retain];
}
return sharedMOM;
}
- (NSPersistentStoreCoordinator *) persistentStoreCoordinator {
if(sharedPSC == nil) {
NSURL *storeUrl = [self dataStorePath];
NSError *error = nil;
sharedPSC = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: [self managedObjectModel]];
if (![sharedPSC addPersistentStoreWithType: NSSQLiteStoreType configuration: nil URL: storeUrl options: nil error: &error]) {
WLLOG(#"%#: %#", error, [error userInfo]);
}
}
return sharedPSC;
}
#pragma mark -
#pragma mark Path to data store file
- (NSURL *) dataStorePath {
return [NSURL fileURLWithPath: [WL_DOCUMENTS_DIR() stringByAppendingPathComponent: #"/DB.sqlite"]];
}
- (void)dealloc {
WL_RELEASE_SAFELY(managedObjectModel);
[super dealloc];
}
#end
I'd really love to know what's going on here and why it behaves so strange (and - of course - why it does not work, in particular). Can anybody explain that?
thanks to all
Have you read Multi Threading with Core-Data twice?
First, do not load or construct UI elements on a background thread. The UI (whether on the desktop or on the iPhone) is single threaded and manipulating it on multiple threads is a very bad idea.
Second, data that you load into one context will not be immediately visible in another context. This is what is causing part of your problem.
The solution is to move all your UI code to the main thread and warm up the Core Data cache on a background thread. This means to load the data on a background thread (into a separate cache) to load it into the NSPersistentStoreCoordinator cache. Once that is complete your main thread can access that data very quickly because it is now in memory.
You realize that [WLDataService service] does not actually return a singleton? It creates a new instance every time. So you are effectively working with multiple instances of the Core Data components.
What about:
static WLDataService* gSharedService = NULL;
#implementation WLDataService
+ (id) service
{
#synchronized (self) {
if (gSharedService == NULL) {
gSharedService = [[self alloc] init];
}
}
return gSharedService;
}
#end
That will create the same instance every time. You will also want to make your managedObjectContext, managedObjectModel and persistentStoreCoordinator methods thread safe by using a #synchronized block. Otherwise there is a change that multiple threads will initialize those at the same time, leading to unexpected behaviour.