AOphagen
AOphagen

Reputation: 308

NSFetchedResultsController: NSSortDescriptor with relation as key sends unrecognized selector compare: to core data entity object

I am trying to save a one-to-many relation in core data. There is a user decision involved to determine whether the new child list object needs to be attached to a new parent object. In the other case an existing database entry is used as a parent object. Under certain circumstances after saving, the app crashes.

FINAL EDIT: Sorry if you mind me keeping all of the edits, I still will. The process of enlightenment was quite convoluted. After all I started out thinking it was a data conflict... Thanks again to Tom, who helped point me in the right direction: I am still using a relation for sorting and grouping core data entities with an NSFetchedResultsController. I have written a valid compare: method for my entity class now and so far from what I can see it is working. I am about to write an answer for my question. I will still be very grateful for any information or warnings from you concerning this!

EDIT 3: The save procedure and the user alert seem to be incidental to the problem. I have zoomed in on the NSFetchedResultsController now and on the fact that I am using a relation ('examination') as sectionNameKeyPath. I will now try to write a compare: method in a category to my Examination entity class. If that does not work either, I will have to write a comparable value into my Image entity class in addition to the relation and use that for sections. Are y'all agreed?

EDIT 1: The crash only occurs after the user has been asked whether she wants a new examination and has answered YES. The same method is also entered when there was no user prompt (when the creation of a new examination has been decided by facts (no examination existing = YES, existing examination not timed-out = NO). In these cases the error does NOT occur. It must be that the view finishes loading while the alert view is open and then the collection view and its NSFetchedResultsController join the fun.

EDIT 2: Thanks to Tom, here is the call stack. I did not think it was relevant, but the view controller displays images in a collection view with sections of images per examination descending. So both the section key and the sort descriptor of the NSFetchedResultsController are using the examination after the MOCs change notification is sent. It is not the save that crashes my app: it is the NSSortDescriptor (or, to be fair, my way to use all of this). image of the call stack


The code for the NSFetchedResultsController:

#pragma mark - NSFetchedResultsController

- (NSFetchedResultsController *)fetchedResultsController
{
    if (m_fetchedResultsController != nil) {
        return m_fetchedResultsController;
    }

    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    // Edit the entity name as appropriate.
    NSEntityDescription *entity = [NSEntityDescription entityForName:NSStringFromClass([Image class]) inManagedObjectContext:[[HLSModelManager currentModelManager] managedObjectContext]];
    [fetchRequest setEntity:entity];

    // Set the batch size to a suitable number.
    [fetchRequest setFetchBatchSize:20];

    // Edit the sort key, identical sort to section key path must be first criterion
    NSSortDescriptor *examinationSortDescriptor = [[NSSortDescriptor alloc] initWithKey:kexaminationSortDescriptor ascending:NO];
    NSSortDescriptor *editDateSortDescriptor = [[NSSortDescriptor alloc] initWithKey:keditDateSortDescriptor ascending:NO];

    NSArray *sortDescriptors =[[NSArray alloc] initWithObjects:examinationSortDescriptor, editDateSortDescriptor, nil];

    [fetchRequest setSortDescriptors:sortDescriptors];

    // Edit the section name key path and cache name if appropriate.
    // nil for section name key path means "no sections".
    NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:[[HLSModelManager currentModelManager] managedObjectContext] sectionNameKeyPath:kSectionNameKeyPath cacheName:NSStringFromClass([Image class])];
    aFetchedResultsController.delegate = self;
    m_fetchedResultsController = aFetchedResultsController;

    NSError *error = nil;
    if (![self.fetchedResultsController performFetch:&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.
        HLSLoggerFatal(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }

    return m_fetchedResultsController;
}

#pragma mark - NSFetchedResultsControllerDelegate - optional

/* Asks the delegate to return the corresponding section index entry for a given section name.  Does not enable NSFetchedResultsController change tracking.
 If this method isn't implemented by the delegate, the default implementation returns the capitalized first letter of the section name (seee NSFetchedResultsController sectionIndexTitleForSectionName:)
 Only needed if a section index is used.
 */
- (NSString *)controller:(NSFetchedResultsController *)controller sectionIndexTitleForSectionName:(NSString *)sectionName
{
    return sectionName;
}

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller
{
    // In the simplest, most efficient, case, reload the table view.
    [[self collectionView] reloadData];
}

/* THE OTHER DELEGATE METHODS ARE ONLY FOR UITableView! */

The code for saving examination (existing or new) and the new image:

-(BOOL)saveNewImage
{
    BOOL done = NO;

    // remove observer for notification after alert
    [[NSNotificationCenter defaultCenter] removeObserver:self name:kExaminationTimedoutAlertDone object:nil];

    Examination * currentExamination = [self getCurrentExamination];

    if ([self userWantsNewExamination] == YES)
    { // if an examination was found
        if (currentExamination != nil)
        { // if the found examination is not closed yet
            if ([currentExamination endDate] == nil)
            { // close examination & save!
                [currentExamination closeExamination];
                NSError *savingError = nil;
                [HLSModelManager saveCurrentModelContext:(&savingError)];

                if (savingError != nil)
                {
                    HLSLoggerFatal(@"Failed to save old, closed examination: %@, %@", savingError, [savingError userInfo]);
                    return NO;
                }
            }
        }
        currentExamination = nil;
    }

    // the examination to be saved, either new or old
    Examination * theExamination = nil;

    // now, whether user wants new examination or no current examination was found - new examination will be created
    if (currentExamination == nil)
    {
        // create new examination
        theExamination = [Examination insert];
        if (theExamination == nil)
        {
            HLSLoggerFatal(@"Failed to create new examination object.");
            currentExamination = nil;
            return NO;
        }

        // set new examinations data
        [theExamination setStartDate: [NSDate date]];
    }
    else
    {
        theExamination = currentExamination;
    }

    if (theExamination == nil)
    { // no image without examination!
        HLSLoggerFatal(@"No valid examination object.");
        return NO;
    }

    Image *newImage = [Image insert];

    if (newImage != nil)
    {
        // get users last name from app delegate
        AppDelegate * myAppDelegate = (AppDelegate *) [[UIApplication sharedApplication] delegate];

        // set image data
        [newImage setEditUser: [[myAppDelegate user] lastName]];
        [newImage setEditDate: [NSDate date]];
        [newImage setExamination: theExamination];
        [newImage setImage: [self stillImage]];
        [newImage createImageThumbnail];

        // update edit data
        [theExamination setEditUser: [[myAppDelegate user] lastName]];
        [theExamination setEditDate: [NSDate date]];
        // unnecessary! CoreData does it automatically! [theExamination addImagesObject:newImage];

        //! Important: save all changes in one go!
        NSError *savingError = nil;
        [HLSModelManager saveCurrentModelContext:(&savingError)];

        if (savingError != nil)
        {
            HLSLoggerFatal(@"Failed to save new image + the examination: %@, %@", savingError, [savingError userInfo]);
        }
        else
        {
            // reload data into table view
            [[self collectionView] reloadData];
            return YES;
        }
    }
    else
    {
        HLSLoggerFatal(@"Failed to create new image object.");
        return NO;
    }

    return done;
}

The error:

2013-05-22 17:03:48.803 MyApp[11410:907] -[Examination compare:]: unrecognized selector sent to instance 0x1e5e73b0
2013-05-22 17:03:48.809 MyApp[11410:907] CoreData: error: Serious application error.  Exception was caught during Core Data change processing.  This is usually a bug within an observer of NSManagedObjectContextObjectsDidChangeNotification.  -[Examination compare:]: unrecognized selector sent to instance 0x1e5e73b0 with userInfo (null)
2013-05-22 17:03:48.828 MyApp[11410:907] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Examination compare:]: unrecognized selector sent to instance 0x1e5e73b0'

And here are the entity class files, too:

//
//  Examination.h
//  MyApp
//

#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>

@class Image;

@interface Examination : NSManagedObject

@property (nonatomic, retain) NSDate * editDate;
@property (nonatomic, retain) NSString * editUser;
@property (nonatomic, retain) NSDate * endDate;
@property (nonatomic, retain) NSDate * startDate;
@property (nonatomic, retain) NSSet *images;
@end

@interface Examination (CoreDataGeneratedAccessors)

- (void)addImagesObject:(Image *)value;
- (void)removeImagesObject:(Image *)value;
- (void)addImages:(NSSet *)values;
- (void)removeImages:(NSSet *)values;

@end

//
//  Examination.m
//  MyApp
//

#import "Examination.h"
#import "Image.h"

@implementation Examination

@dynamic editDate;
@dynamic editUser;
@dynamic endDate;
@dynamic startDate;
@dynamic images;

@end

Upvotes: 1

Views: 984

Answers (1)

AOphagen
AOphagen

Reputation: 308

This error had nothing to do with the saving of data to the MOC.

Because the saving of the new image data is triggered in the prepareForSegue of the previous view controller and the user alert gives the next view controller the time to finish loading, also creating the NSFetchedResultsController and its connection to its delegate, the exception was raised in the temporary context of the save to the MOC and only after the user alert.

The NSFetchedResultsController had started listening for changes of the MOC only in this case. It seems that if it gets alerted to an MOC change it will fetch only the changes and only then it needs to compare the new data with the existing data. Further information on this would be very welcome!

Then, because I had set a sort descriptor (and also the sectionNameKeyPath) to a relation and not provided the means to sort the entity objects in my core data entity class, the NSFetchedResultsController could not continue. Looking back it seems all so easy and natural, I really become suspicious of the simplicity of my solution...

I find it interesting that it could fetch the initial data in one go, when no change interfered. After all it was using the same NSSortDescriptor. Any ideas?

This is my solution:

//
//  MyCategoryExamination.m
//  MyApp
//

#import "MyCategoryExamination.h"

@implementation Examination (MyCategoryExamination)

- (NSComparisonResult)compare:(Examination *)anotherExamination;
{
    return [[self startDate] compare:[anotherExamination startDate]];
}

@end

Please tell me if there is something wrong with this.

Upvotes: 1

Related Questions