I have the following problem.
I have successfully created my iCloud Magical Record Store and also works great if you filled it from zero :)
But now I have many data in the old Magical Record Local Store and I want to take this to the iCloud store.
But how i can migrate them.
if ([[NSFileManager defaultManager] ubiquityIdentityToken]) {
[self iCloudCoreDataSetup]}
else {
NSLog(#"iCloud not enabled");
[MagicalRecord setupAutoMigratingCoreDataStack];
}
Here i am checking iCloud is available. Before iCloud, i have always initialized with
[MagicalRecord setupAutoMigratingCoreDataStack];
Here is my code for the iCloud Installation
- (void)iCloudCoreDataSetup {
// containerID should contain the same string as your iCloud entitlements
NSString *containerID = [NSString stringWithFormat:#"iCloud.%#", [[NSBundle mainBundle] bundleIdentifier]];
[MagicalRecord setupCoreDataStackWithiCloudContainer:containerID
contentNameKey:#"App" // Must not contain dots
localStoreNamed:#"App.sqlite"
cloudStorePathComponent:#"Documents/CloudLogs" // Subpath within your ubiquitous container that will contain db change logs
completion:^{
// This gets executed after all the setup steps are performed
// Uncomment the following lines to verify
NSLog(#"%#", [MagicalRecord currentStack]);
// NSLog(#"%i events", [Event countOfEntities]);
}];
// NOTE: MagicalRecord's setup is asynchronous, so at this point the default persistent store is still probably NIL!
// Uncomment the following line if you want to check it.
NSLog(#"%#", [MagicalRecord currentStack]);
// The persistent store COORDINATOR is however fully setup and can be accessed
// Uncomment the following line if you want to check it.
NSLog(#"Store coordinator at this point %#", [NSPersistentStoreCoordinator MR_defaultStoreCoordinator]);
////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Finally, store change notifications must be observed. Without these, your app will NOT function properly!
////////////////////////////////////////////////////////////////////////////////////////////////////////////
// This notification is issued only once when
// 1) you run your app on a particular device for the first time
// 2) you disable/enable iCloud document storage on a particular device
// usually a couple of seconds after the respective event.
// The notification must be handled on the MAIN thread and synchronously
// (because as soon as it finishes, the persistent store is removed by OS).
// Refer to Apple's documentation for further details
[[NSNotificationCenter defaultCenter] addObserverForName:NSPersistentStoreCoordinatorStoresWillChangeNotification
object:[NSPersistentStoreCoordinator MR_defaultStoreCoordinator]
// queue:nil // Run on the posting (i.e. background) thread
queue:[NSOperationQueue mainQueue] // Run on the main thread
usingBlock:^(NSNotification *note) {
// For debugging only
NSLog(#"%s notificationBlockWillChange:%#, isMainThread = %i", __PRETTY_FUNCTION__, note, [NSThread isMainThread]);
// Disable user interface with setEnabled: or an overlay
// NOTE: Probably not crucial since the store switch is almost instantaneous.
// I only hint it here by changing the tint color to red.
self.window.tintColor = [UIColor redColor];
// Save changes to current MOC and reset it
if ([[NSManagedObjectContext MR_defaultContext] hasChanges]) {
[[NSManagedObjectContext MR_defaultContext] MR_saveToPersistentStoreAndWait];
}
[[NSManagedObjectContext MR_defaultContext] reset];
// TODO: Drop any managed object references here
}];
// This notification is issued couple of times every time your app starts
// The notification must be handled on the BACKGROUND thread and asynchronously to prevent deadlock
// Refer to Apple's documentation for further details
[[NSNotificationCenter defaultCenter] addObserverForName:NSPersistentStoreCoordinatorStoresDidChangeNotification
object:[NSPersistentStoreCoordinator MR_defaultStoreCoordinator]
queue:nil // Run on the posting (i.e. background) thread
usingBlock:^(NSNotification *note) {
// For debugging only
NSLog(#"%s notificationBlockDidChange:%#, isMainThread = %i", __PRETTY_FUNCTION__, note, [NSThread isMainThread]);
// This block of code must be executed asynchronously on the main thread!
dispatch_async(dispatch_get_main_queue(), ^{
// Recommended by Apple
[[NSManagedObjectContext MR_defaultContext] reset];
// Notify UI that the data has changes
// NOTE: I am using the same notification that MagicalRecord sends after merging changes
[[NSNotificationCenter defaultCenter] postNotificationName:kMagicalRecordPSCDidCompleteiCloudSetupNotification object:nil];
// Re-enable user interface with setEnabled: or removing the overlay
// NOTE: Probably not crucial since the store switch is almost instantaneous.
// I only hint it here by changing the tint color back to default.
self.window.tintColor = nil;
});
}];
}
But how can I take the old data from the local store to iCloud?
Thanks for your help.
Related
I Added ios-8's new touchID API to my app.
It usually works as expected, BUT when entering app while my finger is already on home-button - API's success callback is called but pop-up still appears on screen. after pressing CANCEL UI becomes non-responsive.
I also encountered the same issue, and the solution was to invoke the call to the Touch ID API using a high priority queue, as well as a delay:
// Touch ID must be called with a high priority queue, otherwise it might fail.
// Also, a dispatch_after is required, otherwise we might receive "Pending UI mechanism already set."
dispatch_queue_t highPriorityQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 0.75 * NSEC_PER_SEC), highPriorityQueue, ^{
LAContext *context = [[LAContext alloc] init];
NSError *error = nil;
// Check if device supports TouchID
if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&error]) {
// TouchID supported, show it to user
[context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics
localizedReason:#"Unlock Using Touch ID"
reply:^(BOOL success, NSError *error) {
if (success) {
// This action has to be on main thread and must be synchronous
dispatch_async(dispatch_get_main_queue(), ^{
...
});
}
else if (error) {
...
}
}];
}
});
When testing our app, we found a delay of 750ms to be optimal, but your mileage may vary.
Update (03/10/2015): Several iOS developers, like 1Password for example, are reporting that iOS 8.2 have finally fixed this issue.
Whilst using a delay can potentially address the issue, it masks the root cause. You need to ensure you only show the Touch ID dialog when the Application State is Active. If you display it immediately during the launch process (meaning the Application is still technically in an inactive state), then these sorts of display issues can occur. This isn't documented, and I found this out the hard way. Providing a delay seems to fix it because you're application is in an active state by then, but this isn't guarenteed.
To ensure it runs when the application is active, you can check the current application state, and either run it immediately, or when we receive the applicationDidBecomeActive notification. See below for an example:
- (void)setup
{
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(applicationDidBecomeActive:)
name:UIApplicationDidBecomeActiveNotification
object:nil];
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
// We need to be in an active state for Touch ID to play nice
// If we're not, defer the presentation until we are
if([UIApplication sharedApplication].applicationState == UIApplicationStateActive)
{
[self presentTouchID];
}
else
{
__weak __typeof(self) wSelf = self;
_onActiveBlock = ^{
[wSelf presentTouchID];
};
}
}
-(void)applicationDidBecomeActive:(NSNotification *)notif
{
if(_onActiveBlock)
{
_onActiveBlock();
_onActiveBlock = nil;
}
}
- (void)presentTouchID
{
_context = [[LAContext alloc] init];
_context.localizedFallbackTitle = _fallbackTitle;
[_context evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics
localizedReason:_reason
reply: ^(BOOL success, NSError *authenticationError)
{
// Handle response here
}];
}
This accepted answer does not address the underlying cause of the problem: invoking evaluatePolicy() twice, the second time while the first invocation is in progress. So the current solution only works sometimes by luck, as everything is timing dependent.
The brute-force, straightforward way to work around the problem is a simple boolean flag to prevent subsequent calls from happening until the first completes.
AppDelegate *delegate = [[UIApplication sharedApplication] delegate];
if ( NSClassFromString(#"LAContext") && ! delegate.touchIDInProgress ) {
delegate.touchIDInProgress = YES;
LAContext *localAuthenticationContext = [[LAContext alloc] init];
__autoreleasing NSError *authenticationError;
if ([localAuthenticationContext canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics error:&authenticationError]) {
[localAuthenticationContext evaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics localizedReason:kTouchIDReason reply:^(BOOL success, NSError *error) {
delegate.touchIDInProgress = NO;
if (success) {
...
} else {
...
}
}];
}
I started getting the "Pending UI mechanism already set." error mentioned as well, so I decided to see if other apps were affected. I have both Dropbox and Mint set up for Touch ID. Sure enough Touch ID wasn't working for them either and they were falling back to passcodes.
I rebooted my phone and it started working again, so it would seem the Touch ID can bug out and stop working. I'm on iOS 8.2 btw.
I guess the proper way to handle this condition is like those apps do and fallback to password / passcode.
I am relatively new to Core Data and am looking for some advice.
My project: I have a few object here:
- SMCoreData which is largely the auto generated code around a CoreData
project
- SMUploader which indexes assets in ALAssetLibrary, writes the results to SMCoreData, and has a queue mechanism to upload photos.
- SMUploaderViewController a UICollectionView and a few switches which show the progress of the queue.
These classes work great for the most part. When a user launches the app, their Camera Roll is indexed and uploading begins. It will successfully upload 1000's of photos.
The problem comes later when more is added to the upload queue. I'm observing changes to the ALAssetLibrary (say someone saves a photo using Safari). My app will receive the notification, queue the image, and try to upload it when the user flips the switch (same as earlier).
This is where I am running into a quirk. I first grab the next object from core data, acuire the asset from a URL (core data object), then copy the file to my sandbox. I then go to update CoreData with the filesize and temporaryFilePath. However the call to [context save] never returns (however it has already worked great 1000s of times before). The processor is at 0% and it just sits there not returning. Sounds like a queue problem at first, however I am using background queues:
MYCoreData is mainly the boilerplate code from a CoreData project. I moved it to a class to encapsulate it. I did change how the managedObjectContext is created though:
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
MYUploader has two queues. One for indexing the asset library, and one for processing the upload queue.
MYUploaderViewController has a registers a few calllback blocks with MYUploader so that UI updates can be rendered. These calls are always done from the main queue withing MYUploader.
A little code:
-(void)uploadAsset:(SMUploadAsset*)uploadAsset completion:(SMUploaderBoolBlock)completion{
// 1.) Dump asset to a temporary file to operate with
// 1a) Update core data with temporaryFile, size, and md5
// 2.) Create asset on our server
// 2a) Update core data with s3URL
// 3.) Upload file to s3RUL
// 4.) Tell our server that the upload has completed
// 5.) Update core data with uploaded and uploadedDate
// 6.) Delete temporary file
// 7.) Remove the uploadAsset from the queue
NSLog(#"%s", __func__);
if(uploadAsset.image){
NSLog(#"........................................................... Uploading Image ................................................ ");
uploadAsset.uploadQueueState = SMUploadAssetQueueStateInProgress;
// 1.) Dump asset to a temporary file to operate with
NSLog(#"Upload step 1. ");
NSURL *assetURL = [NSURL URLWithString:[NSString stringWithFormat:#"%#", uploadAsset.image.uri]];
NSLog(#"--- UPLOAD 1.) Dumping file");
[SMUploaderImageRipper bytesFromObject:assetURL completion:^(SMUploadProperties *uploadProperties) {
if(uploadProperties == nil){
NSLog(#"Upload step 1 FAILED");
uploadAsset.uploadQueueState = SMUploadAssetQueueStateFailed;
NSLog(#"****** UPLOAD ERROR: Failed to dump asset to temporary file");
completion(NO);
return;
}
NSLog(#"Upload step 1 complete");
// 1a) Update core data with temporaryFile, size, and md5
NSLog(#"Upload step 1a. Updating core data with temp file, size, and md5");
[self updateUploadAsset:uploadAsset uploadProperties:uploadProperties completion:^(BOOL success) {
if(success == NO){
NSLog(#"Upload step 1a FAILED");
uploadAsset.uploadQueueState = SMUploadAssetQueueStateFailed;
NSLog(#"****** UPLOAD ERROR: Failed to add temporaryFile, size, and md5 to CoreData");
completion(NO);
return;
}
NSLog(#"Upload step 1a complete");
....
And then the method that updates CoreData object:
-(void)updateUploadAsset:(SMUploadAsset*)uploadAsset
uploadProperties:(SMUploadProperties*)uploadProperties
completion:(SMUploaderBoolBlock)completion{
SMCoreData *coreData = [SMCoreData sharedInstance];
NSManagedObjectContext *context = [coreData managedObjectContext];
uploadAsset.md5 = uploadProperties.hash;
uploadAsset.temporaryFile = uploadProperties.temporaryFile;
uploadAsset.size = uploadProperties.size;
NSError *cdError;
// THIS CALL NEVER RETURNS
if (![context save:&cdError]) {
NSLog(#"Whoops, couldn't save: %#", [cdError localizedDescription]);
completion(NO);
}
completion(YES);
}
I am happy to post more code or answer questions as I am quite stuck.
UPDATE: It seems to me that it's the ALAssetLibarary observer that's causing this problem somehow. Here is what I am doing for the notification. I've tried using the main queue as well as a different background queue I have, but always this is what gets CoreData into the funky state. If I comment this out, then run the app as described above (go to safari, save an image, return) but instead of receiving this notification, I just press a button which calls this same code (enqueueCameraRollWithCompletion), everthing works great.
What could be the solution here? I would really like to keep this feature in.
[[NSNotificationCenter defaultCenter] addObserverForName:ALAssetsLibraryChangedNotification
object:nil
queue:[NSOperationQueue mainQueue]
usingBlock:^(NSNotification *note) {
dispatch_async(self.processingQueue, ^{
[self enqueueCameraRollWithCompletion:^(BOOL success) {
}];
});
}];
EDIT: Requested code update using performBlockAndWait
-(void)updateUploadAsset:(SMUploadAsset*)uploadAsset
uploadProperties:(SMUploadProperties*)uploadProperties
completion:(SMUploaderBoolBlock)completion{
SMCoreData *coreData = [SMCoreData sharedInstance];
NSManagedObjectContext *context = [coreData managedObjectContext];
[context performBlockAndWait:^{
uploadAsset.md5 = uploadProperties.hash;
uploadAsset.temporaryFile = uploadProperties.temporaryFile;
uploadAsset.size = uploadProperties.size;
if ([coreData saveContext]){
completion(YES);
}
else{
completion(NO);
}
}];
}
I have an iOS app which is accessing a core data sql database from two threads. Thread A (the main UI thread) updates a core data record, and Thread B then attempts to read from the Entity collection that Thread A has just updated. Trouble is, Thread B is not 'seeing' the change that Thread A persisted.
Thread B is created by adding an NSOperation subclass object to an NSOperationQueue. The main method of the NSOperation subclass looks like this:
-(void) main {
// NEED to create the MOC here and pass to the methods.
NSManagedObjectContext* moc = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
[moc setUndoManager:nil];
[moc setMergePolicy:NSMergeByPropertyStoreTrumpMergePolicy]; // Had been working for months
[moc setPersistentStoreCoordinator:getApp().persistentStoreCoordinator];
[self doTheWorkWithMOC:moc]; // Actually performs updates, using moc
moc = nil;
}
Later, Thread B saves its changes as follows:
#try {
// register for the moc save notification - this is so that other MOCs can be told to merge the changes
[[NSNotificationCenter defaultCenter]
addObserver:getApp()
selector:#selector(handleDidSaveNotification:)
name:NSManagedObjectContextDidSaveNotification
object:moc];
NSError* error = nil;
if ([moc save:&error] == YES)
{
NSLog(#"%s SAVED FINE",__FUNCTION__);
}else {
NSLog(#"%s NOT saved, error=%# %#",__FUNCTION__,error,[error localizedDescription]);
}
// unregister from notification
[[NSNotificationCenter defaultCenter]
removeObserver:getApp()
name:NSManagedObjectContextDidSaveNotification
object:moc];
}
#catch (NSException * e) {
NSLog(#"%s Exception: %#",__FUNCTION__, e);
}
The main UI appdelegate contains the following code to handle the save notification:
- (void)handleDidSaveNotification:(NSNotification*) note
{
#try {
// Notifications run on the same thread as the notification caller.
// However, we need to ensure that any db merges run on the main ui thread.
// Hence:
[self performSelectorOnMainThread:#selector(mergeContexts:) withObject:note waitUntilDone:NO];
}
#catch (NSException * e) {
NSLog(#"appDelegate handleDidSaveNotification Exception: %#", e);
}
}
-(void)mergeContexts:(NSNotification*) note
{
if ([__managedObjectContext tryLock]==YES)
{
[__managedObjectContext mergeChangesFromContextDidSaveNotification:note];
[__managedObjectContext unlock];
}
}
It all works fine most of the time.
However, I have one iPad where the changes written by Thread B are not detected when Thread A reads the database.
Can anyone see anything in my code which would cause this?
Many thanks
Journeyman,
First, you should move to using the queue based MOCs introduced in iOS v5 and Lion. This will make it much easier to keep your two MOCs in sync. You will no longer need to use the locking system.
Second, once you've moved to the queued MOCs, then it is quite straightforward to keep them in sync in response to the "did save" notifications.
Third, why are you always adding and removing the observer for the did save notifications? Doesn't that look suspicious to you? Clearly, you are missing some update between the MOCs.
Andrew
What I'm trying todo in a nutshell is I am using a background queue to save JSON objects pulled from a web service to the Core Data Sqlite3 database. The saving takes place on a serialized background queue I've created via GCD, and saved to a secondary instance of NSManagedObjectContext that is created for that background queue. Once the save is complete I need to update the instance of NSManagedObjectContext that is on the main thread with the newly created/updated objects. The problem I am having though is the instance of NSManagedObjectContext on the main thread is not able to find the objects that were saved on the background context. Below is a list of actions I'm taking with code samples. Any thoughts on what I'm doing wrong?
Create a background queue via GCD, run all pre-processing logic and then save the background context on that thread:
.
// process in the background queue
dispatch_async(backgroundQueue, ^(void){
if (savedObjectIDs.count > 0) {
[savedObjectIDs removeAllObjects];
}
if (savedObjectClass) {
savedObjectClass = nil;
}
// set the thead name
NSThread *currentThread = [NSThread currentThread];
[currentThread setName:VS_CORE_DATA_MANAGER_BACKGROUND_THREAD_NAME];
// if there is not already a background context, then create one
if (!_backgroundQueueManagedObjectContext) {
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil) {
_backgroundQueueManagedObjectContext = [[NSManagedObjectContext alloc] init];
[_backgroundQueueManagedObjectContext setPersistentStoreCoordinator:coordinator];
}
}
// save the JSON dictionary starting at the upper most level of the key path, and return all created/updated objects in an array
NSArray *objectIds = [self saveJSON:jsonDict objectMapping:objectMapping class:managedObjectClass managedObjectContext:_backgroundQueueManagedObjectContext level:0];
// save the object IDs and the completion block to global variables so we can access them after the save
if (objectIds) {
[savedObjectIDs addObjectsFromArray:objectIds];
}
if (completion) {
saveCompletionBlock = completion;
}
if (managedObjectClass) {
savedObjectClass = managedObjectClass;
}
// save all changes object context
[self saveManagedObjectContext];
});
The "saveManagedObjectContext" method basically looks at which thread is running and saves the appropriate context. I have verified that this method is working correctly so I will not place the code here.
All of this code resides in a singleton, and in the singleton's init method I am adding a listener for the "NSManagedObjectContextDidSaveNotification" and it calls the mergeChangesFromContextDidSaveNotification: method
.
// merge changes from the context did save notification to the main context
- (void)mergeChangesFromContextDidSaveNotification:(NSNotification *)notification
{
NSThread *currentThread = [NSThread currentThread];
if ([currentThread.name isEqual:VS_CORE_DATA_MANAGER_BACKGROUND_THREAD_NAME]) {
// merge changes to the primary context, and wait for the action to complete on the main thread
[_managedObjectContext performSelectorOnMainThread:#selector(mergeChangesFromContextDidSaveNotification:) withObject:notification waitUntilDone:YES];
// on the main thread fetch all new data and call the completion block
dispatch_async(dispatch_get_main_queue(), ^{
// get objects from the database
NSMutableArray *objects = [[NSMutableArray alloc] init];
for (id objectID in savedObjectIDs) {
NSError *error;
id object = [_managedObjectContext existingObjectWithID:objectID error:&error];
if (error) {
[self logError:error];
} else if (object) {
[objects addObject:object];
}
}
// remove all saved object IDs from the array
[savedObjectIDs removeAllObjects];
savedObjectClass = nil;
// call the completion block
//completion(objects);
saveCompletionBlock(objects);
// clear the saved completion block
saveCompletionBlock = nil;
});
}
}
As you can see in the method above I am calling the "mergeChangesFromContextDidSaveNotification:" on the main thread, and I have set the action to wait until done. According to the apple documentation the background thread should wait until that action is complete before it continues with the rest of the code below that call. As I mentioned above once I run this code everything seems to work, but when I try to print out the fetched objects to the console I don't get anything back. It seems that the merge is not in fact taking place, or possibly not finishing before the rest of my code runs. Is there another notification that I should be listening for to ensure that the merge has completed? Or do I need to save the main object context after the merge, but before the fecth?
Also, I apologize for the bad code formatting, but it seems that SO's code tags don't like method definitions.
Thanks guys!
UPDATE:
I've made the changes that were recommended below, but still having the same problem. Below is the updated code I have.
This is the code that invokes the background thread saving processes
// process in the background queue
dispatch_async(backgroundQueue, ^(void){
if (savedObjectIDs.count > 0) {
[savedObjectIDs removeAllObjects];
}
if (savedObjectClass) {
savedObjectClass = nil;
}
// set the thead name
NSThread *currentThread = [NSThread currentThread];
[currentThread setName:VS_CORE_DATA_MANAGER_BACKGROUND_THREAD_NAME];
// if there is not already a background context, then create one
if (!_backgroundQueueManagedObjectContext) {
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil) {
_backgroundQueueManagedObjectContext = [[NSManagedObjectContext alloc] init];
[_backgroundQueueManagedObjectContext setPersistentStoreCoordinator:coordinator];
}
}
// save the JSON dictionary starting at the upper most level of the key path
NSArray *objectIds = [self saveJSON:jsonDict objectMapping:objectMapping class:managedObjectClass managedObjectContext:_backgroundQueueManagedObjectContext level:0];
// save the object IDs and the completion block to global variables so we can access them after the save
if (objectIds) {
[savedObjectIDs addObjectsFromArray:objectIds];
}
if (completion) {
saveCompletionBlock = completion;
}
if (managedObjectClass) {
savedObjectClass = managedObjectClass;
}
// listen for the merge changes from context did save notification
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(mergeChangesFromBackground:) name:NSManagedObjectContextDidSaveNotification object:_backgroundQueueManagedObjectContext];
// save all changes object context
[self saveManagedObjectContext];
});
This is the code that is called with by the NSManagedObjectContextDidSaveNotification notification
// merge changes from the context did save notification to the main context
- (void)mergeChangesFromBackground:(NSNotification *)notification
{
// kill the listener
[[NSNotificationCenter defaultCenter] removeObserver:self name:NSManagedObjectContextDidSaveNotification object:_backgroundQueueManagedObjectContext];
NSThread *currentThread = [NSThread currentThread];
// merge changes to the primary context, and wait for the action to complete on the main thread
[[self managedObjectContext] performSelectorOnMainThread:#selector(mergeChangesFromContextDidSaveNotification:) withObject:notification waitUntilDone:YES];
// dispatch the completion block
dispatch_async(dispatch_get_main_queue(), ^{
// get objects from the database
NSMutableArray *objects = [[NSMutableArray alloc] init];
for (id objectID in savedObjectIDs) {
NSError *error;
id object = [[self managedObjectContext] existingObjectWithID:objectID error:&error];
if (error) {
[self logError:error];
} else if (object) {
[objects addObject:object];
}
}
// remove all saved object IDs from the array
[savedObjectIDs removeAllObjects];
savedObjectClass = nil;
// call the completion block
//completion(objects);
saveCompletionBlock(objects);
// clear the saved completion block
saveCompletionBlock = nil;
});
}
UPDATE:
So I found the solution. Turns out that the way I was saving out the object IDs on the background thread and then trying to use them on the main thread to re-fetch them wasn't working out. So I ended up pulling the inserted/updated objects from the userInfo dictionary that is sent with the NSManagedObjectContextDidSaveNotification notification. Below is my updated code that is now working.
As before this code starts the pre-prossesing and saving logic
// process in the background queue
dispatch_async(backgroundQueue, ^(void){
// set the thead name
NSThread *currentThread = [NSThread currentThread];
[currentThread setName:VS_CORE_DATA_MANAGER_BACKGROUND_THREAD_NAME];
[self logMessage:[NSString stringWithFormat:#"(%#) saveJSONObjects:objectMapping:class:completion:", [managedObjectClass description]]];
// if there is not already a background context, then create one
if (!_backgroundQueueManagedObjectContext) {
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil) {
_backgroundQueueManagedObjectContext = [[NSManagedObjectContext alloc] init];
[_backgroundQueueManagedObjectContext setPersistentStoreCoordinator:coordinator];
}
}
// save the JSON dictionary starting at the upper most level of the key path
[self saveJSON:jsonDict objectMapping:objectMapping class:managedObjectClass managedObjectContext:_backgroundQueueManagedObjectContext level:0];
// save the object IDs and the completion block to global variables so we can access them after the save
if (completion) {
saveCompletionBlock = completion;
}
// listen for the merge changes from context did save notification
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(mergeChangesFromBackground:) name:NSManagedObjectContextDidSaveNotification object:_backgroundQueueManagedObjectContext];
// save all changes object context
[self saveManagedObjectContext];
});
This is the modified method that handles the NSManagedObjectContextDidSaveNotification
- (void)mergeChangesFromBackground:(NSNotification *)notification
{
// kill the listener
[[NSNotificationCenter defaultCenter] removeObserver:self name:NSManagedObjectContextDidSaveNotification object:_backgroundQueueManagedObjectContext];
// merge changes to the primary context, and wait for the action to complete on the main thread
[[self managedObjectContext] performSelectorOnMainThread:#selector(mergeChangesFromContextDidSaveNotification:) withObject:notification waitUntilDone:YES];
// dispatch the completion block
dispatch_async(dispatch_get_main_queue(), ^{
// pull the objects that were saved from the notification so we can get them on the main thread MOC
NSDictionary *userInfo = [notification userInfo];
NSMutableArray *modifiedObjects = [[NSMutableArray alloc] init];
NSSet *insertedObject = (NSSet *)[userInfo objectForKey:#"inserted"];
NSSet *updatedObject = (NSSet *)[userInfo objectForKey:#"updated"];
if (insertedObject && insertedObject.count > 0) {
[modifiedObjects addObjectsFromArray:[insertedObject allObjects]];
}
if (updatedObject && updatedObject.count > 0) {
[modifiedObjects addObjectsFromArray:[updatedObject allObjects]];
}
NSMutableArray *objects = [[NSMutableArray alloc] init];
// iterate through the updated objects and find them in the main thread MOC
for (NSManagedObject *object in modifiedObjects) {
NSError *error;
NSManagedObject *obj = [[self managedObjectContext] existingObjectWithID:object.objectID error:&error];
if (error) {
[self logError:error];
}
if (obj) {
[objects addObject:obj];
}
}
modifiedObjects = nil;
// call the completion block
saveCompletionBlock(objects);
// clear the saved completion block
saveCompletionBlock = nil;
});
}
I'm going to throw this out there. Stop following the best practices for concurrency listed in the Core Data Programming Guide. Apple has not updated it since adding nested contexts which are MUCH easier to use. This video goes into full detail: https://developer.apple.com/videos/wwdc/2012/?id=214
Setup your primary context to use your main thread (appropriate for handling UI):
NSManagedObjectContext * context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[context setPersistentStoreCoordinator:yourPSC];
For any object you create that may be doing concurrent operations, create a private queue context to use
NSManagedObjectContext * backgroundContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[backgroundContext setParentContext:context];
//Use backgroundContext to insert/update...
//Then just save the context, it will automatically sync to your primary context
[backgroundContext save:nil];
The QueueConcurrencyType refers to the queue the context will do it's fetch (save and fetch request) operations on. The NSMainQueueConcurrencyType context does all it's work on the main queue, which makes it appropriate for UI interaction. A NSPrivateQueueConcurrencyType does it on it's own private queue. So when you call save on the backgroundContext, it merges it's private data calling the parentContext using performBlock as appropriate automatically. You don't want to call performBlock on the private queue context in case it happens to be on the main thread which will cause a deadlock.
If you want to get really fancy, You can create a primary context as a private queue concurrency type (which is appropriate for background saving) with a main queue context for just your UI and then child contexts of your main queue context for background operations (like imports).
I see you've worked out an answer that works for you. But I have been having some similar issues and wanted to share my experience and see if it is at all helpful to you or others looking at this situation.
Multi-threaded Core Data stuff is always a little confusing to read, so please excuse me if I misread your code. But it appears that there could be a simpler answer for you.
The core issue you had in the first attempt is that you saved off managed object IDs (supposedly the object identifiers that can be passed between threads) to a global variable for use on the main thread. You did this on a background thread. The problem was that you did this BEFORE saving to the background thread's managed object context. Object IDs are not safe to pass to another thread/context pair prior to a save. They can change when you save. See the warning in the documentation of objectID: NSManagedObject reference
You fixed this by notifying your background thread of the save, and inside that thread, grabbing the now-safe-to-use-because-the-context-has-been-saved object IDs from the notification object. These were passed to the main thread, and the actual changes were also merged into the main thread with the call to mergeChangesFromContextDidSaveNotification. Here's where you might save a step or two.
You are registering to hear the NSManagedObjectContextDidSaveNotification on the background thread. You can register to hear that same notification on the main thread instead. And in that notification you will have the same object IDs that are safe to use on the main thread. The main thread MOC can be safely updated using mergeChangesFromContextDidSaveNotification and the passed notification object, since the method is designed to work this way: mergeChanges docs. Calling your completion block from either thread is now safe as long as you match the moc to the thread the completion block is called on.
So you can do all your main thread updating stuff on the main thread, cleanly separating the threads and avoiding having to pack and repack the updated stuff or doing a double save of the same changes to the persistent store.
To be clear - the Merge that happens is on the managed object contextand its in-memory state - the moc on the main thread is updated to match the one on the background thread, but a new save isn't necessary since you ALREADY saved these changes to the store on the background thread. You have thread safe access to any of those updated objects in the notification object, just as you did when you used it on the background thread.
I hope your solution is working for you and you don't have to re-factor - but wanted to add my thoughts for others who might see this. Please let me know if I've misinterpreted your code and I'll amend.
in your case because your writing to the background moc the notification for mergeChangesFromContextDidSaveNotification will come in on the background moc, not the foreground moc.
so you'll need to register for notifications on the background thread coming to the background moc object.
when you receive that call you can send a message to the main thread moc to mergeChangesFromContextDidSaveNotification.
andrew
update:
here's a sample that should work
//register for this on the background thread
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc addObserver:self selector:#selector(mergeChanges:) name:NSManagedObjectContextDidSaveNotification object:backgroundMOC];
- (void)mergeChanges:(NSNotification *)notification {
NSManagedObjectContext *mainThreadMOC = [singleton managedObjectContext];
//this tells the main thread moc to run on the main thread, and merge in the changes there
[mainThreadMOC performSelectorOnMainThread:#selector(mergeChangesFromContextDidSaveNotification:) withObject:notification waitUntilDone:YES];
}
I am having an issue with Core Data in a background GCD thread... I want to update a record, but after fetching it and setting the values it doesn't seem to actually save the updated record.
isUpdate is a BOOL I have setup that tells me whether I am running a first time parse/save or whether it's a record I need to update. In my case, when I update a record it doesn't actually seem to update in my store.
I'm using MagicalRecord helpers. Here's my code:
// Create background context
NSManagedObjectContext *backgroundContext = [[NSManagedObjectContext alloc] init];
[backgroundContext setPersistentStoreCoordinator:[NSPersistentStoreCoordinator defaultStoreCoordinator]];
// Save the background context and handle the save notification
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(backgroundContextDidSave:)
name:NSManagedObjectContextDidSaveNotification
object:backgroundContext];
// Parsing data...
//..
Record *record;
if (!isUpdate) {
record = [NSEntityDescription insertNewObjectForEntityForName:#"Record" inManagedObjectContext:backgroundContext];
} else {
NSPredicate *recordPredicate = [NSPredicate predicateWithFormat:#"SELF.tag == %#", [[node attributeForName:#"tag"] stringValue]];
record = [Record findFirstWithPredicate:recordPredicate];
}
[record setTitle:[[recordNode attributeForName:#"title"] stringValue]];
// Parsing other data...
//..
NSError *error = nil;
// save the context
[backgroundContext save:&error];
if (error) {
NSLog(#"An error occurred: %#", error);
}
And here's the notification:
- (void)backgroundContextDidSave:(NSNotification *)notification {
// Make sure we're on the main thread when updating the main context
if (![NSThread isMainThread]) {
[self performSelectorOnMainThread:#selector(backgroundContextDidSave:)
withObject:notification
waitUntilDone:NO];
return;
}
// merge in the changes to the main context on the main thread
[[NSManagedObjectContext defaultContext] mergeChangesFromContextDidSaveNotification:notification];
}
Your code sounds quite strange to me.
Why do you register NSManagedObjectContextDidSaveNotification notification in the background thread? Maybe I'm wrong but you need to register that notification in a different point in your app.
If you want to make it works you could register that notification in the main thread. For example you could do it in the AppDelegate.
For example in didFinishLaunchingWithOptions: method you cand do
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(backgroundContextDidSave:)
name:NSManagedObjectContextDidSaveNotification
object:backgroundContext];
Then, always within the AppDelegate, you can merge the changes with the method you wrote:
- (void)backgroundContextDidSave:(NSNotification *)notification {
// Make sure we're on the main thread when updating the main context
if (![NSThread isMainThread]) {
[self performSelectorOnMainThread:#selector(backgroundContextDidSave:)
withObject:notification
waitUntilDone:YES];
return;
}
// merge in the changes to the main context on the main thread
[[NSManagedObjectContext defaultContext] mergeChangesFromContextDidSaveNotification:notification];
}
The code performs these steps:
First checks if you are running in the main thread or not.
Since the notification you register could originate from a thread
different from the main one, you need to perform the selector on the
main thread.
Finally performs the merge with the notification that contains the
changes you made in background.
Once done, you can see that the main context is updated with the changes made in the other one.
Edit
Maybe you can try also to change the waitUntilDone to YES.
Hope it helps.
You are mixing two contexts. This code is probably bad:
record = [Record findFirstWithPredicate:recordPredicate];
I assume that this finds record in different context instead of Your backgroundContext. You should change it to something like this:
record = [Record findFirstWithPredicate:recordPredicate inManagedObjectContext:backgroundContext];