The dance of NSFetchedResultsController, NSManagedObjectContextDidSaveNotification, and MagicalRecord - ios

The problem: I'm getting NSManagedObjectContextDidSaveNotification notifications. I'm merging the changes into my NSFetchedResultsController's context. But the NSFetchedResultsController doesn't fire the didChangeSection, didChangeObject, controllerDidChangeContent methods.
self.managedObjectContext = [NSManagedObjectContext MR_context]; //set up my own context to avoid deadlocking
self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:nil cacheName:#"Master"];
self.fetchedResultsController.delegate = self;
[self.managedObjectContext setStalenessInterval:0];
//listen for changes in the main context:
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(managedObjectContextDidSave:)
name:NSManagedObjectContextDidSaveNotification
object:[NSManagedObjectContext MR_defaultContext]];
- (void)managedObjectContextDidSave:(NSNotification *)notification {
NSLog(#"[%# %#] REFRESH!", THIS_FILE, THIS_METHOD); //this happens
void (^mergeChanges) (void) = ^{
for(NSManagedObject *object in [[notification userInfo] objectForKey:NSUpdatedObjectsKey]) {
//this happens (a lot)
[[self.managedObjectContext objectWithID:[object objectID]] willAccessValueForKey:nil];
}
[self.managedObjectContext mergeChangesFromContextDidSaveNotification:notification];
};
if ([NSThread isMainThread]) {
mergeChanges();
}
else {
dispatch_sync(dispatch_get_main_queue(), mergeChanges);
}
}
So again, I get the notifications just fine. But my fetched results controller doesn't update. This is killing me! Please help!
EDIT: Also tried this:
self.fetchedResultsController = [NSManagedObject MR_fetchController:fetchRequest delegate:self useFileCache:NO groupedBy:nil inContext:[NSManagedObjectContext MR_defaultContext]];
Which causes the same deadlocking. If I do this:
self.fetchedResultsController = [NSManagedObject MR_fetchController:fetchRequest delegate:self useFileCache:NO groupedBy:nil inContext:[NSManagedObjectContext MR_context]]; //note that I just changed the context
The fetchedResultsController does not update its results.

I don't see how or on which context you are saving. If you are not saving all the way down to the persistent store, then the defaultcontext is not getting those changes and this your nsfetchedresultscontroller won't update either. You may need to set up a connection between your local context and which ever context the nsfrc is using in order for changes to propagate.

Related

save NSManagedContext in some cases only

I need to implement cancel/save behavior for the Core Data objects. I have a UITableView for which I got data from NSFetchedResultsController.
- (void) configureWithCategoryItem: (CategoryItem *) categoryItem
{
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:[CategoryItem entityName]];
request.predicate = [NSPredicate predicateWithFormat:#"categoryId = %#", categoryItem.categoryId];
request.sortDescriptors = #[[NSSortDescriptor sortDescriptorWithKey:#"title" ascending:YES]];
self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:request
managedObjectContext:[CategoryItem defaultContext]
sectionNameKeyPath:#"title"
cacheName:nil];
[self.fetchedResultsController performFetch: NULL];
self.layers = [self categoryLayers];
self.sectionTitle = categoryItem.title;
}
Every click on the cell in this table view
LayerItem *layerItem = [self.layers objectAtIndex:indexPath.row];
layerItem.isSelected = [NSNumber numberWithBool:YES];
And in the navigation bar I have two buttons:
Cancel - I need to dismiss controller without saving the results to the core data,
Save - I need to save results and dismiss controller
The save method now looks like this:
- (void) saveChanges
{
NSManagedObjectContext *localContext = self.fetchedResultsController.managedObjectContext;
if ([localContext hasChanges]) {
[localContext save: NULL];
[[NSNotificationCenter defaultCenter] postNotificationName:kEADatabaseWasUpdated object:nil];
}
}
Is there some instrument in Core Data which can help me to implement this behavior?
I tried to use separate NSManagedObjectContext and mergeChangesFromContextDidSaveNotification but didn't receive good result.. Waiting for your help.
- (void) cancelChanges
{
NSManagedObjectContext *localContext = self.fetchedResultsController.managedObjectContext;
[localContext rollback];
}

Update TableView after adding CoreData

