Multiple NSManagedObjectContexts issue - ios

I'm having an issue implementing a parent/child relationship between two NSManagedObjectContext's. My app imports a lot of data from a web service that was causing UI lag while saving the context. So in my AppDelegate I created a parent context (master) with NSPrivateQueueConcurrencyType:
- (NSManagedObjectContext *)masterMOC
{
if (_masterMOC != nil) {
return _masterMOC;
}
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil) {
_masterMOC = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[_masterMOC setPersistentStoreCoordinator:coordinator];
}
return _masterMOC;
}
...and a child context (main) with NSMainQueueConcurrencyType. I set the main MOC's parentContext to masterMOC:
- (NSManagedObjectContext *)mainMOC
{
if (_mainMOC != nil) {
return _mainMOC;
}
_mainMOC = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_mainMOC setUndoManager:nil];
[_mainMOC setParentContext:[self masterMOC]];
return _mainMOC;
}
In my applicationDidFinishLaunching I kick off an import operation that queries the web service and saves the results on the master (PrivateQueue) context. I also register for the NSManagedObjectContextDidSaveNotification and attempt to merge those changes into the child mainMOC.
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter addObserver:self selector:#selector(contextChanged:) name:NSManagedObjectContextDidSaveNotification object:self.masterMOC];
RequestHandler *handler = [[RequestHandler alloc] initWithManagedObjectContext:self.masterMOC];
[handler importAllViews];
...
return YES;
}
- (void) contextChanged: (NSNotification *) notification
{
// Only interested in merging from master into main.
if ([notification object] != self.masterMOC) return;
[self.mainMOC performBlock:^{
[self.mainMOC mergeChangesFromContextDidSaveNotification:notification];
}];
}
The RequestHandler class has a saveContext method that saves the master context on the correct thread:
#implementation KDBRequestHandler
...
- (void) saveContext
{
[self.managedObjectContext performBlock:^{
NSError *error;
if (![self.managedObjectContext save:&error]) {
[NSException raise:#"Unable to save build details." format:#"Error saving context: %#", error];
}
}];
}
...
#end
I've verified the import to be correctly saving its objects on a background thread with Instruments. My problem lies with the child managed object context that has NSMainQueueConcurrencyType. After the import is kicked off, application didFinishLaunching initializes the UI as is standard. The view controllers are assigned the mainMOC.
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
// Override point for customization after application launch.
if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) {
UISplitViewController *splitViewController = (UISplitViewController *)self.window.rootViewController;
UINavigationController *navigationController = [splitViewController.viewControllers lastObject];
splitViewController.delegate = (id)navigationController.topViewController;
UINavigationController *masterNavigationController = splitViewController.viewControllers[0];
KDBMasterViewController *controller = (KDBMasterViewController *)masterNavigationController.topViewController;
controller.managedObjectContext = self.mainMOC;
} else {
UINavigationController *navigationController = (UINavigationController *)self.window.rootViewController;
KDBMasterViewController *controller = (KDBMasterViewController *)navigationController.topViewController;
controller.managedObjectContext = self.mainMOC;
}
return YES;
}
The MasterViewController is essentially the boilerplate controller created for you when creating a core data project. It's fetchedResultsController ends up performing its work on the mainMOC therefore the main thread.
- (NSFetchedResultsController *)fetchedResultsController
{
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// Edit the entity name as appropriate.
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Job" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
// Set the batch size to a suitable number.
[fetchRequest setFetchBatchSize:20];
// Edit the sort key as appropriate.
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:NO];
NSArray *sortDescriptors = #[sortDescriptor];
[fetchRequest setSortDescriptors:sortDescriptors];
// Edit the section name key path and cache name if appropriate.
// nil for section name key path means "no sections".
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil cacheName:#"Master"];
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
NSError *error = nil;
if (![self.fetchedResultsController performFetch:&error]) {
// Replace this implementation with code to handle the error appropriately.
// abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
return _fetchedResultsController;
}
On initial load when the database is fresh and empty everything works as expected. The data is downloaded in the background and populated in the MasterViewController's UITableView as expected. However on subsequent runs, the app often crashes in the MasterViewController's fetchedResultsController. The fetch fails with this error:
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: '+entityForName: could not locate an NSManagedObjectModel for entity name 'Job''
*** First throw call stack:
(
0 CoreFoundation 0x01c6a1e4 __exceptionPreprocess + 180
1 libobjc.A.dylib 0x019678e5 objc_exception_throw + 44
2 CoreData 0x002c6a1b +[NSEntityDescription entityForName:inManagedObjectContext:] + 251
3 JMobile 0x00032ad4 -[KDBMasterViewController fetchedResultsController] + 340
4 JMobile 0x00031d6e -[KDBMasterViewController numberOfSectionsInTableView:] + 78
5 UIKit 0x0088e712 -[UITableViewRowData(UITableViewRowDataPrivate) _updateNumSections] + 102
6 UIKit 0x0088f513 -[UITableViewRowData invalidateAllSections] + 69
7 UIKit 0x006fa6ea -[UITableView _updateRowData] + 197
....
Since the crash does not happen 100% of the time I suspected a concurrency problem. I confirmed by noting that the mainMOC's parent context was nil at the time the fetchedResultsController was querying. How can I ensure the child context (mainMOC) is correctly set with a parent before attempting queries on it?

There is a race condition accessing your mainMOC:
Adding the line: [self mainMOC]; after you add the observer to the save of your masterMOC and before you start your import, will work around the MOC initialisation race.
You can read THIS for a discussion on race conditions in a similar case (regards which thread access your contexts in parallel).
The template code offered by Apple is good where you have only one thread initialising your contexts (the ones that are not temporary), otherwise, you should synchronise the initialisation using some sort of thread locking mechanism.

Related

How to insert batch data with CoreData?

My situation is more than 200 objects would come to me one by one per second through a callback function, obviously I cannot save a entity every single time in this callback function, that will cause UI suspend, so my question is how to insert batch data with core data?
------------------------------------------------------------
Edit:
thanks, guys, I re-edit my code like this, and there are some issues,
if I receive "1", "2", "3"...from the callback function frequently, my tableview often shows "1", "3", "2"... , they are in wrong order. if anything wrong, plz let me know.
in AppDelegate.m file:
- (NSManagedObjectContext *)mainManagedObjectContext {
if (_ mainManagedObjectContext != nil) {
return _managedObjectContext;
}
_mainManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
_mainManagedObjectContext.parentContext = [self rootManagedObjectContext];
return _mainManagedObjectContext;
}
- (NSManagedObjectContext*)rootManagedObjectContext {
if (_rootManagedObjectContext != nil) {
return _rootManagedObjectContext;
}
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (!coordinator) {
return nil;
}
_rootManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[_rootManagedObjectContext setPersistentStoreCoordinator:coordinator];
return _rootManagedObjectContext;
}
in the callback function I do this, I tried to replace _mainManagedObjectContext
with _rootContext or _workContextint callback function, but data never show on the tableview;
- (void)saveChatMessage:(ChatMessage*)msg userInfo:(UserInfo*)user chatType:(ChatType)chatType receiverID:(long long)receiverID receiverName:(NSString*)receiverName
{
ChatObject *chatObject = [NSEntityDescription
insertNewObjectForEntityForName:#"ChatObject"
inManagedObjectContext: _mainManagedObjectContext];
chatObject.receiveTime = [NSNumber numberWithLongLong:[NSDate date].timeIntervalSince1970];
chatObject.text = msg.text;
chatObject.richText = msg.richText;
chatObject.senderID = [NSNumber numberWithLongLong:user.userID];
chatObject.senderName = user.userName;
chatObject.chatType = [NSNumber numberWithInteger:chatType];
chatObject.isFromHost = [NSNumber numberWithBool:user.isOrganizer];
chatObject.receiverID = [NSNumber numberWithLongLong:receiverID];
chatObject.receiverName = receiverName;
NSError *error;
if (![_workerContext save:&error]) {
}
if (![_rootContext save:&error]) {
}
}
- (NSFetchedResultsController *)fetchedResultsController {
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription
entityForName:#"ChatObject" inManagedObjectContext:_mainManagedObjectContext];
[fetchRequest setEntity:entity];
NSSortDescriptor *sort = [[NSSortDescriptor alloc]
initWithKey:#"receiveTime" ascending:YES];
[fetchRequest setSortDescriptors:[NSArray arrayWithObject:sort]];
[fetchRequest setFetchBatchSize:20];
NSFetchedResultsController *theFetchedResultsController =
[[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:_mainManagedObjectContext sectionNameKeyPath:nil
cacheName:#"Root"];
self.fetchedResultsController = theFetchedResultsController;
_fetchedResultsController.delegate = self;
return _fetchedResultsController;
}
You have to use background contexts. I recommend this setup:
RootContext (background) for saving --> is parent of
MainContext (main thread) for UI --> is parent of
WorkerContext (background) for insert, update, delete operations
Root context and main context should be set up at app start. You can create a worker context in your web callbacks (when you have the new data available) and then use the context with the block APIs:
workerContext.performBlock() {
// update data here
}
When you call save() on the worker context, the UI context gets the changes and can update itself gracefully (e.g. via NSFetchedResultsControllerDelegate). Make sure you also call save on the root context to persist the data to the persistent store.
This is the basic outline of a good approach. In case you are not familiar with some of the mentioned techniques, you can find good and detailed explanations of many of these on this platform.
You can insert all data in a background thread and save the context when you are done - that will update the context in other threads. Just take care that all saving is done on that background thread to avoid concurrency problems.

CoreData child contexts, NSFetchedResultsController and main thread

Following this excellent post by Olivier Drobnik, I've implemented the three-layer CoreData stack proposed by CoreData guru Marcus S. Zarra:
The only difference from this diagram and my code is that I only use one Temporary Background MOC, in order to avoid duplicates when inserting objects in several temp MOCs. Here's my context initialisation code:
#pragma mark - NSManagedObjectContexts
+ (NSManagedObjectContext *)privateManagedObjectContext
{
if (!_privateManagedObjectContext) {
// Setup MOC attached to PSC
_privateManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[_privateManagedObjectContext setPersistentStoreCoordinator:[self persistentStoreCoordinator]];
// Add notification to perform save when the child is updated
_privateContextSaveObserver =
[[NSNotificationCenter defaultCenter]
addObserverForName:NSManagedObjectContextDidSaveNotification
object:nil
queue:nil
usingBlock:^(NSNotification *note) {
NSManagedObjectContext *savedContext = [note object];
if (savedContext.parentContext == _privateManagedObjectContext) {
[_privateManagedObjectContext performBlock:^{
NSLog(#"AMBCoreData -> saving privateMOC");
NSError *error;
if (![_privateManagedObjectContext save:&error]) {
NSLog(#"AMBCoreData -> error saving _privateMOC: %# %#", [error localizedDescription], [error userInfo]);
}
}];
}
}];
}
return _privateManagedObjectContext;
}
+ (NSManagedObjectContext *)mainUIManagedObjectContext
{
if (!_mainUIManagedObjectContext) {
// Setup MOC attached to parent privateMOC in main queue
_mainUIManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_mainUIManagedObjectContext setParentContext:[self privateManagedObjectContext]];
// Add notification to perform save when the child is updated
_mainUIContextSaveObserver =
[[NSNotificationCenter defaultCenter]
addObserverForName:NSManagedObjectContextDidSaveNotification
object:nil
queue:nil
usingBlock:^(NSNotification *note) {
NSManagedObjectContext *savedContext = [note object];
if (savedContext.parentContext == _mainUIManagedObjectContext) {
NSLog(#"AMBCoreData -> saving mainUIMOC");
[_mainUIManagedObjectContext performBlock:^{
NSError *error;
if (![_mainUIManagedObjectContext save:&error]) {
NSLog(#"AMBCoreData -> error saving mainUIMOC: %# %#", [error localizedDescription], [error userInfo]);
}
}];
}
}];
}
return _mainUIManagedObjectContext;
}
+ (NSManagedObjectContext *)importManagedObjectContext
{
if (!_importManagedObjectContext) {
// Setup MOC attached to parent mainUIMOC in private queue
_importManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[_importManagedObjectContext setParentContext:[self mainUIManagedObjectContext]];
}
return _importManagedObjectContext;
}
This code is pretty straightforward. I'm replicating the above diagram using only the mainUIManagedObjectContext in the NSMainQueueConcurrencyType. Every time the child context, importManagedObjectContext gets saved, a notification is fired and all the parent contexts performs a save in it's current thread.
I've implemented a test view controller with a UITableView and a NSFetchedResultsController attached. This is the code in the viewDidLoad of my test view controller:
- (void)viewDidLoad
{
[super viewDidLoad];
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"Task"];
[request setSortDescriptors:#[[NSSortDescriptor sortDescriptorWithKey:#"insertDate" ascending:NO]]];
self.fetchRequest = request;
NSFetchedResultsController *frc =
[[NSFetchedResultsController alloc]
initWithFetchRequest:self.fetchRequest
managedObjectContext:[AMBCoreData mainUIManagedObjectContext]
sectionNameKeyPath:nil
cacheName:nil];
frc.delegate = self;
[self setFetchedResultsController:frc];
[self.fetchedResultsController performFetch:nil];
}
Here I attach the mainUIManagedObjectContext to the NSFetchedResultsController. Later, in my viewDidAppear, I run a loop to insert a few Task entities:
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[[AMBCoreData importManagedObjectContext] performBlock:^{
for (int i = 0; i < 5000; i++) {
Task *task = [NSEntityDescription insertNewObjectForEntityForName:#"Task" inManagedObjectContext:[AMBCoreData importManagedObjectContext]];
task.title = [NSString stringWithFormat:#"Task %d", i];
task.insertDate = [NSDate new];
[[AMBCoreData importManagedObjectContext] save:nil];
}];
}
The thing is, I'm inserting 5000 objects and the UI is freezing when the data is populated into the table view. Florian Kugler ran a test with this architecture inserting 15.000 objects and with instruments he got this main thread usage (blue is for main thread, gray for any other threads):
But here's my main thread CPU usage with 5000 objects, profiled using an iPhone 5:
As you can see, my main thread usage is far greater than Florian's and also my UI freezes for a few seconds. My question is, am I doing something wrong? Is this the expected behaviour when using this three-layer MOC architecture with a NSFetchedResultsController and a UITableView? I know that inserting 5000 objects is not the usual behaviour of most apps, so when I've tried with 50 or 100 objects the freeze was inexistent or unnoticeable, but the main thread usage was high (although I admit that in this case it can be due another reasons like waking up the app).
Yes, it is expected, because Main MOC is involved in the saves of its children. It is convenient and kind of okay when children of the UI context don’t do big saves, but often becomes a performance problem if those saves are bigger. You can’t be sure that the UI thread does only minimum job when using this pattern.
For the big saves I would recommend creating a context that is configured directly with the persistent store coordinator. After big save happens, you just refetch and optionally refresh data in the UI context. For more details see my answer here.

CoreData Crashing Upon Deletion and More

I have some really mysterious behaviour with CoreData.
I'll add an Object. I save this object. I fetch the new results and reload the collection view (from which is display the objects). The new object shows up. Hoorah! Just as expected.
I do this a second time, but every time from now (unless the app is restarted) when re-fetching the data from my NSFetchedResultsController and reloading the collection view, the new object doesn't appear.
Equally, if I delete an object. First time, A-OK! The next time I do this, the app actually crashes with the following error:
(Aircraft is my NSManagedObject)
Terminating app due to uncaught exception 'NSObjectInaccessibleException', reason: 'CoreData could not fulfill a fault for '0xd0000000000c0000 <x-coredata://C418948D-90CD-40E9-A502-C4CAB0134419/Aircraft/p3>''
*** First throw call stack:
(0x18b79f09c 0x197ad5d78 0x18b4a77ac 0x18b4a6cac 0x18b4a6b00 0x100034438 0x18e6d8a44 0x18e6d6dc0 0x18e6d2e44 0x18e66ed78 0x18e26b0cc 0x18e265c94 0x18e265b4c 0x18e2653d4 0x18e265178 0x18e25ea30 0x18b75f7e0 0x18b75ca68 0x18b75cdf4 0x18b69db38 0x19106f830 0x18e6dc0e8 0x1000217dc 0x1980bfaa0)
libc++abi.dylib: terminating with uncaught exception of type _NSCoreDataException
Time for some code. I can't see any issues, but here it is. I won't spam you with everything, but if something rings any alarms, I can always add it on request.
Starting with the main view controller. This contains my collection view. Just as a note, it has two sections each fetching data from an individual NSFetchedResultsController. I am only seeing the issue with this specific one though. Fairly standard fetched results controller.
- (NSFetchedResultsController *)aircraftFetchedResultsController
{
if (_aircraftFetchedResultsController != nil) {
return _aircraftFetchedResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// Edit the entity name as appropriate.
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Aircraft" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
// Set the batch size to a suitable number.
[fetchRequest setFetchBatchSize:50];
// Edit the sort key as appropriate.
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:YES];
NSArray *sortDescriptors = #[sortDescriptor];
[fetchRequest setSortDescriptors:sortDescriptors];
// Edit the section name key path and cache name if appropriate.
// nil for section name key path means "no sections".
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil cacheName:#"Master"];
aFetchedResultsController.delegate = self;
self.aircraftFetchedResultsController = aFetchedResultsController;
NSError *error = nil;
if (![self.aircraftFetchedResultsController performFetch:&error]) {
// Replace this implementation with code to handle the error appropriately.
// abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
return _aircraftFetchedResultsController;
}
Anywhere I use an NSManagedObjectContext I am getting it from my AppDelegate. When adding the new object, the user is in a modal (form sheet) view controller. I create a new object, but do not insert it immediately, incase the user cancels:
SLAppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
NSManagedObjectContext *managedObjectContext = appDelegate.managedObjectContext;
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Aircraft" inManagedObjectContext:managedObjectContext];
self.aircraft = [[Aircraft alloc] initWithEntity:entity insertIntoManagedObjectContext:nil];
Then, when done, save the object:
SLAppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
//Only need to insert the new object if its 'NEW' else just save the existing one we are editing
if (!isEditing)
{
//Create new aircraft
NSManagedObjectContext *managedObjectContext = appDelegate.managedObjectContext;
//We are definetly saving the object, so now we insert it
[managedObjectContext insertObject:self.aircraft];
}
//Save
[appDelegate saveContextWithCompletionBlock:^(BOOL didSaveSuccessfully) {
if (didSaveSuccessfully)
{
[self dismissViewControllerAnimated:YES completion:^{
[delegate addAircraftDidSave:YES];
}];
}
else
{
[self dismissViewControllerAnimated:YES completion:^{
//ALERT with error
}];
}
}];
I use a delegate to send a message back to the main view controller saying the object has saved. That method then fetches the new data and reloads the collection view to show the new object:
-(void)fetchAircraft
{
NSError *error;
if (![[self aircraftFetchedResultsController] performFetch:&error])
{
// Update to handle the error appropriately.
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
[UIAlertView showGenericErrorAlert];
}
//Success, we have results
else
{
[self.collectionView reloadData];
}
}
Done. As I said, this works first time, then start acting up. Equally, you can substitute the save code for the delete code I have, fairly similar, delete and save changes:
SLAppDelegate *appDelegate = [[UIApplication sharedApplication] delegate];
NSManagedObjectContext *managedObjectContext = appDelegate.managedObjectContext;
[managedObjectContext deleteObject:self.aircraft];
[appDelegate saveContextWithCompletionBlock:^(BOOL didSaveSuccessfully) {
if (didSaveSuccessfully)
{
[self dismissViewControllerAnimated:YES completion:^{
[delegate addAircraftDidSave:YES];
}];
}
else
{
//ALERT with error
}
}];
(From my above comment:) The two fetched results controllers must use different
caches (cacheName: parameter). I also think (but I am not 100% sure about that)
that without sections, a cache does not give any advantages, so you can also
try cacheName:nil.
I believe you'll need to use separate ManagedObjectContexts for saves on a background thread.
https://developer.apple.com/library/ios/documentation/cocoa/conceptual/CoreData/Articles/cdConcurrency.html

Multi-Context CoreData with batch fetch by relationship

Problem in short
Since NSManagedObjectContext without persistent store coordinator doesn't support setFetchBatchSize selector, I've used a solution from this post and it works with certain issue, that I would like to resolve.
Here is the database scheme and Coredata structure with terms in brackets. Test application has two screens: master table with list of Chats and detail table with list of Messages. Master screen uses Main MOC in fetch controller for showing data in table and Worker MOC to create Chats and Messages. Detail screen uses Fetch MOC for showing data in table.
After I create a new Chat with Messages on master screen and save them with calling save on all MOCs in the hierarchy, I can't fetch Messages by a selected Chat in detail screen. All I got in console is: "CoreData: annotation: total fetch execution time: 0.0000s for 0 rows". it is possible to fetch this data after app restart.
It seems it has something to do with fault Messages in Fetch MOC having fault relations with Chats that have different objectID than Chats I have in Main MOC. Because when I fetch Chat object in Fetch MOC and then use it for finding Messages, everything is working fine.
I would appreciate it if someone could help me resolve this issue with Fetch MOC or maybe it is just fine to screw with all Object Graph concept and fetch data by my own ID fields instead of using relations.
Some code
Here is Coredata stack initialization which is done on didFinishLaunchingWithOptions:
- (void)initializeCoreDataStack
{
NSURL *modelURL = [[NSBundle mainBundle] URLForResource:#"FaultsFetching" withExtension:#"momd"];
_managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:_managedObjectModel];
_writerMOC = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[_writerMOC setUndoManager:nil];
[_writerMOC setPersistentStoreCoordinator:_persistentStoreCoordinator];
_mainThreadMOC = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_mainThreadMOC setUndoManager:nil];
[_mainThreadMOC setParentContext:_writerMOC];
_fetchMainThreadMOC = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_fetchMainThreadMOC setUndoManager:nil];
[_fetchMainThreadMOC setMergePolicy:NSMergeByPropertyStoreTrumpMergePolicy];
[_fetchMainThreadMOC setPersistentStoreCoordinator:_persistentStoreCoordinator];
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(backgroundContextDidSave:) name:NSManagedObjectContextDidSaveNotification object:_writerMOC];
NSURL *storeURL = [APP_DOC_DIR URLByAppendingPathComponent:#"FaultsFetching.sqlite"];
NSError *error = nil;
if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error])
{
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
}
- (void)backgroundContextDidSave:(NSNotification *)notification
{
[_fetchMainThreadMOC mergeChangesFromContextDidSaveNotification:notification];
NSLog(#"Yep, everything is merged");
}
Here is how I create Worker MOCs:
+ (NSManagedObjectContext *)createPrivateMOC
{
CoreDataManager *scope = [CoreDataManager sharedInstance];
NSManagedObjectContext *workerMOC = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
workerMOC.parentContext = scope.mainThreadMOC;
[workerMOC setUndoManager:nil];
workerMOC.mergePolicy = NSMergeByPropertyObjectTrumpMergePolicy;
return workerMOC;
}
Here is how Multi-Context save looks like. Argument async is YES. Naturally this selector is called within performBlock selector of a worker MOC
+ (void)writeToDiskAsync:(BOOL)async
{
CoreDataManager *scope = [CoreDataManager sharedInstance];
NSManagedObjectContext *writeManagedObjectContext = scope.writerMOC;
NSManagedObjectContext *mainManagedObjectContext = scope.mainThreadMOC;
PerformBlock mainMOCBlock = ^
{
NSError *mainError = nil;
if ([mainManagedObjectContext hasChanges] && ![mainManagedObjectContext save:&mainError])
{
ALog(#"Unresolved error %#, %#", mainError, [mainError userInfo]);
}
PerformBlock writerBlock = ^
{
NSError *writeError = nil;
if ([writeManagedObjectContext hasChanges] && ![writeManagedObjectContext save:&writeError])
{
ALog(#"Unresolved error %#, %#", writeError, [writeError userInfo]);
}
NSLog(#"Yep, everything is saved");
};
[scope performBlock:writerBlock onMOC:writeManagedObjectContext async:async];
};
[scope performBlock:mainMOCBlock onMOC:mainManagedObjectContext async:async];
}
- (void)performBlock:(PerformBlock)block onMOC:(NSManagedObjectContext *)target async:(BOOL)async
{
if (async)
[target performBlock:block];
else
[target performBlockAndWait:block];
}
Here is my fetch results controller on detail screen, where "detailItem" is a Chat entity set from master screen and "[CoreDataManager sharedInstance]" is a singleton:
- (NSFetchedResultsController *)fetchedResultsController
{
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
if (self.detailItem == nil)
return nil;
NSManagedObjectContext *fetchMOC = [CoreDataManager sharedInstance].fetchMainThreadMOC;
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Messages" inManagedObjectContext:fetchMOC];
[fetchRequest setEntity:entity];
[fetchRequest setFetchBatchSize:20];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"sentDate" ascending:NO];
[fetchRequest setSortDescriptors:#[sortDescriptor]];
NSPredicate *chatPredicate = [NSPredicate predicateWithFormat:#"relatedChat=%#", self.detailItem.objectID];
[fetchRequest setPredicate:chatPredicate];
_fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:fetchMOC sectionNameKeyPath:#"sectionIdentifier" cacheName:nil];
_fetchedResultsController.delegate = self;
NSError *error = nil;
if (![_fetchedResultsController performFetch:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
return _fetchedResultsController;
}
A bit of background
Parent/child MOCs were used to improve stability and responsiveness in the app that wasn't properly written from the beginning. However, because now everything related to Coredata is more or less centralized, it is possible to change the stack to something different.
SectionIdentifier is used for grouping messages by day like this: http://i.imgur.com/17tuKS7.png
Something else I might add later, also sorry for links and images: reputation and silly stuff
This is due to a bug. The workaround is to call obtainPermanentIDsForObjects: before saving the newly inserted objects.
See the following SO issue for more details:
Core Data: Do child contexts ever get permanent objectIDs for newly inserted objects?

Core Data issue. Data won't save

So I have a utility app and I am trying to save some text into a "To" and "Message: text field on the Flipside View Controller. However, my data won't save. I am new to objective C and I have been using multiple different tutorials to the point where I have totally confused myself. Hopefully you can help me out. Not sure what else to do at this point...
FlipsideViewController.m
#import "CCCFlipsideViewController.h"
#import "CCCAppDelegate.h"
#import "CCCMainViewController.h"
#import "MessageDetails.h"
#interface CCCFlipsideViewController ()
{
// NSManagedObjectContext *context;
}
#end
#implementation CCCFlipsideViewController
#synthesize allMessageDetails;
#synthesize managedObjectContext;
- (void)awakeFromNib
{
[super awakeFromNib];
CCCAppDelegate *appDelegateController = [[CCCAppDelegate alloc]init];
self.managedObjectContext = appDelegateController.managedObjectContext;
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription
entityForName:#"MessageDetails" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
NSError *error;
self.allMessageDetails = [self.managedObjectContext executeFetchRequest:fetchRequest error:&error];
/*
NSManagedObject *managedObject; = [_fetchedResultsController valueForKey:#"to"];
self.toTextField.text = managedObject to;
messageDetails.to = [allMessageDetails firstObject];
self.toTextField.text = messageDetails.to;
messageDetails.message = [allMessageDetails valueForKey:#"message"];
self.messageTextField.text = messageDetails.message;
*/
NSLog(#"The 'to' is currently at %# after viewdidload", self.toTextField.text);
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (BOOL)textFieldShouldReturn:(UITextField *)textField
{
return [textField resignFirstResponder]; //function says that if (bool) the text field is open and the keyboard hits return, text field is to resign first responder.
}
#pragma mark - Actions
- (IBAction)done:(id)sender
{
[self.delegate flipsideViewControllerDidFinish:self];
}
- (IBAction)resignFirstResponder:(id)sender {
[self.toTextField resignFirstResponder];
[self.messageTextField resignFirstResponder];
NSLog(#"Resigned First Responder");
}
- (IBAction)save:(id)sender {
// Create a new instance of the entity managed by the fetched results controller.
NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
NSEntityDescription *entity = [[self.fetchedResultsController fetchRequest] entity];
NSManagedObject *newManagedObject = [NSEntityDescription insertNewObjectForEntityForName:[entity name] inManagedObjectContext:context];
// If appropriate, configure the new managed object.
[newManagedObject setValue:self.toTextField.text forKey:#"to"];
[newManagedObject setValue:self.messageTextField.text forKey:#"message"];
// Save the context.
NSError *error = nil;
if (![context save:&error]) {
/*
Replace this implementation with code to handle the error appropriately.
abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. If it is not possible to recover from the error, display an alert panel that instructs the user to quit the application by pressing the Home button.
*/
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
}
#pragma mark -
#pragma mark Fetched results controller
- (NSFetchedResultsController *)fetchedResultsController {
if (_fetchedResultsController != nil) {
return _fetchedResultsController;
}
/*
Set up the fetched results controller.
*/
// Create the fetch request for the entity.
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// Edit the entity name as appropriate.
NSEntityDescription *entity = [NSEntityDescription entityForName:#"MessageDetails" inManagedObjectContext:self.managedObjectContext];
[fetchRequest setEntity:entity];
// Set the batch size to a suitable number.
[fetchRequest setFetchBatchSize:20];
// Edit the sort key as appropriate.
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"to" ascending:NO];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
// Edit the section name key path and cache name if appropriate.
// nil for section name key path means "no sections".
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil cacheName:#"Root"];
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
NSError *error = nil;
if (![_fetchedResultsController performFetch:&error]) {
/*
Replace this implementation with code to handle the error appropriately.
abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. If it is not possible to recover from the error, display an alert panel that instructs the user to quit the application by pressing the Home button.
*/
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
return _fetchedResultsController;
}
#end
I didn't look at all your code because there was a problem near the top that negates everything you do thereafter. Don't alloc/init your app delegate in awakeFromNib or anywhere else for that matter. The one and only instance of your app delegate already exists (I have no idea what happens when there is more than one app delegate).
CCCFlipsideViewController needs to gain access to the managed object context through another means. Perhaps CCCMainViewController (or another view controller) could set the CCCFlipsideViewController's managedObjectContext property. If CCCMainViewController does not have access to the managed object context, have the app delegate pass that context to it.
Example:
App delegate sets a managedObjectContext property on the root view controller; the root view controller, in turn, sets the managedObjectContext property on a child view controller (like your flipside VC), etc.
You don't seem to ever actually set self.messageTextField.text or self.toTextField.text to anything -- you have commented out code in your viewDidLoad method which sets these fields. bilobatum is entirely correct about your AppDelegate issue as well -- you could also use something like
[((NSObject*)[UIApplication sharedApplication].delegate) valueForKey: #"managedObjectContext"];
to get the app delegate for your application if you want to fix that fast, though long term bilobatum's solution to this is better design.
Honestly, I think you've done quite a number on this code... ;)
OK, first off, in your save method, don't create another NSManagedObjectContext, use the instance variable you already declared, "managedObjectContext."
Secondly, I think you've made things way too complicated for yourself... Storing Core Data is actually shockingly simple once you've created the NSManagedObject subclasses and set up everything in the App Delegate...
It seems as if you wouldn't need any info from the "fetchedResultsController" at that point in your code, since you're saving, not fetching. Maybe try changing your save method to something like:
- (IBAction)save:(id)sender {
NSEntityDescription *entity = [NSEntityDescription insertNewObjectForEntityForName:#"MessageDetails" inManagedObjectContext:self.managedObjectContext];
// If appropriate, configure the new managed object.
[entity setValue:self.toTextField.text forKey:#"to"];
[entity setValue:self.messageTextField.text forKey:#"message"];
// Save the context.
NSError *error = nil;
[self.managedObjectContext save:&error]
if (error) {
/*
Replace this implementation with code to handle the error appropriately.
abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. If it is not possible to recover from the error, display an alert panel that instructs the user to quit the application by pressing the Home button.
*/
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
}
Edit: And to get the managed object context from the app delegate...
Right after your #synthesize's, create a variable for the App Delegate.
AppDelegate* appDelegateController;
And in viewDidLoad, initialize it:
appDelegateController = (AppDelegate*)[[UIApplication sharedApplication] delegate];
Right after viewDidLoad (or anywhere you want), you can stick in a method to declare the managed object context:
- (NSManagedObjectContext*)managedObjectContext {
return appDelegateController.managedObjectContext;
}
Then back in viewDidLoad, call that method with:
self.managedObjectContext = [self managedObjectContext];

Resources