开发者

Problem with NSFetchedResultsController updates and a to-one relationship

开发者 https://www.devze.com 2023-03-19 07:48 出处:网络
I\'m having some trouble with inserts using a NSFetchedResultsController with a simple to-one relationship. When I create a new Source object, which has a to-one relationship to a Target object, it se

I'm having some trouble with inserts using a NSFetchedResultsController with a simple to-one relationship. When I create a new Source object, which has a to-one relationship to a Target object, it seems to call - [(void)controller:(NSFetchedResultsController *)controller didChangeObject ... ] twice, with both NSFetchedResultsChangeInsert and NSFetchedResultsChangeUpdate types, which causes the tableview to display inaccurate data right after the update.

I can recreate this with a simple example based off the standard template project that XCode generates in a navigation-based CoreData app. The template creates a Event entity with a timeStamp attribute. I want to add a new entity "Tag" to this event which is just a 1-to-1 relation with Entity, the idea being that each Event has a particular Tag from some list of tags. I create the relationship from Event to Tag in the Core Data editor, and an inverse relationship from Tag to Event. I then generate the NSManagedObject sub-classes for both Event and Tag, which are pretty standard:

@interface Event : NSManagedObject {
@private
}
@property (nonatomic, retain) NSDate * timeStamp;
@property (nonatomic, retain) Tag * tag;

and
@interface Tag : NSManagedObject {
@private
}
@property (nonatomic, retain) NSString * tagName;
@property (nonatomic, retain) NSManagedObject * event;

I then pre-filled the Tags entity with some data at launch, so that we can pick from a Tag when inserting a new Event. In AppDelegate, call this before returning persistentStoreCoordinator:

NSManagedObjectContext *context = [self managedObjectContext];
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:@"Tag" inManagedObjectContext:context];
[fetchRequest setEntity:entity];

NSError *error = nil;
NSArray *fetchedObjects = [context executeFetchRequest:fetchRequest error:&error];
//check if Tags haven't already been created. If not, then create them
if (fetchedObjects.count == 0) {
    NSLog(@"create new objects for Tag");

    Tag *newManagedObject1 = [NSEntityDescription insertNewObjectForEntityForName:@"Tag" inManagedObjectContext:context];
    newManagedObject1.tagName = @"Home";

    Tag *newManagedObject2 = [NSEntityDescription insertNewObjectForEntityForName:@"Tag" inManagedObjectContext:context];
    newManagedObject2.tagName = @"Office";

    Tag *newManagedObject3 = [NSEntityDescription insertNewObjectForEntityForName:@"Tag" inManagedObjectContext:context];
    newManagedObject3.tagName = @"Shop";
}

[fetchRequest release];

if (![context save:&error])
{
    NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
    abort();
}

Now, I changed the insertNewObject code to add a Tag to the Event attribute we're inserting. I just pick the first one from the list of fetchedObjects for this example:

- (void)insertNewObject
{
    NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
    NSEntityDescription *entity = [[self.fetchedResultsController fetchRequest] entity];
    Event *newManagedObject = [NSEntityDescription insertNewObjectForEntityForName:[entity name] inManagedObjectContext:context];
    // If appropriate, configure the new managed object.
    // Normally you should use accessor methods, but using KVC here avoids the need to add a custom class to the template.
    [newManagedObject setValue:[NSDate date] forKey:@"timeStamp"];

    NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
    NSEntityDescription *entityTag = [NSEntityDescription entityForName:@"Tag" inManagedObjectContext:context];
    [fetchRequest setEntity:entityTag];
    NSError *errorTag = nil;
    NSArray *fetchedObjects = [context executeFetchRequest:fetchRequest error:&errorTag];
    if (fetchedObjects.count > 0) {
        Tag *newtag = [fetchedObjects objectAtIndex:0];
        newManagedObject.tag = newtag;
    }

    // Save the context.
    NSError *error = nil;
    if (![context save:&error])
    {
        NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
        abort();
    }
}

I want to now see the tableview reflecting these changes, so I made the UITableViewCell to type UITableViewCellStyleSubtitle and changed configureCell to show me the tagName in the detail text label:

- (void)configureCell:(UITableViewCell *)cell atIndexPath:(NSIndexPath *)indexPath
{
    Event *managedObject = [self.fetchedResultsController objectAtIndexPath:indexPath];
    cell.textLabel.text = [[managedObject valueForKey:@"timeStamp"] description];
    cell.detailTextLabel.text = managedObject.tag.tagName;
}

Now everything's in place. When I call insertNewObject, it seems to create the first row fine, but the 2nd row is a duplicate of the first, even though the timestamp should be a few seconds apart:

Problem with NSFetchedResultsController updates and a to-one relationship

When I scroll the screen up and down, it refreshes the rows and then displays the right results with the correct time. When I step through the code, the core problem comes up: inserting a new row seems to be calling [(NSFetchedResultsController *)controller didChangeObject ...] twice, once for the insert and once for an update. I'm not sure WHY the update is called though. And here's the clincher: if I remove the inverse relationship between Event and Tag, the inserts starts working just fine! Only the insert is called, the row isn't duplicated, and things work well.

So what is it with the inverse relationship that is causing NSFetchedResultsController delegate methods to be called twice? And should I just live without them in this case? I know that XCode gives a warning if the 开发者_运维技巧inverse isn't specified, and it seems like a bad idea. Am I doing something wrong here? Is this some known issue with a known work-around?

Thanks.


With regards to didChangeObject being called multiple times, I found one reason why this will going to happen. If you have multiple NSFetchedResultsController in your controller that shares NSManagedObjectContext, the didChangeObject will be called multiple times when something changes with the data. I stumbled on this same issue and after a series of testing, this was the behavior I noticed. I have not tested though if this behavior will going to happen if the NSFetchedResultsControllers does not share NSManagedObjectContext. Unfortunately, the didChangeObject does not tell which NSFetchedResultsController triggered the update. To achieve my goal, I ended up using a flag in my code.

Hope this helps!


You can use [tableView reloadRowsAtIndexPaths:withRowAnimation:] for NSFetchedResultsChangeUpdate instead of configureCell method.

case NSFetchedResultsChangeUpdate:
    [tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
    break;


I had the same problem. And have a solution. Under certain circumstances, the NSFetchedResultsController gets fired twice when calling the -(BOOL)save: on the managed object context, directly after inserting or manipulating.

In my case, I'm doing some magic with the object in the NSManagedObject -(void)willSave method, which causes the NSFetchedResultsController to fire twice. This seems to be a bug.

Not to manipulate inserted objects while being saved did the trick for me!

To delay the context save to a later run loop seems to be another solution, for example:

dispatch_async(dispatch_get_main_queue(), ^{ [context save:nil]; });


Objects in NSFetchedResultsController must be inserted with permanent objectID. After creating object and before saving to persistent store, it has temporary objectID. After saving object receive permanent objectID. If object with temporary objectID is inserted into NSFetchedResultsController, then after save object and change its objectID to permanent, NSFetchedResults controller may report about inserting fake duplicate object. Solution after instantiating object that will be fetched in NSFetchedResultsController - just call obtainPermanentIDsForObjects on its managedObjectContext with it.

0

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号