NSFetchedResultsControllerDelegate not firing - iphone

I can't figure out why, for the life of me, the NSFetchedResultsControllerDelegate methods are not firing when I add data to the underlying datastore. The data shows up immediately if I restart the iPhone application.
I have subclassed UITableViewController and conform to NSFetchedResultsControllerDelegate:
#interface ProjectListViewController : UITableViewController <NSFetchedResultsControllerDelegate> {
NSFetchedResultsController* fetchedResultsController_;
NSManagedObjectContext* managedObjectContext_;
}
I instantiate the NSFetchedResultsController and set the delegate to self:
// Controller
fetchedResultsController_ = [[NSFetchedResultsController alloc] initWithFetchRequest:request
managedObjectContext:self.managedObjectContext
sectionNameKeyPath:#"Client"
cacheName:#"ProjectsCache"];
fetchedResultsController_.delegate = self;
I implement the delegate methods:
- (void)controllerWillChangeContent:(NSFetchedResultsController*)controller {
NSLog(#"ProjectListViewController.controllerWillChangeContent");
// The fetch controller is about to start sending change notifications, so prepare the table view for updates.
[self.tableView beginUpdates];
}
- (void)controllerDidChangeContent:(NSFetchedResultsController*)controller { ... }
- (void)controller:(NSFetchedResultsController*)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath { ... }
- (void)controller:(NSFetchedResultsController *)controller didChangeSection:(id<NSFetchedResultsSectionInfo>)sectionInfo atIndex:(NSUInteger)sectionIndex forChangeType:(NSFetchedResultsChangeType)type { ... }
I create the entity I wish to save:
Project* newProject = [NSEntityDescription insertNewObjectForEntityForName:#"Project" inManagedObjectContext:self.managedObjectContext];
ProjectDetailViewController* detail = [[ProjectDetailViewController alloc] initWithStyle:UITableViewStyleGrouped
delegate:self
selector:#selector(finishedAdding:)
project:newProject];
And later, I save it:
- (void)save {
// NSLog(#"ProjectDetailViewController.save");
self.project.name = projectNameTextField_.text;
NSError* error;
BOOL b = [self.project.managedObjectContext save:&error];
if (!b) {
NSLog(#"Error saving project!");
} else {
NSLog(#"Project was successfully saved.");
[delegate_ performSelector:selector_ withObject:self.project];
}
[self dismissModalViewControllerAnimated:YES];
}
It all works just fine except for the fact that my delegate methods don't fire. Obviously my table view doesn't get updated and the only way to see the new data is to explicitly refresh or restart the app.
I've looked through the CoreData Recipe app - but can't seem to find what I'm missing. Thoughts?
-Luther

If I change the sectionNameKeyPath from #"Client" to nil when I create the original fetchedResultsController_, both the Project entity creation and save indeed invoke the delegate methods!
fetchedResultsController_ =
[[NSFetchedResultsController alloc] initWithFetchRequest:request
managedObjectContext:self.managedObjectContext
sectionNameKeyPath:nil
cacheName:#"ProjectsCache"];
Upon hyper scrutiny, that is what the CoreData Recipes example does. As my data gets more complex - I think I'm going to need that argument to help break the results up into sections but for now, it is nice to see the delegate handlers being invoked.

My read of the above looks like you are using 2 NSManagedObjectContexts, one in ProjectListViewController, and seperate one in ProjectDetailViewController (I assume it is created there since I don't see it passed in.
When you save one context it does not automatically propagate changes into another, so saving the one in ProjectDetailViewController will not cause the changes to appear in the context of ProjectListViewController, which means there are no changes in that one to tell the delegate about it.
If you want to push the changes between contexts, look at NSManagedObjectContextDidSaveNotification and mergeChangesFromContextDidSaveNotification: (they are the end of the NSManagedObjectContext documentation).

Related

Using core data with and with out a nsfetchedResultsController

In core data, you initially add objects/set their attributes values using:
-(IBAction)save{
if (self.managedObjectContext == nil)
{
self.managedObjectContext = [(RootAppDelegate *)[[UIApplication sharedApplication] delegate] managedObjectContext];
}
Frame *f = [NSEntityDescription insertNewObjectForEntityForName:#"Frame" inManagedObjectContext:self.managedObjectContext];
f.typeLabel = self.textFieldtext.text;
[self dismissViewControllerAnimated:YES completion:nil];
NSError *error;
if (![self.managedObjectContext save:&error]) {
NSLog(#"Error");
}
}
And you would typically edit the values using:
-(IBAction)save{
[self.f setValue:self.newTextfield.text forKey:#"typeLabel"];
[self dismissViewControllerAnimated:YES completion:nil];
NSError *error;
if (![self.managedObjectContext save:&error]) {
//Handle Error
}
}
It's obviously a bit different using A NSFetchedResultsController
I guess my question would be, how can I set properties and edit them using a NSFetchedResultsController?
A fetched results controller acts as a link between a fetch request and a table view. The useful part is that if you make any changes to the managed object context that would affect the results of the fetch request, the FRC automatically picks up on these and sends various delegate methods which you can tie in to your table view datasource code to keep the table up to date. See "Implementing the Table View Datasource Methods" here.
Your code above isn't really relevant to this, unless it is contained within a modal view controller that is called from a table view displaying the results of a fetch request, and is used for adding new items. In that case, the code above would be identical, but when you returned to the table view, it would already contain your new data.

App Crash on setting UITableViewCell:textLabel:text

I am creating a split-view iPad application. When the user presses the bar button item in the master view, a modal is presented. This modal has a textfield and has an IBAction to pick up keyboard returns.
On keyboard returns, a new instance of my Farm class is created (code below). This instance is then added to an array that is stored in my delegate. I then try to reload the MasterViewController's table. Upon this reload the application crashes on cell.textLabel.text with a EXC_BAD_ACCESS error.
Farm *current = [delegate.arrayOfFarms objectAtIndex:indexPath.row];
cell.textLabel.text = [current getFarmTitle];
If I ask the array within the delegate how many elements it has, it will indeed show the current amount, even. This is what is bizarre to me about this whole thing: the Farm instances appear to be in existence.
I have instances of AppDelegate in both my MasterViewController and my NewFarmNamingView classes. The instance in the Master is to populate the table. The instance in NewFarm is to add the newly created Farm to the delegate. Code below.
Segments from class NewFarmNamingView:
- (IBAction) keyboardDonePushed:(id)sender
{
// create a Farm and add it to the delegate
NSString *text = newFarmTextField.text;
Farm *newFarm = [[Farm alloc] init];
[newFarm setFarmTitle:text];
[[delegate arrayOfFarms] addObject:newFarm];
[newFarm release];
NSLog(#"Added farm: %#" , text);
// dismiss the view
[self closeView:nil];
}
- (void)viewDidLoad
{
[super viewDidLoad];
// initialize the delegate
delegate = [[UIApplication sharedApplication] delegate];
}
Segments from the class Farm
- (void) setFarmTitle : (NSString *) _farmTitle
{
farmTitle = _farmTitle;
}
- (NSString *) getFarmTitle
{
return farmTitle;
}
// NSCoding Methods
- (void) encodeWithCoder:(NSCoder *)aCoder
{
[aCoder encodeObject:farmTitle forKey:#"kFarmTitle"];
}
- (id) initWithCoder:(NSCoder *)aDecoder
{
farmTitle = [aDecoder decodeObjectForKey:#"kFarmTitle"];
return self;
}
// Initialization method
- (id)init
{
self = [super init];
if (self) {
// Initialization code here.
}
return self;
}
From the runtime reference: "objc_msgsend sends a message to the receiver and expects a simple return value."
I'll bet you anything that what you're returning (if you're returning anything at all) in that class method getTitleFarm is returning an incorrect value. It should be an NSString. Be absolutely sure it is returning an NSString, and not anything else.
If you need to use the respondsToSelector method to see if the class is being released, try:
if([current respondsToSelector:#selector(getFarmTitles)]) {. [current getFarmTitle];
}
else {
NSLog:(#"FAILURE!!");
}
EDIT: maybe you are not retaining or even creating this string at all. In it's initialization, wrap it in a retain]; message

problem with delegate of object which is delegate of NSURLConnection

I've a class PictureDownloader for the purpose of asynchronously loading images from a server. It assigns itself as a delegate of NSURLConnection and as such, is retained by NSURLConnection. I create several of those PictureDownloader in a DetailViewController to fetch the corresponding images, so the DetailViewController is a delegate of each PictureDownloader.
When the user leaves the DetailViewController, all remaining downloads are cancelled, however sometimes it seems to be the case, that a PictureDownloader has finished loading an image (connectionDidFinishedLoading called) before the connection was cancelled, but the DetailViewController doesn't exist anymore (but the PictureDownloader does, because it's retained by NSURLConnection), so the call
[self.delegate didLoadPictureWithID:self.ID];
inside PictureDownloader will give an EXC_BAD_ACCESS or sometimes a "unrecognized selector sent to instance".
Here are the relevant parts of the source code:
creation of the PictureDownloader inside the DetailViewController
- (void)startPictureDownload:(Picture *)pic withPictureId:(NSString *)pId forID:(int)ID
{
PictureDownloader *downloader = [self.downloadsInProgress objectForKey:[NSNumber numberWithInt:ID]];
if(!downloader)
{
downloader = [[PictureDownloader alloc] init];
downloader.picture = pic;
downloader.pictureId = pId;
downloader.ID = ID;
downloader.delegate = self;
[self.downloadsInProgress setObject:downloader forKey:[NSNumber numberWithInt:ID]];
[downloader startDownload];
[downloader release];
}
}
canceling the downloads (called when the DetailViewController returns to the overview)
- (void)cancelAllDownloads
{
[self.downloadsInProgress enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop){
[obj cancelDownload];
}];
}
delegate method which is called when PictureDownloader finished loading
- (void)didLoadPictureWithID:(int)dID;
{
PictureDownloader *downloader = [self.downloadsInProgress objectForKey:[NSNumber numberWithInt:dID]];
if(downloader)
{
UIImageView *imageView = (UIImageView *)[self.view viewWithTag:dID];
imageView.image = [UIImage imageWithData:downloader.imageData];
[self.downloadsInProgress removeObjectForKey:[NSNumber numberWithInt:dID]];
}
}
cancelDownload method inside PictureDownloader
- (void)cancelDownload
{
[self.imageConnection cancel];
self.imageConnection = nil;
self.imageData = nil;
}
connectionDidFinishedLoading inside PictureDownloader
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
if(self.picture)
{
self.picture.data = self.imageData;
NSError *error = nil;
[self.picture.managedObjectContext save:&error];
}
if(self.delegate != nil && [self.delegate respondsToSelector:#selector(didLoadPictureWithID:)] ) //place of failure
[self.delegate didLoadPictureWithID:self.ID];
self.imageData = nil;
self.imageConnection = nil;
}
Can someone give me a hint, how I can deal with this problem?
Help is much appreciated.
To avoid situations like this, I usually add a check like this at the top of connectionDidFinishLoading: and other NSURLConnection delegate methods:
if (connection != self.imageConnection) return;
As another option, you could set the delegate on each PictureDownloader to nil as you cancel it in cancelAllDownloads. Or you could set self.delegate = nil in cancelDownload.
You should check for the existence of the delegate object (and ideally the method/selector) before you attempt to make the call.
For example:
if(self.delegate && [[self.delegate] respondsToSelector:#selector(didLoadPictureWithID:)]) {
...
}
By doing this, you'll ensure that your not attempting to call a delegate that's no longer there. For more information on the respondsToSelector method, see the NSObject Protocol Reference.
When your DetailViewController goes out of scope - dealloc -, set the PictureDownloader's delegate property to nil as well.
Your issue is interesting in that the NSUrlConnection's delegate can't be set to nil in the same way. e.g. When you're PictureDownloader is de-alloced. All you can do is cancel the NSUrlConnection.
The NSURLConnection docs say the following:
Unless a NSURLConnection receives a cancel message, the delegate will receive one and only one of connectionDidFinishLoading:, or connection:didFailWithError: message, but never both. In addition, once either of messages are sent, the delegate will receive no further messages for the given NSURLConnection.
Indicating that the you can verify that the delegate will not be called back after the above messages are received.

How to synchronise two NSManagedObjectContext

I'm working on an ipad application that use coredata. It download information on a database that is on the web, and record them in coredata. The application is based on a split view. My problem was to make the download and the record of the data in background. Here is how I've done :
- I've create an NSOperation, that does the download and the record of the data.
- This NSOperation use a different NSManagedObjectContext than the context of the appDelegate, return by this function, that is in the appDelegate :
(NSManagedObjectContext*)newContextToMainStore {
NSPersistentStoreCoordinator *coord = nil;
coord = [self persistentStoreCoordinator];
NSManagedObjectContext *moc = [[NSManagedObjectContext alloc] init];
[moc setPersistentStoreCoordinator:coord];
return [moc autorelease];
}
- I've had an observer in the NSOperation, that will call this function in the appDelegate when I save the context, to modify the context of the delegate too :
- (void)mergeChangesFromContextSaveNotification:(NSNotification*)notification {
[[self managedObjectContext]mergeChangesFromContextDidSaveNotification:notification];
}
But I've a problem, the synchronisation doesn't work, because the data on the rootViewController (that is a UITableViewController), that have a NSManagedObjectContext initialised with the context of the appDelegate and use as datasource a NSFetchedResultsController, don't actualise automatically the informations, as it normaly must do.
So I ask you : What did I do wrong ? Is it the good way to use two different context and synchonise them ?
What you have here looks correct. You do want to make sure you implement the NSFetchedResultControllerDelegate methods in the rootViewController so the changes will appear in the UI.

CoreData weird behavior when data are loaded on background thread

I have very strange problem, when I don't understand what's going on at all, so I'm looking for explanation of it. Situation is as following:
I have a view controller with scrollview with three subviews in it. Those three subviews have method
-(void)loadContent
which loads content from database using CoreData in the background thread, creates subviews which represent loaded items and add them as own subviews calling [self addSubview: itemView]; That method is invoked as
[self performSelectorInBackground: #selector(loadContent) withObject: nil];
To load data from DB I'm using a singleton service class. Everything worked fine, but when those three views are loading their portions of data, it sometimes crashes the app.
I guessed it's because it shares one NSManagedObjectContext instance for all read operations, so I rewrote the class so it shares only NSManagedObjectModel and NSPersistentStoreCoordinator instances and creates it's own NSManagedObjectContext instance.
Suddenly, very strange thing happened. Data are loaded ok, subviews are created and added to the view hierarchy, but it get never displayed on the screen. When I switch back to the old singleton service class (sharing one managedObjectContext), it works again like a charm! (but with risk of crashing the app, though).
I absolutely don't get the point how loading data from DB is related to displaying items on the screen. More on that - when subviews are created and added to the view hierarchy, why the hell it don't get displayed?
The source looks like this:
- (void) loadContent {
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
NSArray *results = [(WLDataService *)[WLDataService service] loadItemsForGDView];
NSUInteger channelPosition = 0;
CGFloat position = 0.0;
CGFloat minuteWidth = ((self.superview.frame.size.width / 2.0) / 60.0);
for(Item *it in results) {
/// On following lines size and position of the view is computed according to item setup - skipping here...
/// Create item; it's simple subclass of UIView class
WLGDItemView *item = [[WLGDItemView alloc] init];
/// Variables used here are declared above when size and position is computed
item.frame = CGRectMake(itemX, itemY, itemWidth, itemHeight);
[self performSelectorOnMainThread: #selector(addSubview:) withObject: item waitUntilDone: NO];
/// This is just helper macro to release things
WL_RELEASE_SAFELY(item);
}
[pool drain];
}
The basic service class (non-singleton one) implementation is as follows (just interesting parts):
#import "WLLocalService.h"
static NSPersistentStoreCoordinator *sharedPSC = nil;
static NSManagedObjectModel *sharedMOM = nil;
#implementation WLLocalService
#synthesize managedObjectContext;
/// This is here for backward compatibility reasons
+ (WLLocalService *) service {
return [[[self alloc] init] autorelease];
}
#pragma mark -
#pragma mark Core Data stack
- (NSManagedObjectContext *) managedObjectContext {
if (managedObjectContext == nil) {
NSPersistentStoreCoordinator *coordinator = [self persistentStoreCoordinator];
if (coordinator != nil) {
managedObjectContext = [[NSManagedObjectContext alloc] init];
[managedObjectContext setPersistentStoreCoordinator: coordinator];
}
[managedObjectContext setUndoManager: nil];
[managedObjectContext setMergePolicy: NSMergeByPropertyStoreTrumpMergePolicy];
}
return managedObjectContext;
}
- (NSManagedObjectModel *) managedObjectModel {
if(sharedMOM == nil) {
sharedMOM = [[NSManagedObjectModel mergedModelFromBundles: nil] retain];
}
return sharedMOM;
}
- (NSPersistentStoreCoordinator *) persistentStoreCoordinator {
if(sharedPSC == nil) {
NSURL *storeUrl = [self dataStorePath];
NSError *error = nil;
sharedPSC = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel: [self managedObjectModel]];
if (![sharedPSC addPersistentStoreWithType: NSSQLiteStoreType configuration: nil URL: storeUrl options: nil error: &error]) {
WLLOG(#"%#: %#", error, [error userInfo]);
}
}
return sharedPSC;
}
#pragma mark -
#pragma mark Path to data store file
- (NSURL *) dataStorePath {
return [NSURL fileURLWithPath: [WL_DOCUMENTS_DIR() stringByAppendingPathComponent: #"/DB.sqlite"]];
}
- (void)dealloc {
WL_RELEASE_SAFELY(managedObjectModel);
[super dealloc];
}
#end
I'd really love to know what's going on here and why it behaves so strange (and - of course - why it does not work, in particular). Can anybody explain that?
thanks to all
Have you read Multi Threading with Core-Data twice?
First, do not load or construct UI elements on a background thread. The UI (whether on the desktop or on the iPhone) is single threaded and manipulating it on multiple threads is a very bad idea.
Second, data that you load into one context will not be immediately visible in another context. This is what is causing part of your problem.
The solution is to move all your UI code to the main thread and warm up the Core Data cache on a background thread. This means to load the data on a background thread (into a separate cache) to load it into the NSPersistentStoreCoordinator cache. Once that is complete your main thread can access that data very quickly because it is now in memory.
You realize that [WLDataService service] does not actually return a singleton? It creates a new instance every time. So you are effectively working with multiple instances of the Core Data components.
What about:
static WLDataService* gSharedService = NULL;
#implementation WLDataService
+ (id) service
{
#synchronized (self) {
if (gSharedService == NULL) {
gSharedService = [[self alloc] init];
}
}
return gSharedService;
}
#end
That will create the same instance every time. You will also want to make your managedObjectContext, managedObjectModel and persistentStoreCoordinator methods thread safe by using a #synchronized block. Otherwise there is a change that multiple threads will initialize those at the same time, leading to unexpected behaviour.