I want my app to check the core data store at start-up. If the store is empty, it will add two items into it. What is the best way to implement this?
Can I put the following code in viewDidLoad?
- (void)viewDidLoad
{
[super viewDidLoad];
NSManagedObjectContext *managedObjectContext = [self managedObjectContext];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] initWithEntityName:#"MonitorItem"];
self.monitorItemArray = [[managedObjectContext
executeFetchRequest:fetchRequest
error:nil] mutableCopy];
// If the core data is empty, populate it with the two compulsory items
if ([self.monitorItemArray count] == 0)
{
self.AddMandatoryItems;
}
[self.tableView reloadData];
}
I have searched other articles but none seems to give me an answer that I could understand.
Getting information from managedObjectContext in viewDidLoad is good.
If in cellForRowAtIndexPath you populate the cells from self.monitorItemArray than there is no reason to call reloadData (Which essentially erase the entire table view and re-draw it from scratch - which is exactly what happens when the view is appearing on screen any way...).
If you also show information from a web service, you can call reloadData in the response method to replace the existing data with the one that came from the web. Otherwise - if only information from core data is shown on the table view - no need for reloadData (Or maybe only in a case where the information in your managedObjectContext has changed while the table view is on screen).
Related
I am trying to delete an image and a string from core data. I have a UITableView and when you click on a cell, it takes you to a deletedViewController there is a button to delete the items in the cell from core data as well as delete the cell from the table view.
Here is the code I am using:
//Delete Photo
NSManagedObject *objectToBeDeleted = [self managedObject]; // Replace this with whatever you use to reference the managed object
NSManagedObjectContext *context = [objectToBeDeleted managedObjectContext];
[context deleteObject:objectToBeDeleted];
[self.navigationController popViewControllerAnimated:YES];
Are you using fetchresultcontroller? The deletion will be performed finally when you call
[context save:&error];
And the fetchresultcontroller delegate will be called after this save to update the table view cells.
Code looks fine..
I will suggest if you use breakpoint to check the managedObject you are sending to deleteViewController is same as one you are deleting.
I have a UITableViewController that's a subclass of CoreDataTableViewController (it's the Stanford class). That implements a fetchedResultsController.
Now, in my viewWillAppear, I have this:
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
if(!self.managedObjectContext) {
[self useManagedDocument];
}
}
It initializes the managedObjectContext if I don't have one, and gets it from a helper class. In the MOC setter, I initialize the fetchedResultsController:
- (void)setManagedObjectContext:(NSManagedObjectContext *)managedObjectContext
{
_managedObjectContext = managedObjectContext;
if(self.managedObjectContext) {
NSFetchRequest *request = [NSFetchRequest fetchRequestWithEntityName:CD_ITEM_ENTITY_NAME];
request.sortDescriptors = #[[NSSortDescriptor
sortDescriptorWithKey:CD_ITEM_NAME_PROPERTY
ascending:YES
selector:#selector(localizedCaseInsensitiveCompare:)]];
request.predicate = nil;
self.fetchedResultsController = [[NSFetchedResultsController alloc]
initWithFetchRequest:request
managedObjectContext:self.managedObjectContext
sectionNameKeyPath:nil
cacheName:nil];
} else {
self.fetchedResultsController = nil;
}
}
When my program starts, it loads the table data up correctly and my debugger says there was a fetch request made. However, after inserting data into my Core Data graph, and saving, it says the context changes and fires this delegate method:
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
/*NSError *error;
[controller performFetch:&error];
if(error) {
NSLog(#"%#", error);
}
[self.tableView reloadData];*/
}
I commented this because it wasn't working. Basically, what I want to do is reload the data every time the context changes. If I add an item in another view controller and then go back to this one, it should reload in that case too.
How do I implement this? I tried doing performFetch in that delegate method and it entered it (I checked by setting a breakpoint inside), but the performFetch did nothing and my table wasn't reloaded.
When I add an item in a modal VC (another one I have for managing items) this is what happens in my logger:
2013-05-10 22:41:38.264 App1[7742:c07] [ItemCDTVC performFetch] fetching all Item (i.e., no predicate)
2013-05-10 22:41:46.454 App1[7742:c07] NSManagedObjects did change.
2013-05-10 22:41:46.456 App1[7742:c07] NSManagedContext did save.
When I close my app but do not quit it from the multitasking bar, and then reopen it, it does nothing. No fetch. Well, if the context didn't change I don't want it to fire a request, but imagine if the user adds an item in another ViewController and then goes back to my ItemCDTVC, which lists all items. Does it get a context changed notification so it can call the delegate method to update the table, or will I always have to refresh regardless of changes in my viewWillAppear? I currently have it set to do it only once, on app load.
Fixed, all I had to do is put a one liner in that delegate method:
- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
[self.tableView endUpdates];
}
This ends updates, inserts, deletes, and changes made to the table (refreshing my view, essentially) the same as per Apple's documentation.
It now updates on content change.
I have more of a conceptual question than code based because my code works.
When my app launches I fetch the sessionObject from coreData and validate the authToken.
This code works when in my loading controller. The Fetch request works and returns an array of sessionObjects. However in App Delegate where I validate the authToken the returned array is empty. Why does the code work in a controller but not in the App Delegate? There are no errors from the fetch request. The context is not nil. It's the exact code I use in the loading controller and that works.
Do I have to do requests differently for CoreData in the App Delegate? Can I use a fetch request in App Delegate?
Sample Code in the app Delegate DidBecomeActive method. I use DidBecomeActive so we can validate on return from background and init.
// check for valid authtoken if present so the correct home screen will display
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"CurrentSession" inManagedObjectContext:[self managedObjectContext]];
[request setEntity:entity];
CurrentSession *sessionObj = nil;
NSError *cdError = nil;
if([self managedObjectContext] == nil){
NSLog(#"context is nil");
}
NSArray *sessionArray = [[[self managedObjectContext] executeFetchRequest:request error:&cdError] mutableCopy];
if (sessionArray != nil && [sessionArray count]) {
sessionObj = (CurrentSession *)[sessionArray lastObject];
NSLog(#"Found session %#",sessionObj.authToken);
if (![sessionObj.authToken isEqualToString:#""]) {
[Solid_Utilities validateAuthToken:[self managedObjectContext]];
}
} else {
NSLog(#"NO SESSION FOUND");
}
EDIT
I'm sure my issue may be thread related.
In the loading controller I run a lot of tasks on separate threads and I assume App Delegate runs on main thread.
However the context I provide to the loading controller is generated in app delegate.
EDIT
I did a isMainThread check in App Delegate and Loading controller and both came back as true. Not sure why if they use the same context and store they wouldn't return the same array of objects.
Looks like the answer was related to another issue I had. Turns out the original developer added the core data stack Into the base controller and I was using the stack in the app delegate and passing it to the base controller. The base controller would overwrite my context with its own stack and this is why I couldn't get the fetch results expected in app delegate.
I would say from what I've learned over this project is to create the context in the app delegate and pass it to your first controller. Then in prepareForSegue or when you manually push pass the context along. Also in the view will disappear you check if your going back and update the stores context. If you do any multi threading then make sure in app delegate your context is nsmainconcurrencytype so you can create child context for other threads. This will keep data results as expected and conflicts to minimal.
Thanks for all the input on this. Your responses helped me track down the stupid issue.
I spend the whole night debugging a simple app. The app retrieves one image (yes one..intend to make my life easier) from web, and displays it in table view. I do that as a practice to learn Core Data. Before I fix it, the error message shows below:
2012-09-30 06:16:12.854 Thumbnail[34862:707] CoreData: error: Serious
application error. An exception was caught from the delegate of
NSFetchedResultsController during a call to
-controllerDidChangeContent:. Invalid update: invalid number of sections. The number of sections contained in the table view after
the update (1) must be equal to the number of sections contained in
the table view before the update (0), plus or minus the number of
sections inserted or deleted (0 inserted, 0 deleted). with userInfo
(null)
Basically it's saying that something goes wrong with FRC delegate methods. At one hand, section number changes from 0 to 1. At the other hand, "0 inserted, 0 deleted". So how the section number can increase? That should not happen.. Hence the error.
I fix the bug by simply adding [self.tableView reloadData] to my FRC setup method. I got the inspiration from this post, yet I don't quite understand it. The answer seems like too complicated and project specific. Could someone explain why adding reloadData can fix the bug? The answer might be an easy one, I hope so.
Key components of my app, if that matters:
Use UIManagedDocument to establish the core data stack
Create a helper method to download images from Flickr API
In NSManagedObject subclass file, try fetch image from persistent store. If it's not there yet, insert it into MOC.
- (void)setupFetchedResultsController
{
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"BigImage" inManagedObjectContext:self.document.managedObjectContext];
[fetchRequest setEntity:entity];
NSSortDescriptor *imageDescriptor = [[NSSortDescriptor alloc] initWithKey:#"image" ascending:YES];
NSArray *sortDescriptors = [NSArray arrayWithObject: imageDescriptor];
[fetchRequest setSortDescriptors:sortDescriptors];
[fetchRequest setFetchBatchSize:20];
// Create fetch results controller
self.fetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:self.document.managedObjectContext sectionNameKeyPath:nil cacheName:#"Root"];
self.fetchedResultsController.delegate = self;
NSError *error;
if (![self.fetchedResultsController performFetch:&error])
{
NSLog(#"Error in performFetch: %#, %#", error, [error userInfo]);
}
// Critical!! I add this line to fix the bug!
[self.tableView reloadData];
}
A fetched results controller tracks only changes to the managed object contents after the first fetch. These changes are then propagated to the table view using the delegate methods didChangeSection, didChangeObject etc.
But there is no automatic mechanism that the result of the initial fetch is sent to the table view. That is the reason why you have to call reloadData after performFetch.
However, there is a situation where this seems to work automatically. UITableViewController calls reloadData in viewWillAppear (if the table is loaded the first time). Therefore, if you set up the FRC for example in viewDidLoad, reloadData will be called in viewWillAppear, and you don't have to call it manually.
I need some help in using objects from Core Data with GCD; I seem to get NSManagedObjects that are aren't faulted into the main thread, even when I access their properties. Would appreciate some help.
This is what I'm doing: on launch, I need to load a list of Persons from the Core Data DB, do some custom processing in the background, then reload the table to show the names. I am following the guidelines for Core Data multi-threading by only passing in the objectIDs into the GCD queues. But when I reload the tableview on the main thread, I never see the name (or other properties) displayed for the contacts, and on closer inspection, the NSManagedObjects turn out to be faults on the main thread, even though I access various properties in cellForRowAtIndexPath. The name property is visible in the background thread when I NSLog it; and it's also showing correctly on the main thread in NSLogs in cellForRowAtIndexPath. But they don't show in the tableView no matter what I do. I tried accessing the name property using the dot notation, as well as valueForKey, but neither worked.
Here's my code …. it's called from the FRC initializer:
- (NSFetchedResultsController *)fetchedResultsController
{
if (__fetchedResultsController != nil)
{
return __fetchedResultsController;
}
__fetchedResultsController = [self newFetchedResultsControllerWithSearch:nil]; // creates a new FRC
[self filterAllContactsIntoDictionary: __fetchedResultsController];
return [[__fetchedResultsController retain] autorelease];
}
- (void) filterAllContactsIntoDictionary: (NSFetchedResultsController *) frc
{
NSArray *fetchedIDs = [[frc fetchedObjects] valueForKey:#"objectID"];
NSArray *fetched = [frc fetchedObjects];
if (filterMainQueue == nil) {
filterMainQueue = dispatch_queue_create("com.queue.FilterMainQueue", NULL);
}
dispatch_async(self.filterMainQueue, ^{
NSManagedObjectContext *backgroundContext = [[[NSManagedObjectContext alloc] init] autorelease];
[backgroundContext setPersistentStoreCoordinator:[[self.fetchedResultsController managedObjectContext] persistentStoreCoordinator]];
NSMutableArray *backgroundObjects = [[NSMutableArray alloc] initWithCapacity: fetchedIDs.count];
// load the NSManagedObjects in this background context
for (NSManagedObjectID *personID in fetchedIDs)
{
Person *personInContext = (Person *) [backgroundContext objectWithID: personID];
[backgroundObjects addObject:personInContext];
}
[self internal_filterFetchedContacts: backgroundObjects]; // loads contacts into custom buckets
// done loading contacts into character buckets ... reload tableview on main thread before moving on
dispatch_async(dispatch_get_main_queue(), ^{
CGPoint savedOffset = [self.tableView contentOffset];
[self.tableView reloadData];
[self.tableView setContentOffset:savedOffset];
});
});
}
What am I doing wrong here? Is there any other way to explicitly make the Person objects fire their faults on the main thread? Or am I doing something wrong with GCD queues and Core Data that I'm not aware of?
Thanks.
Why not take the easy route, since you are not saving anything new ?
Instead of creating an extra context for the background thread and working with IDs, use the main managedObjectContext in the background thread after locking it.
for example:
- (void) filterAllContactsIntoDictionary: (NSFetchedResultsController *) frc
{
if (filterMainQueue == nil) {
filterMainQueue = dispatch_queue_create("com.queue.FilterMainQueue", NULL);
}
dispatch_async(self.filterMainQueue, ^{
NSManagedObjectContext *context = ... // get the main context.
[context lock]; // lock the context.
// do something with the context as if it were on the main thread.
[context unlock]; // unlock the context.
dispatch_async(dispatch_get_main_queue(), ^{
CGPoint savedOffset = [self.tableView contentOffset];
[self.tableView reloadData];
[self.tableView setContentOffset:savedOffset];
});
});
}
This works for me when I call a method with performSelectorInBackground, so I guess it should work for GCD dispatch too.
Well, mergeChangesFromContextDidSaveNotification: is your friend. You'll need to tell the MOC on the main thread that there have been changes elsewhere. This will do the trick.
Here's Apple's documentation. To quote from there:
This method refreshes any objects which have been updated in the other context, faults in any newly-inserted objects, and invokes deleteObject:: on those which have been deleted.
EDIT: original answer removed, OP is not fetching in the background
I looked closer at your code and it doesn't look like you are doing anything that will change data and/or affect the context on the main thread.
You have a fetchedResultsController on the main thread. Presumably, this is working and your table is populating with data. Is this true?
When filterAllContentsIntoDictionary is invoked, you pass an array of the fetchedResultsController's current objectIDs to a background thread and do some processing on them (presumably filtering them based on some criteria) but you are not changing data and saving backgroundContext.
internalFilterFetchedContents is a black box. Without knowing what you intend for it to do, hard to say why it's not working.
When this is done, you reload the table on the main thread.
You haven't made any changes to the store, the context, or the fetchedResultsController so of course, the table shows the same data it did before. The missing details to help further are:
Is your tableView showing correct data from the fetchedResultsController to begin with? If not, most likely your only problem is in handling the tableView delegate and dataSource methods and the rest of this isn't really relevant.
What do you intend to happen in filterAllContentsIntoDictionary and internalFilterFetchedContents?
If your intent is to filter the data as displayed by the fetchedResultsController not sure you need to do anything in the background. If you modify the fetchRequest and do performFetch again your table will reload based on the new results.
I you need more help, please answer my questions, add more relevant code to your post and let me know if I'm missing anything wrt the problem and what you're trying to accomplish.
good luck!