I'm working on a CoreData App which displays the content of the CoreData in a TableView.
The Problem is, after adding a string the tableView is not Updating the Content of the CoreData.
i tried it with:
- (void)viewWillAppear:(BOOL)none
{
[self.tableView reloadData];
}
But no Chance, its not reloading.
Any guesses or should I post some more Code? thank you :)
So more Code :)
Saving string to CoreData:
- (void) prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
AppDelegate *appDelegate =
[[UIApplication sharedApplication] delegate];
NSManagedObjectContext *context =
[appDelegate managedObjectContext];
NSManagedObject *newProduct;
newProduct = [NSEntityDescription
insertNewObjectForEntityForName:#"Products"
inManagedObjectContext:context];
[newProduct setValue: _textField.text forKey:#"productName"];
NSError *error;
[context save:&error];
NSLog(#"Product saved");
}
And the Code of the TableViewController:
#implementation CartViewController
- (id)initWithStyle:(UITableViewStyle)style
{
self = [super initWithStyle:style];
if (self) {
// Custom initialization
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
//Core Data Versuche
/*
Fetch existing events.
Create a fetch request for the Event entity; add a sort descriptor; then execute the fetch.
*/
NSFetchRequest *request = [[NSFetchRequest alloc] initWithEntityName:#"Products"];
[request setFetchBatchSize:20];
// Order the events by creation date, most recent first.
// NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"creationDate" ascending:NO];
// NSArray *sortDescriptors = #[sortDescriptor];
// [request setSortDescriptors:sortDescriptors];
// Execute the fetch.
NSError *error;
AppDelegate *delegate = (AppDelegate*)[[UIApplication sharedApplication] delegate];
NSArray *fetchResult = [delegate.managedObjectContext executeFetchRequest:request error:&error];
if (fetchResult== nil) {
// 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]);
}
// Set self's events array to a mutable copy of the fetch results.
[self setCartProductsArray:[fetchResult mutableCopy]];
/*
Reload the table view if the locale changes -- look at APLEventTableViewCell.m to see how the table view cells are redisplayed.
*/
__weak UITableViewController *cell = self;
[[NSNotificationCenter defaultCenter] addObserverForName:NSCurrentLocaleDidChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
[cell.tableView reloadData];
}];
// Ende Core Data Versuche
}
- (void)viewWillAppear:(BOOL)none
{
[self.tableView reloadData];
}
What you probably want to do is use an NSFetchedResultsController.
Make sure that when you are updating the data, you are really just updating CoreData.
When things change in CoreData and you have an NSFetchedResultsController you can subscribe to updates. Apple has great documentation on how to make this work. These updates will update your table correctly using the delegation methods. This ends up mostly being a copy and paste from the documentation.

iOS 5 Core Data duplicate row with multiple NSManagedObjectContext

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.

How can I resolve issue with data not populating UITableView until I redeploy app after updating?

I've just setup a UITableview with Core Data and Grand Central Dispatch to update my app and display information through my fetchedResultsController. I have the application updating my database; however, the UITableView only gets populated once I redeploy the application to my phone through Xcode. For instance I run the update and everything works fine except I have an empty UITableView. Then I can close the app and click "Run" again through Xcode and when the app comes up the information is in the UITableView. I'm including the code below in hopes someone can help me discover why this is the case. If I need to include more code please just let me know. Thanks!
TeamTableViewController.m
- (NSFetchedResultsController \*)fetchedResultsController {
...
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchREquest:fetchRequest managedObjectContext:self.managedObjectContext sectionNameKeyPath:#"stateCode" cacheName:nil];
self.fetchedResultsController = aFetchedResultsController;
...
}
-(IBAction) refreshList:(id)sender {
dispatch_queue_t queue = dispatch_queue_create("updateQueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue,^ { [self updateFromXMLFile:#"https://path/to/file.xml"];});
dispatch_async(queue,^ { [self updateFromXMLFile:#"https://path/to/file2.xml"];});
dispatch_async(queue,^ { [self updateFromXMLFile:#"https://path/to/file3.xml"];});
}
- (BOOL)updateFromXMLFile:(NSString *)pathToFile {
AppDelegate *appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
XMLParser *parser1 = [[XMLParser alloc] initXMLParser];
XMLParser *parser2 = [[XMLParser alloc] initXMLParser];
XMLParser *parser3 = [[XMLParser alloc] initXMLParser];
BOOL success = FALSE;
NSURL *url = [[NSURL alloc] initWithString:pathToFile];
NSXMLParser *xmlParser = [[NSXMLParser alloc] initWithContentsOfURL:url];
if ([pathToFile rangeOfString:#"people"].location != NSNotFound) {
NSManagedObjectContext * peopleMOC = [[NSManagedObjectContext alloc] init];
[peopleMOC setPersistentStoreCoordinator:[appDelegate persistentStoreCoordinator]];
NSNotificationCenter * notify = [NSNotificationCenter defaultCenter];
[notify addObserver:self selector:#selector(mergeChanges:) name:NSManagedObjectContextDidSaveNotification object: peopleMOC];
parser1.managedObjectContext = peopleMOC;
[xmlParser setDelegate: parser1];
success = [xmlParser parse];
if (success) {
NSError * error = nil;
#try {
[parser1.managedObjectContext save:&error];
} catch (NSException * exception) {
// NSLog logs the exception...
}
[NSNotificationCenter defaultCenter] removeObserver:self];
Return TRUE;
} else {
// NSLog logs errors
return FALSE;
}
} elseif ... { // other 3 use practically same code here }
[appDelegate saveContext];
}
-(void) mergeChanges:(NSNotification *) notification {
AppDelegate *theDelegate = [[UIApplication sharedApplication] delegate];
[[theDelegate managedObjectContext] performSelectorOnMainThread:#selector(mergeChangesFromContextDidSaveNotification:) withObject: notification waitUntilDone:YES];
}
UPDATE
I'm able to kind of get it to work by adding [theDelegate saveContext]; to the end of my -(void)mergeChanges method... This just doesn't seem like the proper way of doing it to me. Thoughts?
UPDATE 2
The above method worked one time but I've been unable to get it to replicate.
You should use the NSFetchedResultsControllerDelegate protocol to inform your view controller of any changes in the data.
Use controllerDidChangeContent: rather than the other methods. Like this, the results will be reflected once all the downloads have finished. Shorter incremental updates might get computationally expensive.
There is a very succinct "Typical Use" example in the delegate documentation.

NSFetchedResultsController with pull-to-refresh shows nothing after update

The problem:
When I do a total Core Data refresh via a pull-to-refresh control (deleting everything and then adding everything again), my automatic NSFetchedResultsController (CoreDataTableViewController) just deletes all rows and doesn't show the new data.
My setup:
I use a Core Data datastore which I fetch data from via the use of a subclass of CoreDataTableVewController. In this case, I have a list of groups the logged-in user is added to, which is fetched by using the predicate:
[NSPredicate predicateWithFormat:#"users CONTAINS %#", self.currentUser];
I then use a datastore refresh function (the same as on app startup) to refresh the data, via a pull-to-refresh control (SVPullToRefresh).
[self.tableView addPullToRefreshWithActionHandler:^{
DatabaseMerger *merger = [DatabaseMerger sharedInstance];
[merger refreshDatabase:self.context];
self.tableView.pullToRefreshView.lastUpdatedDate = [NSDate date];
}];
In this refresh, all the Core Data Managed Objects get deleted, and then added again with the use of JSON data.
With the use of the CoreDataTableViewController, which observes the context, the rows should be refreshed if the context changes. Currently, the only thing that changes, is that all the rows disappear.
The weird thing is, that when I pop the viewcontroller, and push it again (via the navigationbar's back-button), the rows are correctly displayed with the data. This seems to me that it is a problem with fetching the data realtime.
What I have tried:
Manually refetching the data via the use of [self.fetchedresultscontroller performFetch:&error]
Deleting the fetchedresultscontroller's cache via the use of [NSFetchedResultsController deleteCacheWithName:nil]
Setting the fetchresultscontroller to nil, and then setting the controller up again
Relevant Code:
- (void)setupFetchedResultsController
{
NSFetchRequest *groupRequest = [NSFetchRequest fetchRequestWithEntityName:#"Group"];
groupRequest.sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:#"name" ascending:YES]];
groupRequest.predicate = [NSPredicate predicateWithFormat:#"users CONTAINS %#", self.currentUser];
self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:groupRequest managedObjectContext:self.context sectionNameKeyPath:nil cacheName:nil];
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self setupFetchedResultsController];
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.debug = YES;
self.currentUser = [Settings currentUser:self.context];
[self.tableView addPullToRefreshWithActionHandler:^{
DatabaseMerger *merger = [DatabaseMerger sharedInstance];
[merger refreshDatabase:self.context];
self.tableView.pullToRefreshView.lastUpdatedDate = [NSDate date];
}];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(receiveFinishedNotification:)
name:#"dataMergeFinished"
object:nil];
// Do any additional setup after loading the view.
}
- (void)receiveFinishedNotification:(NSNotification *) notification
{
if ([[notification name] isEqualToString:#"dataMergeFinished"]) {
[self.tableView.pullToRefreshView stopAnimating];
[NSFetchedResultsController deleteCacheWithName:nil];
NSError *error = nil;
[self.fetchedResultsController performFetch:&error];
if (error) {
DebugLog(#"Error: %#", error);
}
}
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
self.fetchedResultsController = nil;
}
I am using iOS 5.1.1 with ARC
Have you implemented the NSFetchedResultsController delegates? If you haven't the FRC wont auto-update.
implement the following:
- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller
{
NSLog(#"POSTDETAIL: controllerWillChangeContent");
}
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
NSLog(#"POSTDETAIL: controllerDidChangeContent");
[self.tableView reloadData];
}
That should refresh your table view

Resources