ios - error when calling viewWillAppear() manually (Objective-C) - ios

I'm trying to update core-data in the background of my ios app, I do this by first deleting the core-data and then adding it back. However, I need a certain segue to occur for some functions to run but when I try to do everything in the background these functions never run, unless I change the page and go back to it.
So I tried to fix this error by calling viewWillAppear() manually but I get the following error.
*** Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'An instance 0x17191c00 of class CardScanView was deallocated while key value observers were still registered with it. Current observation info: <NSKeyValueObservationInfo 0x16d99c30> (
<NSKeyValueObservance 0x16dcdf20: Observer: 0x17191c00, Key path: verifyingCard, Options: <New: YES, Old: NO, Prior: NO> Context: 0x0, Property: 0x16d5db30>
method in class where error occurs:
- (void) resetDatabase {
count++;
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void){
ConDAO *con = [[ConDAO alloc] init];
DatabaseManager *manager = [DatabaseManager sharedManager];
NSError * error;
NSURL * storeURL = [[[manager managedObjectContext] persistentStoreCoordinator] URLForPersistentStore:[[[[manager managedObjectContext] persistentStoreCoordinator] persistentStores] lastObject]];
[[manager managedObjectContext] reset];//to drop pending changes
if ([[[manager managedObjectContext] persistentStoreCoordinator] removePersistentStore:[[[[manager managedObjectContext] persistentStoreCoordinator] persistentStores] lastObject] error:&error])
{
// remove the file containing the data
[[NSFileManager defaultManager] removeItemAtURL:storeURL error:&error];
//recreate the store like in the appDelegate method
[[[manager managedObjectContext] persistentStoreCoordinator] addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error];//recreates the persistent store
}
NSLog(#"*****************************");
NSLog(#"updating");
NSLog(#"count: %d", count);
NSLog(#"*****************************");
[self populateDatabase:0 con:con];
NSTimer *timer = [NSTimer timerWithTimeInterval:60.0
target:self
selector:#selector(resetDatabase)
userInfo:nil repeats:NO];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
dispatch_async(dispatch_get_main_queue(), ^(void){
CardScanView *card = [[CardScanView alloc] init];
[[NSNotificationCenter defaultCenter] addObserver:card
selector:#selector(viewWillAppear:)
name:#"updated" object:nil];
[[NSNotificationCenter defaultCenter] postNotificationName:#"updated" object:nil];
});
});
}
viewWillAppear and viewDidDissapear in other class:
- (void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
// Setup KVO for verifyingcard
[self addObserver:self forKeyPath:#"verifyingCard" options:NSKeyValueObservingOptionNew context:nil];
if([BluetoothTech isEqualToString:#"BLE"]){
self.centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil options:#{CBCentralManagerOptionShowPowerAlertKey: #YES}];
}
else if([BluetoothTech isEqualToString:#"HID"]){
[self.bluetoothScanTextView becomeFirstResponder];
}
[self loadStudents];
}
- (void)viewDidDisappear:(BOOL)animated{
[super viewDidDisappear:animated];
// Disconnect the bluetooth peripheral device if it exists
if(self.discoveredPeripheral != nil){
[self.centralManager cancelPeripheralConnection:self.discoveredPeripheral];
}
// Remove KVO for verifyingCard
[self removeObserver:self forKeyPath:#"verifyingCard"];
}
Whats causing the error, also is there a better way to approach this rather than manually calling viewDidLoad? thanks

Core-data is not thread safe. You can't just access any context from any thread. You should treat the viewContext of persistentStoreCoordinator as readonly and ONLY read it from the main thread. All changes to core data should go through performBackgroundTask and use the context that is passed to it.
You can't just delete the files under core-data and expect stuff to work. First there many be other managedObjectContext reading or writing while you are deleting the file. Second core data can be set to use external storage for some entities that will be store in separate files. Third, iOS uses WAL mode for SQLite journalling and there may be (potentially large) journal files sitting around for any Core Data persistent store.
To update the UI for core data change you should use a NSFetchedResultsController. make sure to set persistentContainer.viewContext.automaticallyMergesChangesFromParent = YES in your core data setup.
do not keep any pointers to managedObjects. They can be deleted from the context without you being aware and then accessing them will cause a crash. Instead use a fetchedResultsController - even for just one object.
To delete all entities in core data:
[self.persistentContainer performBackgroundTask:^(NSManagedObjectContext * _Nonnull) {
NSArray* entities = context.persistentStoreCoordinator.managedObjectModel.entities;
for (NSEntityDescription* entity in entities) {
NSFetchRequest* request = [NSFetchRequest fetchRequestWithEntityName:entity.name];
request.predicate = [NSPredicate predicateWithValue:YES];
request.returnsObjectsAsFaults = YES;
NSArray* result = [context executeFetchRequest:request error:NULL];
for (NSManagedObject* i in result) {
[context deleteObject:i];
}
}
[context save:NULL];
}];

You should never call the viewDidLoad, viewWillAppear, etc methods yourself. Only when you override those methods you should call them on super.
Extract the code you are running to update (which looks like you already have in loadStudents) and call that method instead of viewWillAppear.
See https://developer.apple.com/documentation/uikit/uiviewcontroller

Related

Update Core Data without having to change screen

After I update my Core Data store - by deleting then adding the data - in a different thread, I'm required to change the screen and then go back for the data to update it. Is there a way to do update Core Data without having to change the screen in the app?
code to reset database:
- (void) resetDatabase {
count++;
dispatch_async(dispatch_get_global_queue( DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(void){
ConDAO *con = [[ConDAO alloc] init];
DatabaseManager *manager = [DatabaseManager sharedManager];
NSError * error;
NSURL * storeURL = [[[manager managedObjectContext] persistentStoreCoordinator] URLForPersistentStore:[[[[manager managedObjectContext] persistentStoreCoordinator] persistentStores] lastObject]];
[[manager managedObjectContext] reset];//to drop pending changes
if ([[[manager managedObjectContext] persistentStoreCoordinator] removePersistentStore:[[[[manager managedObjectContext] persistentStoreCoordinator] persistentStores] lastObject] error:&error])
{
// remove the file containing the data
[[NSFileManager defaultManager] removeItemAtURL:storeURL error:&error];
//recreate the store like in the appDelegate method
[[[manager managedObjectContext] persistentStoreCoordinator] addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:nil error:&error];//recreates the persistent store
}
NSLog(#"*****************************");
NSLog(#"updating");
NSLog(#"count: %d", count);
NSLog(#"*****************************");
[self populateDatabase:0 con:con];
NSTimer *timer = [NSTimer timerWithTimeInterval:60.0
target:self
selector:#selector(resetDatabase)
userInfo:nil repeats:NO];
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
dispatch_async(dispatch_get_main_queue(), ^(void){
});
});
}
Code that runs when ui is changed:
- (void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
// Setup KVO for verifyingcard
[self addObserver:self forKeyPath:#"verifyingCard" options:NSKeyValueObservingOptionNew context:nil];
if([BluetoothTech isEqualToString:#"BLE"]){
self.centralManager = [[CBCentralManager alloc] initWithDelegate:self queue:nil options:#{CBCentralManagerOptionShowPowerAlertKey: #YES}];
}
else if([BluetoothTech isEqualToString:#"HID"]){
[self.bluetoothScanTextView becomeFirstResponder];
}
[self loadStudents];
}
I think it has to do with the loadStudents() function, but when I use NSNotificationCenter to run it from the other class, it still doesnt work.
LoadStudent code:
- (void)loadStudents{
NSError *error = nil;
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:#"Caf_student_cards"];
NSArray *arr = [[self.manager managedObjectContext] executeFetchRequest:fetchRequest error:&error];
for(int i = 0; i < [arr count]; i++){
if([[[arr objectAtIndex:i] valueForKey:#"user_id"] isEqualToString:#"201509061"]){
NSLog(#"%#",[arr objectAtIndex:i]);
}
}
if(!error){
self.caf_student_cards = [NSMutableArray arrayWithArray:arr];
self.keys = [[[arr.firstObject entity] attributesByName] allKeys];
}
else{
NSLog(#"%s %s %s","\n\n\n",[[error localizedDescription] UTF8String],"\n\n\n");
dispatch_async(dispatch_get_main_queue(), ^{
// Show alert to tell user to reload this page
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:[NSString stringWithFormat:#"Error: %#", [error localizedDescription]] message:#"Check connection and relog back into cafeteria." delegate:nil cancelButtonTitle:#"OK" otherButtonTitles:nil, nil];
[alert show];
});
}
}
There are multiple problems with this code.
The one most directly relevant to your question is that you're updating your persistent store, but not updating your UI. It doesn't happen automatically. If you have new data, you need to tell your UI to update. How to do that depends on what kind of UI you have. If it's a table view, it might be as simple as telling the table view to reload its data. If you have an array that you use to hold the UI's data, you would need to update that too (it looks like this might be what caf_student_cards is in your code but it's impossible to be sure.
Other problems-- and these are major problems that you need to fix immediately:
You're doing Core Data multithreading wrong. Using dispatch_async is not effective here. You need to use performBlock or performBlockAndWait on your managed object context, or else performBackgroundTask on your persistent container.
You're removing the main persistent store file but not removing the journal files. This is pretty much guaranteed to either prevent old data from being deleted or else simply cause data corruption. What you're doing is not a useful technique. Removing the persistent store files is rarely a good idea. If you want to get rid of existing data, delete it from Core Data, maybe by telling your context to delete the objects or else by using NSBatchDeleteRequest.
There may be others. This code is a mess. You would be doing yourself a huge favor if you spent a little time looking over Apple's Core Data Programming Guide.
Also you keep asking nearly the same question repeatedly. You've had some good advice, but you don't seem to be taking any of it. If you want more information, go and read over other answers people have already given when you've posted this question before.

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.

Core Data doesn't save data until the app is closed

I'm trying to save data into core data after the completion of a web request.
In AppDelegate I have my context. In the following the code for get the context:
- (NSManagedObjectContext *)contex {
#synchronized(self) {
if (_contex == nil) {
_contex = [[NSManagedObjectContext alloc] init];
[_contex setPersistentStoreCoordinator:self.persistentStoreCoordinator];
}
return _contex;
}
}
Like the Apple guidelines say, I have one persistent store coordinata shared with multiple contexts.
This is the code where I take the data from web, get the context and make the call to the method for save the new value into core data.
NSURLSessionDownloadTask *task = [session downloadTaskWithRequest:request completionHandler:^(NSURL *localFile, NSURLResponse *response, NSError *error) {
[[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
UIImage *image;
if (!error) {
NSData *imageData = [NSData dataWithContentsOfURL:localFile];
image = [UIImage imageWithData:imageData];
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:#"thumbnailIsSetted" object:self userInfo:#{#"image": image,
#"PhotoUrl": photo.url}];
#synchronized(self) {
NSManagedObjectContext *context = [(SocialMapAppDelegate *)[[UIApplication sharedApplication] delegate] contex];
[Photo addThumbnailData:imageData toPicture:photo.url fromContext:context];
}
});
} else {
//if we get an error, we send the default icon
image = [UIImage imageNamed:#"Photo-thumbnail"];
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:#"thumbnailIsSetted" object:self userInfo:#{#"cached image": image,
#"PhotoUrl": photo.url}];
});
}
}];
[task resume]; //we resume the task in case it is souspended
}];
and this is the method that I use for save the data into core data:
+ (void)addThumbnailData:(NSData *)data toPicture:(NSString *)pictureUrl fromContext:(NSManagedObjectContext *)context
{
if (pictureUrl && context) {
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"Photo"];
request.predicate = [NSPredicate predicateWithFormat:#"url == %#",pictureUrl];
NSError *error = nil;
NSArray *matches = [context executeFetchRequest:request error:&error];
if ([matches count] == 1) {
Photo *photo = [matches lastObject];
photo.thumbnailData = data;
if (![context save:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
}
}
}
}
I can't figure where is the problem, the context is not nil, and I don't get any error. If i restart the app the data are there.
I do not recommend using multiple contexts unless you are using a multi-threaded design in your application. I would recommend stripping the app down to one context to avoid having to pass notifications around. It simply isn't needed 99% of the time.
In addition, if you are using parent/child contexts, I do not recommend re-using the child contexts. They should be transient; used for a single task and then thrown away. The reason for this is that data changes only flow one way; up. If you change something in one child it does not get pushed to any siblings. Further, child contexts go stale. If you make a change in the main context, all of the children will be out of date.
Your UI should be using a single NSManagedObjectContext across the entire application. That will most likely resolve your issues.
the code looks ok... the save persists the context's content to disk alright. to refresh other contexts you have to listen to the ContextDidSave notification and merge in the data
first observe:
...
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(mergeChanges:)
name:NSManagedObjectContextDidSaveNotification
object:managedObjectContext];
...
then merge
- (void)mergeChanges:(NSNotification *)notification {
// Merge changes into the main context on the main thread
[self.mainManagedObjectContext performSelectorOnMainThread:#selector(mergeChangesFromContextDidSaveNotification:)
withObject:notification
waitUntilDone:YES];
}
i solved, the problem was on the class where i visualize the and ask for the data, in that class instead of get the context from the appDelegate on this way:
[(SocialMapAppDelegate *)[[UIApplication sharedApplication] delegate] contex];
i did this
[[SocialMapAppDelegate alloc] init].contex
in this way i simple create another context and another store, thing that is totally wrong, so i solved simply by substitute the code above with
[(SocialMapAppDelegate *)[[UIApplication sharedApplication] delegate] contex];

Ios CoreData with 3 MOC solution (app freezes on saving process)

I use coredata in my app, with 3 contexts:
__masterManagedObjectContext -> is the context that has the NSPersistentStoreCoordinator and save the data to disk.
_mainManagedObjectContext -> is the context used by app, everywhere
dispatchContext -> context used in background method, where i have my webservice access and all coredata insertion/update stuff.
I'll put some code to realize my solution:
App initialisation code:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions //a app começa aqui
{
NSPersistentStoreCoordinator *coordinator = [self newPersistentStoreCoordinator];
__masterManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[__masterManagedObjectContext setPersistentStoreCoordinator:coordinator];
_mainManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_mainManagedObjectContext setUndoManager:nil];
[_mainManagedObjectContext setParentContext:__masterManagedObjectContext];
return YES;
}
Method to create a new store cordinator
- (NSPersistentStoreCoordinator *)newPersistentStoreCoordinator
{
NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:#"example.sqlite"];
NSError *error = nil;
NSPersistentStoreCoordinator *pC = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self newManagedObjectModel]];
NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,
[NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil];
if (![pC addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:options error:&error])
{
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
return pC;
}
- (NSManagedObjectModel *)newManagedObjectModel
{
NSURL *modelURL = [[NSBundle mainBundle] URLForResource:#"example" withExtension:#"momd"];
NSManagedObjectModel *newManagedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
return newManagedObjectModel;
}
Thread call with context specification (essential code):
#try
{
dispatchContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
[dispatchContext setUndoManager:nil];
[dispatchContext setParentContext:__masterManagedObjectContext];
NSNotificationCenter *notify = [NSNotificationCenter defaultCenter];
[notify addObserver:self selector:#selector(mergeChanges:) name:NSManagedObjectContextDidSaveNotification object:dispatchContext];
if(dispatchContext != nil)
{
[NSThread detachNewThreadSelector:#selector(parseDataWithObjects) toTarget:self withObject:nil];
}
else
{
NSLog(#"context IS NIL");
}
}
The background method:
- (void)parseDataWithObjects
{
[dispatchContext lock];
...
webservice data parse, and core data inserting/updating (+/- 5MB)
...
[dispatchContext save:&error];
[dispatchContext unlock];
[__masterManagedObjectContext save:nil];
}
This method is caled in all UI to access coredata data.
- (NSManagedObjectContext *)managedObjectContext
{
return _mainManagedObjectContext;
}
A calling example:
NSManagedObjectContext *context = [(AppDelegate *)[[UIApplication sharedApplication] delegate] managedObjectContext];
...fetching, update, ...
Now, my really problem:
Whenever the master context is to be saved ([__masterManagedObjectContext save:nil];, on background), when I try to access the main context (_mainManagedObjectContext), the app freezes (maybe a lock?).
The saving process takes a long time (because are a lots of data (aprox. 6mb)). While saving, the app turns slow and if i access some data while this process is running, my app freezes forever (i need to force quit).
Another problem is to merge the contexts. Imagine, using the main context in other viewController, and saving that context, everything works fine until i close de app. When i open the app again, nothing was saved.
What i'm doing wrong? Until now, this contexts are confusing to me.
Someone can help me? I really appreciate :)
----------
EDIT:
Following Florian Kugler response, now i have only 2 context, every one with the same coordinator.
When my app is initialised, i call this method:
-(void) createContexts
{
NSURL *modelURL = [[NSBundle mainBundle] URLForResource:#"example" withExtension:#"momd"];
NSManagedObjectModel *newManagedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
NSURL *storeURL = [[self applicationDocumentsDirectory] URLByAppendingPathComponent:#"example.sqlite"];
NSError *error = nil;
pC = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:newManagedObjectModel];
NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,
[NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil];
if (![pC addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:options error:&error])
{
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
mainManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
mainManagedObjectContext.persistentStoreCoordinator = pC;
}
- (void)mergeChanges:(NSNotification*)notification {
[mainManagedObjectContext performBlock:^{
[mainManagedObjectContext mergeChangesFromContextDidSaveNotification:notification];
}];
}
- (void)saveMasterContext
{
[mainManagedObjectContext performBlock:^{
[mainManagedObjectContext save:nil];
}];
}
To start my data import (on background), i use this code:
NSNotificationCenter *notify = [NSNotificationCenter defaultCenter];
[notify addObserver:self selector:#selector(mergeChanges:) name:NSManagedObjectContextDidSaveNotification object:backgroundContext];
[NSThread detachNewThreadSelector:#selector(parseDataWithObjects) toTarget:self withObject:nil];
My background method:
- (void)parseDataWithObjects
{
[self resetTime];
backgroundContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
backgroundContext.persistentStoreCoordinator = pC;
...
[backgroundContext save:&error];
}
Resuming...
1st - I create my main context
2nd - I define background context notification, to merge the changes
after save.
3rd - I call the background method
4rd - I save my background context
And the performance is really better. But the app freezes a little, i think is on "mergeChanges". I'm doing something wrong?
When using the NSPrivateQueueConcurrencyType or NSMainQueueConcurrencyType you should wrap everything you do on these contexts in performBlock:. This makes sure, that these commands get executed on the right queue. For example when you save the master context:
[__masterManagedObjectContext performBlock:^{
[__masterManagedObjectContext save];
}];
Furthermore you don't have to merge the changes manually from the dispatch context if you set it up as a child to your master context. As soon as you save the child context the changes will be pushed into the parent context.
Another problem is that you are initializing a context with the NSConfinementConcurrencyType on one thread and then use it on a different thread. With this concurrency type it is very important that you initialize the context on the thread you are going to use it.
However, I would suggest you don't use NSConfinementConcurrencyType at all. One possible alternative would be to setup your dispatch context with NSPrivateQueueConcurrencyType:
NSManagedObjectContext* dispatchContext = [[NSManagedObjectContext] alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
dispatchContext.parentContext = __masterManagedObjectContext;
[dispatchContext performBlock:^{
[self parseDataWithObjects];
}];
When you do this there is no need to acquire locks, everything you do within the block will be processed on a private serial queue.
Performance
You already mentioned that saving takes quite long and that your app becomes unresponsive.
Your managed object context setup with three nested contexts (private <- main <- dispatch) is not the best choice if you plan to import larger amounts of data in the dispatch context. This will always block the main thread for significant amounts of time, because all the changes you make in the dispatch context have to be copied into the main context before they can be saved in the "root" context.
I recently wrote an article about this, comparing the performance of different core data setups. In a follow up post I explain in greater detail why this setup takes so much time on the main thread.
For importing large amounts of data it is MUCH faster to use independent managed object contexts with a common persistent store coordinator. You create one context with NSMainQueueConcurrencyType (which you use for all UI related stuff), and another one with NSPrivateQueueConcurrencyType (for importing data). You assign the same persistent store coordinator to both of them.
// assuming you have persistendStoreCoordinator
NSManagedObjectContext* mainContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
mainContext.persistentStoreCoordinator = persistentStoreCoordinator;
NSManagedObjectContext* backgroundContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
backgroundContext.persistentStoreCoordinator = persistentStoreCoordinator;
This setup doesn't provide the automatic change propagation you get with nested contexts, but this is very easy to do via the save notification. Notice the use of performBlock: again, so that the merging happens on the right thread:
// ...
NSNotificationCenter *notify = [NSNotificationCenter defaultCenter];
[notify addObserver:self selector:#selector(mergeChanges:) name:NSManagedObjectContextDidSaveNotification object:backgroundContext];
// ...
- (void)mergeChanges:(NSNotification*)notification {
[mainContext performBlock:^{
[mainContext mergeChangesFromContextDidSaveNotification:notification];
}];
}
Hope this helps!

NSFetchedResultsController multiple entities for UITableView

I have two entities one called Post and one called User. Post<<---->User is the relationship in core data. I am using a NSFetchedResultsController to fetch all Post records in my core data stack and then displaying them in a UITableView. Each cell has an image and that image corresponds to a User.profilePicture.
Upon initializing I do not download the profile picture from the server, I only download when it scrolls past that cell (lazy load). Once I download it I save the downloaded image to the corresponding User.profilePicture in the core data stack.
Is there a way for controllerDidChangeContent to be called when I update the User entity?? My current understanding is that my NSFetchedResultsController can only follow the Post entity since that is what I initially set it to do and cannot traverse and monitor updates across a relationship, is that true?
Sadly I know only of an UGLY solution for this issue.
In your User .m file implements the setProfilePicture: like this:
//NOT TESTED IN A MULTITHREADED ENV
- (void) setProfilePicture:(NSData *)data
{
[self willChangeValueForKey:#"profilePicture"];
[self setPrimitiveValue:data forKey:#"profilePicture"];
[self.posts enumerateObjectsUsingBlock:^(Post* p, BOOL *stop) {
[p willChangeValueForKey:#"user"];
[p didChangeValueForKey:#"user"];
}];
[self didChangeValueForKey:#"profilePicture"];
}
This will notify the FRC that the Post element has changes.
You might find additional information here
Edit:
To fetch the data on access you can add this to your User.m:
//UNTESTED
+ (void) mergeToMain:(NSNotification*)notification
{
AppDelegate* appDel = (AppDelegate*)[[UIApplication sharedApplication] delegate];
[appDel.managedObjectContext performSelectorOnMainThread:#selector(mergeChangesFromContextDidSaveNotification:)
withObject:notification
waitUntilDone:YES];
}
- (NSData*)_profilePicture
{
return [self primitiveValueForKey:#"profilePicture"];
}
- (NSData*) profilePicture
{
[self willAccessValueForKey:#"profilePicture"];
NSData* picData = [self primitiveValueForKey:#"profilePicture"];
if (!name) {
__block NSManagedObjectID* objectID = self.objectID;
//This solves the multiple downloads per item by using a single queue
//for all profile pictures download.
//There are more concurrent ways to accomplish that
dispatch_async(downloadSerialQueue, ^{ //define some serial queue for assuring you down download multiple times the same object
NSError* error = nil;
AppDelegate* appDel = (AppDelegate*)[[UIApplication sharedApplication] delegate];
NSManagedObjectContext* context = [[NSManagedObjectContext alloc] init];
[context setPersistentStoreCoordinator:appDel.persistentStoreCoordinator];
[context setUndoManager:nil];
User* user = (User*)[context existingObjectWithID:objectID error:&error];
if (user && [user _profilePicture] == nil) {
NSData *data = //[method to retrieve data from server];
if (data) {
if (user) {
user.profilePicture = data;
} else {
NSLog(#"ERROR:: error fetching user: %#",error);
return;
}
[[NSNotificationCenter defaultCenter] addObserver:[self class] selector:#selector(mergeToMain:) name:NSManagedObjectContextDidSaveNotification object:context];
[context save:&error];
[[NSNotificationCenter defaultCenter] removeObserver:[self class] name:NSManagedObjectContextDidSaveNotification object:context];
}
}
});
}
[self didAccessValueForKey:#"profilePicture"];
return picData;
}
I think this issue can be solved without NSFetchedResultsController involved.
use SDWebImage, SDWebImage can load images from remote server asynchronously, just do this:
[myImageView setImageWithURL:[NSURL URLWithString:#"http://www.domain.com/path/to/image.jpg"]
placeholderImage:[UIImage imageNamed:#"placeholder.png"]];
use KVO, add a observer to User entity and update corresponding image view accordingly. But the code for KVO is rather complex, ReactiveCocoa can simplify them:
[RACAble(user.profilePicture) subscribeNext:^(UIImage *image) {
[myImageView setImage:image];
}];

Resources