Using core data with and with out a nsfetchedResultsController - iphone

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.

Related

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.

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.

NSFetchedResultsControllerDelegate not firing

I can't figure out why, for the life of me, the NSFetchedResultsControllerDelegate methods are not firing when I add data to the underlying datastore. The data shows up immediately if I restart the iPhone application.
I have subclassed UITableViewController and conform to NSFetchedResultsControllerDelegate:
#interface ProjectListViewController : UITableViewController <NSFetchedResultsControllerDelegate> {
NSFetchedResultsController* fetchedResultsController_;
NSManagedObjectContext* managedObjectContext_;
}
I instantiate the NSFetchedResultsController and set the delegate to self:
// Controller
fetchedResultsController_ = [[NSFetchedResultsController alloc] initWithFetchRequest:request
managedObjectContext:self.managedObjectContext
sectionNameKeyPath:#"Client"
cacheName:#"ProjectsCache"];
fetchedResultsController_.delegate = self;
I implement the delegate methods:
- (void)controllerWillChangeContent:(NSFetchedResultsController*)controller {
NSLog(#"ProjectListViewController.controllerWillChangeContent");
// The fetch controller is about to start sending change notifications, so prepare the table view for updates.
[self.tableView beginUpdates];
}
- (void)controllerDidChangeContent:(NSFetchedResultsController*)controller { ... }
- (void)controller:(NSFetchedResultsController*)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath { ... }
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id<NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type { ... }
I create the entity I wish to save:
Project* newProject = [NSEntityDescription insertNewObjectForEntityForName:#"Project" inManagedObjectContext:self.managedObjectContext];
ProjectDetailViewController* detail = [[ProjectDetailViewController alloc] initWithStyle:UITableViewStyleGrouped
delegate:self
selector:#selector(finishedAdding:)
project:newProject];
And later, I save it:
- (void)save {
// NSLog(#"ProjectDetailViewController.save");
self.project.name = projectNameTextField_.text;
NSError* error;
BOOL b = [self.project.managedObjectContext save:&error];
if (!b) {
NSLog(#"Error saving project!");
} else {
NSLog(#"Project was successfully saved.");
[delegate_ performSelector:selector_ withObject:self.project];
}
[self dismissModalViewControllerAnimated:YES];
}
It all works just fine except for the fact that my delegate methods don't fire. Obviously my table view doesn't get updated and the only way to see the new data is to explicitly refresh or restart the app.
I've looked through the CoreData Recipe app - but can't seem to find what I'm missing. Thoughts?
-Luther
If I change the sectionNameKeyPath from #"Client" to nil when I create the original fetchedResultsController_, both the Project entity creation and save indeed invoke the delegate methods!
fetchedResultsController_ =
[[NSFetchedResultsController alloc] initWithFetchRequest:request
managedObjectContext:self.managedObjectContext
sectionNameKeyPath:nil
cacheName:#"ProjectsCache"];
Upon hyper scrutiny, that is what the CoreData Recipes example does. As my data gets more complex - I think I'm going to need that argument to help break the results up into sections but for now, it is nice to see the delegate handlers being invoked.
My read of the above looks like you are using 2 NSManagedObjectContexts, one in ProjectListViewController, and seperate one in ProjectDetailViewController (I assume it is created there since I don't see it passed in.
When you save one context it does not automatically propagate changes into another, so saving the one in ProjectDetailViewController will not cause the changes to appear in the context of ProjectListViewController, which means there are no changes in that one to tell the delegate about it.
If you want to push the changes between contexts, look at NSManagedObjectContextDidSaveNotification and mergeChangesFromContextDidSaveNotification: (they are the end of the NSManagedObjectContext documentation).

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.

Undo Management with Core Data

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;
}