I've subclassed NSManagedObject to add some properties that deal with ordering the object's relationship sets, this ordering is then stored in a property. I use KVO to determine when the original set changes and nil the stored property so that it orders using the updated set's data.
However the issue I'm having is that the changes to these sets are performed in a background context and then merged back into the main context which updates the NSManagedObject subclass but without firing the KVO observers that I've set up. Which in turns means that I don't nil the stored property.
How can I tell when an NSManagedObject subclass has been updated via a merge?
Below is my KVO structure inside the subclass:
#pragma mark - Awake
- (void)awakeFromFetch
{
[super awakeFromFetch];
/*-------------------------------*/
[self addObserver:self
forKeyPath:#"comments"
options:NSKeyValueObservingOptionNew
context:NULL];
}
#pragma mark - KVO
- (void) observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary *)change
context:(void *)context
{
if ([object isEqual:self])
{
if ([keyPath isEqualToString:#"comments"])
{
_orderedComments = nil;
}
}
}
#pragma mark - OrderedComments
- (NSArray *)orderedComments
{
if (!_orderedComments)
{
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"dateCreated"
ascending:NO];
NSArray *sortDescriptors = #[sortDescriptor];
_orderedComments = [self.comments sortedArrayUsingDescriptors:sortDescriptors];
}
return _orderedComments;
}
Additional information:
I'm using the parent-child pattern for controlling my NSManagedObjectContexts so when I create the main context:
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
and in each NSOperation context I create the child/background context:
_localManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSConfinementConcurrencyType];
[_localManagedObjectContext setParentContext:[CDSServiceManager managedObjectContext]];
[_localManagedObjectContext setUndoManager:nil];
[_localManagedObjectContext setMergePolicy:NSMergeByPropertyObjectTrumpMergePolicy];
(CDSServiceManager is a helper class to allow access to the main context)
I use the following to control merging:
- (void) saveLocalContextChangesToMainContextWithSuccess:(void (^)(void))success
failure:(void (^)(NSError *error))failure
{
NSError *localManagedObjectContentSaveError = nil;
if (![self.localManagedObjectContext save:&localManagedObjectContentSaveError])
{
DLog(#"Error to saving local context: %#", [localManagedObjectContentSaveError userInfo]);
if (failure)
{
failure(localManagedObjectContentSaveError);
}
}
else
{
[[CDSServiceManager managedObjectContext] performBlock:^
{
NSError *mergeSaveError = nil;
if (![[CDSServiceManager managedObjectContext] save:&mergeSaveError])
{
DLog(#"Error saving main context: %#", [mergeSaveError userInfo]);
if (failure)
{
failure(mergeSaveError);
}
}
else
{
if (success)
{
success();
}
}
}];
}
}
This is called from the NSOperation subclass.
Related
I'm trying to persist new entities:
- (void)updateStorageWithQuizzess:(NSArray *)quizzess completion:(void(^)(NSArray *quizzess, BOOL succes, NSError *error))completion {
NSMutableArray *mutableArray = [NSMutableArray array];
[Quiz MR_truncateAll];
[[NSManagedObjectContext MR_context] MR_saveWithBlock:^(NSManagedObjectContext *localContext) {
for (NSDictionary *dictionary in quizzess) {
Quiz *quiz = [Quiz MR_createEntity];
[quiz fromDictionary:dictionary];
[mutableArray addObject:quiz];
}
} completion:^(BOOL contextDidSave, NSError *error) {
BlockSafeRun(completion, mutableArray, contextDidSave, error);
}];
}
Or like this:
- (void)updateStorageWithQuizzess:(NSArray *)quizzess completion:(void(^)(NSArray *quizzess, BOOL succes, NSError *error))completion {
NSMutableArray *mutableArray = [NSMutableArray array];
[Quiz MR_truncateAll];
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext) {
for (NSDictionary *dictionary in quizzess) {
Quiz *quiz = [Quiz MR_createEntity];
[quiz fromDictionary:dictionary];
[mutableArray addObject:quiz];
}
} completion:^(BOOL contextDidSave, NSError *error) {
BlockSafeRun(completion, mutableArray, contextDidSave, error);
}];
}
But in completion block I receive contextDidSave == NO, error == nil.
So I cannot figure out what went wrong.
Are there some obvious mistakes I made? How can I debug that issue?
//////
2015-06-17 20:39:27.061 HITO[6733:618953] Set root saving context: <NSManagedObjectContext: 0x16dbe070>
2015-06-17 20:39:27.062 HITO[6733:618953] Created new main queue context: <NSManagedObjectContext: 0x16e855b0>
2015-06-17 20:39:27.063 HITO[6733:618953] Set default context: <NSManagedObjectContext: 0x16e855b0>
2015-06-17 20:39:27.316 HITO[6733:618953] [HockeySDK] WARNING: Detecting crashes is NOT enabled due to running the app with a debugger attached.
2015-06-17 20:39:28.829 HITO[6733:618953] Created new private queue context: <NSManagedObjectContext: 0x16d57870>
2015-06-17 20:39:28.831 HITO[6733:619027] Created new private queue context: <NSManagedObjectContext: 0x16ea4ec0>
2015-06-17 20:39:28.841 HITO[6733:619027] NO CHANGES IN ** saveWithBlock:completion: ** CONTEXT - NOT SAVING
update
Following code from MR:
- (void) MR_saveWithOptions:(MRSaveOptions)saveOptions completion:(MRSaveCompletionHandler)completion;
{
__block BOOL hasChanges = NO;
if ([self concurrencyType] == NSConfinementConcurrencyType)
{
hasChanges = [self hasChanges];
}
else
{
[self performBlockAndWait:^{
hasChanges = [self hasChanges];
}];
}
if (!hasChanges)
{
MRLogVerbose(#"NO CHANGES IN ** %# ** CONTEXT - NOT SAVING", [self MR_workingName]);
if (completion)
{
dispatch_async(dispatch_get_main_queue(), ^{
completion(NO, nil);
});
}
return;
}
So, hasChanges returns NO.
Your objects have no changes occurring in the save block. There are two problems I see here.
You are creating your new objects in the MR_defaultContext when you need to be creating them in the localContext that is the saving context for your save block.
Try this:
- (void)updateStorageWithQuizzess:(NSArray *)quizzess completion:(void(^)(NSArray *quizzess, BOOL succes, NSError *error))completion {
NSMutableArray *mutableArray = [NSMutableArray array];
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext) {
for (NSDictionary *dictionary in quizzess) {
Quiz *quiz = [Quiz MR_createInContext:localContext];
[quiz fromDictionary:dictionary];
[mutableArray addObject:quiz];
}
} completion:^(BOOL contextDidSave, NSError *error) {
BlockSafeRun(completion, mutableArray, contextDidSave, error);
}];
}
Your objects are not actually being deleted before you start importing the new objects. This probably isn't causing your context to have no changes but I'll discuss this as well.
[Quiz MR_truncateAll] just sets all of your Quiz objects deletedproperty to true. This means the next time that the context is saved or processed, the changes will be saved.
So, when you create your new saving context that context still has those objects. I'm not sure what your fromDictionary method is doing, but if its relying on an database, then it's not going to have it.
You need to do it like this:
- (void)updateStorageWithQuizzess:(NSArray *)quizzess completion:(void(^)(NSArray *quizzess, BOOL succes, NSError *error))completion {
NSMutableArray *mutableArray = [NSMutableArray array];
[MagicalRecord saveWithBlock:^(NSManagedObjectContext *localContext) {
[Quiz MR_truncateAllInContext:localContext];
[localContext processPendingChanges];
for (NSDictionary *dictionary in quizzess) {
Quiz *quiz = [Quiz MR_createInContext:localContext];
[quiz fromDictionary:dictionary];
[mutableArray addObject:quiz];
}
} completion:^(BOOL contextDidSave, NSError *error) {
BlockSafeRun(completion, mutableArray, contextDidSave, error);
}];
}
This way, you are deleting the objects in the saving context. You have to also remember to call processPendingChanges on the saving context to delete the objects from the context, rather than just marking them to be deleted.
Short version of my problem: I'm deleting an object and after I do a fetch that returns the previously deleted object.
I'm following this architecture:
SyncService -> Persistence Service -> NSManagedObject
All my classes in Persistence Service layer are children of the following class:
# PersistenceService.h
#import <Foundation/Foundation.h>
#interface PersistenceService : NSObject
#property (nonatomic, retain) NSManagedObjectContext *context;
-(instancetype) init;
-(void) saveContext;
#end
# PersistenceService.m
#implementation PersistenceService
-(instancetype) init {
self = [super init];
if (self) {
self.context = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
self.context.parentContext = [DataManager sharedInstance].managedObjectContext;
}
return self;
}
-(void) saveContext {
NSManagedObjectContext *context = self.context.parentContext;
[self.context performBlock:^{
NSError *error;
[self.context save:&error];
[context performBlock:^{
NSError *error;
[context save:&error];
[context.parentContext performBlock:^{
NSError *error;
[context.parentContext save:&error];
}];
}];
}];
}
#end
Before the deletion, I fetch the object in my main context:
# Synchronizer.m
-(void) synchronize {
NSArray *pseudoLeads = [[[PseudoLeadPersistenceService alloc] init] getAllParentPseudoLeads];
if (pseudoLeads) {
PseudoLeadDAO *pseudoLead = [pseudoLeads objectAtIndex:0];
if ([pseudoLead.type isEqualToNumber:[NSNumber numberWithInt:Capture]]) {
CaptureSyncService *service = [[CaptureSyncService alloc] initWithDelegate:self andPseudoLead:pseudoLead];
[service executeRequest];
} else {
HotleadSyncService *service = [[HotleadSyncService alloc] initWithDelegate:self andPseudoLead:pseudoLead];
[service executeRequest];
}
}
}
.
# PseudoLeadPersistenceService.m
-(NSArray *) getAllParentPseudoLeads {
return [PseudoLeadDAO findAllParentPseudoLeadsInContext:self.context.parentContext];
}
And here I fetch and actually delete the object in my subcontext:
# PseudoLeadPersistenceService.m
-(void) deletePseudoLeadById:(NSNumber *)pseudoLeadId andEventId:(NSNumber *)eventId {
PseudoLeadDAO *pseudoLeadDAO = [PseudoLeadDAO findPseudoLeadById:pseudoLeadId andEventId:eventId inContext:self.context];
[self.context deleteObject:pseudoLeadDAO];
[self saveContext];
}
Then the -(void) synchronize is called again and the deleted object shows up again as a fault. At this point, I can fetch as many times as I want and it will be returned. It only goes away when it comes to -(void) deletePseudoLeadById:(NSNumber *)pseudoLeadId andEventId:(NSNumber *)eventId method again and the fault is fired.
I'll appreciate any help given. Thanks!
The problem was concurrency. The thread started to save the context wasn't getting done before the next fetch.
I solved the problem by using performBlockAndWait:
# PersistenceService.m
-(void) saveContextAndWait {
NSManagedObjectContext *context = self.context.parentContext;
[self.context performBlockAndWait:^{
NSError *error;
[self.context save:&error];
[context performBlockAndWait:^{
NSError *error;
[context save:&error];
[context.parentContext performBlockAndWait:^{
NSError *error;
[context.parentContext save:&error];
}];
}];
}];
}
.
# PseudoLeadPersistenceService.m
-(void) deletePseudoLeadById:(NSNumber *)pseudoLeadId andEventId:(NSNumber *)eventId {
PseudoLeadDAO *pseudoLeadDAO = [PseudoLeadDAO findPseudoLeadById:pseudoLeadId andEventId:eventId inContext:self.context];
[self.context deleteObject:pseudoLeadDAO];
[self saveContextAndWait];
}
I try to use Core Data to make a UITableView, but I come across a crash when I run it:
014-07-29 10:13:12.443 TableAndCoreData[797:60b] -[AppDelegate managedObjectContext]: unrecognized selector sent to instance 0x8f319a0
2014-07-29 10:13:12.446 TableAndCoreData[797:60b] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[AppDelegate managedObjectContext]: unrecognized selector sent to instance 0x8f319a0'
I generally check it and I guess it might be the problem of the creating the managedObjectContext. Does any one have some idea to help me to fix this problem?
#interface DetailViewController ()
#property (weak, nonatomic) IBOutlet UITextField *nameTextField;
#property (weak, nonatomic) IBOutlet UITextField *ageTextField;
#end
#implementation DetailViewController
// Set NSManagedObjectContext
- (NSManagedObjectContext *) managedOjectContext
{
NSManagedObjectContext *context = nil;
id delegate = [[UIApplication sharedApplication]delegate];
if ([delegate performSelector:#selector(managedObjectContext)]) {
context = [delegate managedOjectContext];
}
return context;
}
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
// Custom initialization
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
// Do any additional setup after loading the view.
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (IBAction)cancelButton:(UIButton *)sender
{
[self dismissViewControllerAnimated:YES completion:nil];
}
- (IBAction)saveButton:(UIButton *)sender
{
[self save];
}
- (void)save
{
// Get ManagedObjectContext
NSManagedObjectContext *context = [self managedOjectContext];
// Create a ManagedObject
NSManagedObject *aPerson = [NSEntityDescription insertNewObjectForEntityForName:#"Person" inManagedObjectContext:context];
// Set value for the attributes of the entity
[aPerson setValue:self.nameTextField.text forKey:#"name"];
[aPerson setValue:self.ageTextField.text forKey:#"age"];
// Check the error
NSError *error = nil;
if (![context save:&error]) {
NSLog(#"Can't save due to %#%#", error, [error localizedDescription]);
}
[self dismissViewControllerAnimated:YES completion:nil];
}
Besides, I am trying another way to create UITableView using Core Data:
- (void) save
{
// Create UIManagedDocument
NSFileManager *fileManager = [NSFileManager defaultManager];
NSURL *documentDirectory = [[fileManager URLsForDirectory:NSDocumentationDirectory inDomains:NSUserDomainMask]firstObject];
NSString *documentName = #"Model";
NSURL *url = [documentDirectory URLByAppendingPathComponent:documentName];
UIManagedDocument *document = [[UIManagedDocument alloc]initWithFileURL:url];
if ([fileManager fileExistsAtPath:[url path]]) {
[document openWithCompletionHandler:^(BOOL success) {
if (success) {
if (document.documentState == UIDocumentStateNormal) {
// Get a ManagedObjectContext
NSManagedObjectContext *context = document.managedObjectContext;
// Set managed object (entity)
NSManagedObject *aPerson = [NSEntityDescription insertNewObjectForEntityForName:#"Person" inManagedObjectContext:context];
// Set value for the attribute (which are "name" and "age") of the entity
[aPerson setValue:self.nameTextField.text forKey:#"name"];
[aPerson setValue:self.ageTextField.text forKey:#"age"];
// Check whether there is an error
NSError *error = nil;
if (![context save:&error]) {
NSLog(#"Can't save due to %#%#", error, [error localizedDescription]);
}
// Close the window
[self dismissViewControllerAnimated:YES completion:nil];
}
}
if (!success) {
NSLog(#"couldn't open document at %#", url);
}
}];
}
else {
[document saveToURL:url forSaveOperation:UIDocumentSaveForCreating completionHandler:^(BOOL success) {
if (success) {
if (document.documentState == UIDocumentStateNormal) {
// Get a ManagedObjectContext
NSManagedObjectContext *context = document.managedObjectContext;
// Set managed object (entity)
NSManagedObject *aPerson = [NSEntityDescription insertNewObjectForEntityForName:#"Person" inManagedObjectContext:context];
// Set value for the attribute (which are "name" and "age") of the entity
[aPerson setValue:self.nameTextField.text forKey:#"name"];
[aPerson setValue:self.ageTextField.text forKey:#"age"];
// Check whether there is an error
NSError *error = nil;
if (![context save:&error]) {
NSLog(#"Can't save due to %#%#", error, [error localizedDescription]);
}
// Close the window
[self dismissViewControllerAnimated:YES completion:nil];
}
}
if (!success) {
NSLog(#"couldn't open document at %#", url);
}
}];
}
}
However, it just couldn't find the UIManagedDocument. I really wonder the reason why I should create a UIManagedDocument and the document name I should name it.
Your crash does not have anything to do with Core Data.
Change this:
[delegate performSelector:#selector(managedObjectContext)]
to:
[delegate respondsToSelector:#selector(managedObjectContext)]
The crash was happening because you were sending the message managedObjectContext to the application delegate object, which does not respond to that message. It was being sent because where you meant to check to see if it responds to that message with respondsToSelector:, you had performSelector instead. Your application delegate object still needs to implement managedObjectContext for your code to be functional, but the portions you have posted should no longer crash as you describe.
In general you want to avoid calling a method on the application delegate this way. It's preferred to pass a value like this from the application delegate into the root view controller at startup, and it's passed along to the next view controller and the next.
There is a slightly outdated, but still relevant section of the Core Data documentation that discusses this:
A view controller typically shouldn’t retrieve the context from a global object such as the application delegate—this makes the application architecture rigid. Neither should a view controller create a context for its own use (unless it’s a nested context).
...as well as the iOS 5 release notes:
Nested contexts make it more important than ever that you adopt the “pass the baton” approach of accessing a context (by passing a context from one view controller to the next) rather than retrieving it directly from the application delegate.
When my app loads it give that notification that its using local store: 1 usually it takes 4 to 5 minutes before it returns using local store :0. Not only that reloading using NSNotifications does not seem to work although going to a new view then back does.
appDelegate:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions{
if(![[NSUserDefaults standardUserDefaults] boolForKey:#"firstTime"]){
UIAlertView *msg = [[UIAlertView alloc] initWithTitle:#"Would You Like to Keep Clients Synced With iCloud" message:nil delegate:self cancelButtonTitle:#"NO" otherButtonTitles:#"YES", nil];
msg.tag = 101;
[msg show];
}
[self registerForiCloudNotifications];
UINavigationController *nc = (UINavigationController *)self.window.rootViewController;
ClientListViewController *iC = (ClientListViewController *)[nc viewControllers][0];
iC.context = [self managedObjectContext];
return YES;
}
- (void)applicationWillResignActive:(UIApplication *)application
{
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
// Use this method to pause ongoing tasks, disable timers, and throttle down OpenGL ES frame rates. Games should use this method to pause the game.
}
- (void)applicationDidEnterBackground:(UIApplication *)application
{
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}
- (void)applicationWillEnterForeground:(UIApplication *)application
{
// Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
}
- (void)applicationDidBecomeActive:(UIApplication *)application
{
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
}
- (void)applicationWillTerminate:(UIApplication *)application
{
// Saves changes in the application's managed object context before the application terminates.
[self saveContext];
}
- (void)saveContext
{
NSError *error = nil;
NSManagedObjectContext *managedObjectContext = self.managedObjectContext;
if (managedObjectContext != nil) {
if ([managedObjectContext hasChanges] && ![managedObjectContext save:&error]) {
// Replace this implementation with code to handle the error appropriately.
// abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
}
}
#pragma mark - Core Data stack
// Returns the managed object context for the application.
// If the context doesn't already exist, it is created and bound to the persistent store coordinator for the application.
- (NSManagedObjectContext *)managedObjectContext
{
if (_managedObjectContext != nil) {
return _managedObjectContext;
}
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil) {
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[_managedObjectContext setPersistentStoreCoordinator:coordinator];
}
return _managedObjectContext;
}
// Returns the managed object model for the application.
// If the model doesn't already exist, it is created from the application's model.
- (NSManagedObjectModel *)managedObjectModel
{
if (_managedObjectModel != nil) {
return _managedObjectModel;
}
NSURL *modelURL = [[NSBundle mainBundle] URLForResource:#"Benjii" withExtension:#"momd"];
_managedObjectModel = [[NSManagedObjectModel alloc] initWithContentsOfURL:modelURL];
return _managedObjectModel;
}
// Returns the persistent store coordinator for the application.
// If the coordinator doesn't already exist, it is created and the application's store added to it.
- (NSPersistentStoreCoordinator *)persistentStoreCoordinator
{
if (_persistentStoreCoordinator != nil) {
return _persistentStoreCoordinator;
}
NSError *error = nil;
_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:[self storeURL] options:[self storeOptions] error:&error]) {
/*
Replace this implementation with code to handle the error appropriately.
abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
Typical reasons for an error here include:
* The persistent store is not accessible;
* The schema for the persistent store is incompatible with current managed object model.
Check the error message to determine what the actual problem was.
If the persistent store is not accessible, there is typically something wrong with the file path. Often, a file URL is pointing into the application's resources directory instead of a writeable directory.
If you encounter schema incompatibility errors during development, you can reduce their frequency by:
* Simply deleting the existing store:
[[NSFileManager defaultManager] removeItemAtURL:storeURL error:nil]
* Performing automatic lightweight migration by passing the following dictionary as the options parameter:
#{NSMigratePersistentStoresAutomaticallyOption:#YES, NSInferMappingModelAutomaticallyOption:#YES}
Lightweight migration will only work for a limited set of schema changes; consult "Core Data Model Versioning and Data Migration Programming Guide" for details.
*/
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
return _persistentStoreCoordinator;
}
-(NSURL *)storeURL{
return [[self applicationDocumentsDirectory] URLByAppendingPathComponent:#"Benjii.sqlite"];
}
- (void)registerForiCloudNotifications{
NSLog(#"Registering");
NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter];
[notificationCenter addObserver:self
selector:#selector(storesWillChange:)
name:NSPersistentStoreCoordinatorStoresWillChangeNotification
object:self.persistentStoreCoordinator];
[notificationCenter addObserver:self
selector:#selector(storesDidChange:)
name:NSPersistentStoreCoordinatorStoresDidChangeNotification
object:self.persistentStoreCoordinator];
[notificationCenter addObserver:self
selector:#selector(persistentStoreDidImportUbiquitousContentChanges:)
name:NSPersistentStoreDidImportUbiquitousContentChangesNotification
object:self.persistentStoreCoordinator];
}
#pragma mark - iCloud Support
//Use these options in your call to addPersistentStore:
-(NSDictionary *)storeOptions{
NSMutableDictionary *options = [[NSMutableDictionary alloc] init];
[options setObject:[NSNumber numberWithBool:YES] forKey:NSMigratePersistentStoresAutomaticallyOption];
[options setObject:[NSNumber numberWithBool:YES] forKey:NSInferMappingModelAutomaticallyOption];
[options setObject:#"GraniteCloudStore" forKey:NSPersistentStoreUbiquitousContentNameKey];
return options;
}
- (void) persistentStoreDidImportUbiquitousContentChanges:(NSNotification *)changeNotification{
NSManagedObjectContext *context = self.managedObjectContext;
[context performBlock:^{
[context mergeChangesFromContextDidSaveNotification:changeNotification];
}];
[[NSNotificationCenter defaultCenter] postNotificationName:#"Reload" object:nil];
}
- (void) storesWillChange:(NSNotification *)notification {
NSManagedObjectContext *context = self.managedObjectContext;
[context performBlockAndWait:^{
NSError *error;
if([context hasChanges]){
BOOL success = [context save:&error];
if(!success && error){
//perform error handling
NSLog(#"%#", [error localizedDescription]);
}
}
[context reset];
}];
//Refresh User Interface
NSLog(#"Stores will change");
}
-(void)storesDidChange:(NSNotification *)notification{
//Refresh your User Interface
NSLog(#"Stores did change");
[[NSNotificationCenter defaultCenter] postNotificationName:#"Reload" object:nil];
}
-(void)migrateiCloudStoreToLocalStore{
//assuming you only have one store.
NSPersistentStore *store = [[_persistentStoreCoordinator persistentStores] firstObject];
NSMutableDictionary *localStoreOptions = [[self storeOptions] mutableCopy];
[localStoreOptions setObject:#YES forKey:NSPersistentStoreRemoveUbiquitousMetadataOption];
NSPersistentStore *newStore = [_persistentStoreCoordinator migratePersistentStore:store
toURL:[self storeURL]
options:localStoreOptions
withType:NSSQLiteStoreType
error:nil];
[self reloadStore:newStore];
}
-(void)reloadStore:(NSPersistentStore *)store {
if(store){
[_persistentStoreCoordinator removePersistentStore:store error:nil];
}
[_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType
configuration:nil
URL:[self storeURL]
options:[self storeOptions]
error:nil];
}
#pragma mark - Application's Documents directory
// Returns the URL to the application's Documents directory.
- (NSURL *)applicationDocumentsDirectory
{
return [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
}
#pragma mark - Alert View Delegate
-(void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex{
if(alertView.tag == 101){
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:#"firstTime"];
if(buttonIndex != alertView.cancelButtonIndex){
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:#"iCloudSupport"];
}
}
[[NSUserDefaults standardUserDefaults] synchronize];
}
#end
and fetchresultcontroller:
-(NSFetchedResultsController *)clientResultsController{
if(clientResultsController != nil){
return clientResultsController;
}
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:#"Client"];
// NSPredicate *predicate = [NSPredicate predicateWithFormat:#"name beginswith %#"];
NSSortDescriptor *sort = [[NSSortDescriptor alloc] initWithKey:#"finitial" ascending:YES selector:#selector(localizedCompare:)];
//[fetchRequest setPredicate:predicate];
[fetchRequest setSortDescriptors:[NSArray arrayWithObject:sort]];
NSFetchedResultsController *theFetchedResultController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:context
sectionNameKeyPath:#"finitial"
cacheName:#"Root"];
self.clientResultsController = theFetchedResultController;
self.clientResultsController.delegate = self;
return clientResultsController;
}
and observer in view Controller:
-(void)viewWillAppear:(BOOL)animated{
[super viewWillAppear:animated];
[[NSNotificationCenter defaultCenter] addObserverForName:#"Reload"
object:self
queue:[NSOperationQueue mainQueue]
usingBlock:^(NSNotification *note){
NSLog(#"Reloading");
[self reloadFetchedResults:note];
}];
}
Sorry if the answer is really simple i checked the other similar questions and they were either unanswered or Outdated (though I tried a couple of them anyway). Any help will be much appreciated
edit
it seems like my observer isnt catching the observerForName:#"Reload"
does anyone know how to add an observer to the method that sends this message:
PFUbiquitySwitchboardEntryMetadata setUseLocalStorage:: CoreData: Ubiquity: mobile~843477F1-43D0-4E72-BD91-26C3276A9212:GraniteCloudStore
Using local storage: 0
also noticed that if I wait until the above message then rotate the screen also causes table to populate however the persistentStoreCoordinatorDidChange is called before that notice
I'm having a similar issue here, but in my case, when I migrate my app from a non-icloud version to an iCloud version, the existing data does not show. Though if I add a new entry, then everything shows and with a debugging, I realised why everything is showing.
In my UITableView, I did the following:
- (void)reloadFetchedResults:(NSNotification*)note {
NSError *error = nil;
if (![[self fetchedResultsController] performFetch:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
if (note)
{
[self.timelineTableView reloadData];
}
}
In my viewDidLoad:
[[NSNotificationCenter defaultCenter] addObserver:self selector:#selector(reloadFetchedResults:) name:#"RefetchAllDatabaseData" object:[[UIApplication sharedApplication] delegate]];
// In your case, the name would be Reload
- (void)viewDidUnload
{
[super viewDidUnload];
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
Try this - I'm a complete noob here but this worked for me to be able to see changes straight away.
Our table view controllers use an NSFetchedResultsController to show data from Core Data. We download new data in the background. When an entity is modified in the new data, on iOS 5.1.1 phone, we see that treated as a new row in the table instead of an update. Cannot duplicate on the iOS 5.1 simulator or an iOS 6 device.
The UIApplicationDelegate creates a NSManagedObjectContext with concurrency type NSMainQueueConcurrencyType. Our UITableViewController implements NSFetchedResultsControllerDelegate. In viewWillAppear we go fetch new data. In the method getting data, we create a second NSManagedObjectContext with concurrenty Type NSPrivateQueueConcurrencyType. We do a performBlock on that new context, and do the network call and json parsing. There's a NSFetchRequest to get the previous data, so we can delete the old objects, or modify any existing entities with the same id. After modify the existing entity or creating new ones, we then deleteObject the old entity objects. Then we save this private context. Then on the parent context, do a performBlock to save the changes there.
On iOS5.1, the table is incorrect. If we change on of the objects, instead of being modified, it is added to the table as a new row. If we leave this controller and come back to it, getting new data, it shows the right amount.
AppDelegate.m
- (void)saveContext
{
[self.privateWriterContext performBlock:^{
NSError *error = nil;
[self.privateWriterContext save:&error];
// Handle error...
[[NSNotificationCenter defaultCenter] removeObserver:self name:NSManagedObjectContextDidSaveNotification object:self.privateWriterContext];
}];
}
#pragma mark - Core Data stack
- (NSManagedObjectContext *)privateWriterContext
{
if (__privateWriterContext != nil) {
return __privateWriterContext;
}
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil) {
__privateWriterContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
[__privateWriterContext setPersistentStoreCoordinator:coordinator];
}
return __privateWriterContext;
}
- (NSManagedObjectContext *)managedObjectContext
{
if (__managedObjectContext != nil) {
return __managedObjectContext;
}
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil) {
__managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[__managedObjectContext setParentContext:self.privateWriterContext];
}
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(saveContext:)
name:NSManagedObjectContextDidSaveNotification
object:__managedObjectContext];
return __managedObjectContext;
}
class that fetches from server
+ (void) fetchFromURL:(NSString *) notificationsUrl withManagedObjectContext (NSManagedObjectContext *)managedObjectContext
{
NSManagedObjectContext *importContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];
importContext.parentContext = managedObjectContext;
[importContext performBlock: ^{
NSError *error;
NSURLResponse *response;
[UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
NSData *responseData = [NSData dataWithContentsOfURLUsingCurrentUser:[NSURL URLWithString:notificationsUrl] returningResponse:&response error:&error];
[UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
NSMutableSet *newKeys = [[NSMutableSet alloc] init];
NSArray *notifications;
if(responseData) {
NSDictionary* json = [NSJSONSerialization
JSONObjectWithData:responseData
options:kNilOptions
error:&error];
NSMutableDictionary *previousNotifications = [[NSMutableDictionary alloc] init];
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:#"Notification"];
NSArray * oldObjects = [importContext executeFetchRequest:request error:&error];
for (Notification* oldObject in oldObjects) {
[previousNotifications setObject:oldObject forKey:oldObject.notificationId];
}
notifications = [json objectForKey:#"notifications"];
//create/update objects
for(NSDictionary *notificationDictionary in notifications) {
NSString *notificationId = [notificationDictionary objectForKey:#"id"];
Notification *notification = [previousNotifications objectForKey:notificationId];
if(notification) {
[previousNotifications removeObjectForKey:notificationId];
} else {
notification = [NSEntityDescription insertNewObjectForEntityForName:#"Notification" inManagedObjectContext:importContext];
[newKeys addObject:notificationId];
}
notification.notificationId = [notificationDictionary objectForKey:#"id"];
//other properties from the json response
}
for (NSManagedObject * oldObject in [previousNotifications allValues]) {
[importContext deleteObject:oldObject];
}
}
if (![importContext save:&error]) {
NSLog(#"Could not save to main context after update to notifications: %#", [error userInfo]);
}
//persist to store and update fetched result controllers
[importContext.parentContext performBlock:^{
NSError *parentError = nil;
if(![importContext.parentContext save:&parentError]) {
NSLog(#"Could not save to store after update to notifications: %#", [error userInfo]);
}
}];
}
];
}
I suffered the problem recently too.
The problem is because of two contexts in different threads.
On device which is running iOS 5.1, merging them cause it to insert a new record instead of update it. I change the background thread to use main context instead and the problem is gone.
No clue why merging does not work in this particular case.