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.
Related
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
I am trying to backup a Core Data SQLite database. This code successfully processes the running database and merges the WAL file. Unfortunately, everytime it runs I see a bump of about 3-5 MB in my memory footprint. This is causing issues after the program has run for a while. Can someone help me reclaim the memory? I thought setting everything to nil would dealloc all of the objects from RAM, but that doesn't seem to be it.
-(void) backupDatabaseWithThisTimeStamp: (int) timeStamp withCompletionBlock:(void (^)(void))completion {
NSDate *backupDate = [NSDate date];
NSError *error;
[self.defaultPrivateQueueContext save:&error];
if (error) {
NSLog(#"error -> %#",error);
}
dispatch_async(self.backupQueue, ^{
// Let's use the existing PSC
NSPersistentStoreCoordinator *migrationPSC = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
// Open the store
id sourceStore = [migrationPSC addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:[self persistentStoreURL] options:nil error:nil];
if (!sourceStore) {
NSLog(#" failed to add store");
migrationPSC = nil;
} else {
NSLog(#" Successfully added store to migrate");
NSError *error;
NSLog(#" About to migrate the store...");
id migrationSuccess = [migrationPSC migratePersistentStore:sourceStore toURL:[self backupStoreURLwithTimeStamp: timeStamp] options:[self localStoreOptions] withType:NSSQLiteStoreType error:&error];
if (migrationSuccess) {
NSLog(#"store successfully backed up");
// Now reset the backup preference
NSManagedObjectContext *tempContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
tempContext.persistentStoreCoordinator = migrationPSC;
tempContext.undoManager = nil;
// clip out data
[CDrawColorData purgeDataOlderThan:backupDate fromContext:tempContext];
migrationPSC = nil;
tempContext = nil;
}
else {
NSLog(#"Failed to backup store: %#, %#", error, error.userInfo);
migrationPSC = nil;
}
}
migrationPSC = nil;
dispatch_async(dispatch_get_main_queue(), ^{
if (completion) {
completion();
}
});
});
}
self.backupQueue = _backupQueue = dispatch_queue_create("backup.queue", DISPATCH_QUEUE_SERIAL);
localStoreOptions =
- (NSDictionary*)localStoreOptions {
return #{NSMigratePersistentStoresAutomaticallyOption:#YES,
NSInferMappingModelAutomaticallyOption:#YES,
NSSQLitePragmasOption:#{ #"journal_mode" : #"DELETE" }};
}
commenting out everything that happens after the migrationSuccess point does not effect the memory footprint.
All of the issues that I've seen are directly accountable to the Xcode Scheme.
Product->Scheme
Select Run-Options
UNCHECK -> Queue Debugging (enable backtrack recording)
Once this was done, all of the memory footprint growth immediately disappeared.
Due to too few reps i'm writing this as an "answer" (even if it might not be the solution): I think it's good practice to think about "cache_size" as another NSSQLitePragmasOption and to limit it accordingly:
NSSQLitePragmasOption:#{ #"journal_mode" : #"DELETE", #"cache_size" : #"50"}
see www.sqlite.org for declarations
I think it's likely to be because all relationships in a Core Data object graph are strong references. So you're all but guaranteed retain cycles. For further diagnostics you should use Instruments' leaks tool and see what type of object appears to be leaking.
You're therefore likely to want to call reset on any instances you create of NSManagedObjectContext before discarding them. This will forcibly fault all active objects, thereby breaking any retain cycles (unless you access the objects again, naturally). It's not explicit in the documentation that a dealloc automatically prompts a reset and therefore it doesn't appear to be a contractual guarantee.
I have method called collectData in my app which is the most important part of my View Controller. In that method I do a couple of signicant things (downloading, parsing, saving to persistent store), so it would be easier for you to take a look:
-(void)collectData
{
// Downloading all groups and saving them to Core Data
[[AFHTTPRequestOperationManager manager] GET:ALL_GROUPS parameters:nil success:^(AFHTTPRequestOperation *operation, id responseObject) {
NSMutableDictionary* groups = [NSMutableDictionary new];
NSMutableArray* newIds = [NSMutableArray new];
NSError *error;
// Saving everything from response to MOC
for (id group in responseObject) {
Group *groupEntity = [NSEntityDescription insertNewObjectForEntityForName:#"Group" inManagedObjectContext:self.moc];
groupEntity.name = [group valueForKey:#"name"];
groupEntity.cashID = [group valueForKey:#"id"];
groupEntity.caseInsensitiveName = [[group valueForKey:#"name"] lowercaseString];
groupEntity.selected = #NO;
// Filling up helping variables
groups[groupEntity.cashID] = groupEntity;
[newIds addObject:groupEntity.cashID];
}
// Fetching existing groups from Persistant store
NSFetchRequest* r = [NSFetchRequest fetchRequestWithEntityName:#"Group"];
[r setIncludesPendingChanges:NO];
r.predicate = [NSPredicate predicateWithFormat:#"cashID IN %#",newIds];
NSArray *existingGroups = [self.moc executeFetchRequest:r error:&error];
// Deleting groups which already are in database
for (Group* g in existingGroups) {
Group* newGroup = groups[g.cashID];
g.name = [newGroup valueForKey:#"name"];
g.cashID = [newGroup valueForKey:#"cashID"];
g.caseInsensitiveName = [[newGroup valueForKey:#"name"] lowercaseString];
[self.moc deleteObject:newGroup];
}
// Saving Entity modification date and setting it to pull to refresh
[self saveModificationDate:[NSDate date] forEntityNamed:#"Group"];
[self.pullToRefreshView.contentView setLastUpdatedAt:[self getModificationDateForEntityNamed:#"Group"]
withPullToRefreshView:self.pullToRefreshView];
// Save groups to presistant store
if (![self.moc save:&error]) {
NSLog(#"Couldn't save: %#", [error localizedDescription]);
}
[[self fetchedResultsController] performFetch:&error];
[self.pullToRefreshView finishLoading];
[self.tableView reloadData];
} failure:^(AFHTTPRequestOperation *operation, NSError *error) {
// Show alert with info about internet connection
[self.pullToRefreshView finishLoading];
UIAlertView *internetAlert = [[UIAlertView alloc] initWithTitle:#"Ups!" message:#"Wygląda na to, że nie masz połączenia z internetem" delegate:self cancelButtonTitle:#"OK" otherButtonTitles:nil, nil];
[internetAlert show];
}];
}
So when I start collecting data (first run or push to refresh) this method is blocking UI.
I want to avoid this but when I put the success block into another dispatch_async and get back to main queue only for [self.tableView reloadData] I face problem with saving to persistent store or something with bad indexes.
How can I do this whole thing in background and leave UI responsive to the user?
Just an idea, give it a try using dispatch_sync. Have a look at this explanation here where log result something similar to your need. Put [yourTableView reloadData] after synchronous block.
Hope it helps!
It seems AFNetwork call is not async so just try to call your method via performselector.
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];
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!