views:

310

answers:

1

I have a fetched results controller with a predicate on my Thing entity that looks like this: "thingDomain.domainCurrentAccount !=NULL". It finds all of my Thing objects that are in the current domain. The current domain is defined by a one-to-one relationship between the Account entity and one of the Domain entities. Each Thing belongs to one Domain, and each Domain has many Things. All Domains except one have a NULL relationship with the Account.

This works and my tableview is happy.

Later I change the current domain by modifying the relationship with Account. I add more Things to a Domain that is now the current domain, they are added to the tableview since they satisfy the predicate. However the Things that belong to the now non-current Domain remain in the tableview even though they do not satisfy the predicate any more.

I tried re-fetching, but that had no effect, so I am puzzled as to what fetching again actually does. Does it reevaluate the predicate against the entities?

I fixed the problem by deleting all the Things in the current Domain before changing the current domain, but feel that I should not have to do this.

+1  A: 

Did you reload the table view after performing the fetch again?

This worked for me:

self.fetchedResultsController = nil;
NSError *error = nil;
if (![self.fetchedResultsController performFetch:&error]) {
    NSLog(@"Error in refetch: %@",[error localizedDescription]);
    abort();
}

[self.tableView reloadData];

UPDATE
The fetched results controller is observing Things objects for changes, NOT Domain objects. Since we are changing the Account on a Domain object, it make sense that the fetched results controller is not updating (except for the new Thing).
The ideal solution would be a way to tell the managed object context or fetched results controller which exact Things have "changed" (loop on the Things in the Domain that previously had the Account), but this would still be a manual process.
The alternative is to perform the fetch again and reload the table which worked for me.

I created this model based on your description:

Account <---> Domain <--->> Thing

To test this, I created Things "One" and "Two" in Domain domainOne and Things "Three", "Four", and "Five" in Domain domainTwo. domainOne has the Account while domainTwo has no Account.
When the app loads, the table view shows Things "One" and "Two" as expected.
When I tap "Change", domainTwo is assigned the Account and domainOne loses its account. Also, a new Thing named "Six" is created in domainTwo.
Thing "Six" appears along with "One" and "Two" even though they no longer meet the predicate. This matches the issue that you describe.
Then, I tap "Refetch" and the table reloads with "Three", "Four", "Five", and "Six". This is the desired behavior.

Here is the relevant view controller code:

- (void)addTestData {
    account = [NSEntityDescription insertNewObjectForEntityForName:@"Account" inManagedObjectContext:self.managedObjectContext];
    domainOne = [NSEntityDescription insertNewObjectForEntityForName:@"Domain" inManagedObjectContext:self.managedObjectContext];
    domainTwo = [NSEntityDescription insertNewObjectForEntityForName:@"Domain" inManagedObjectContext:self.managedObjectContext];
    [domainOne setValue:account forKey:@"domainCurrentAccount"];

    NSArray *thingNames = [NSArray arrayWithObjects:@"One", @"Two", @"Three", @"Four", @"Five", nil]; 
    for (NSString *name in thingNames) {
     NSManagedObject *thing = [NSEntityDescription insertNewObjectForEntityForName:@"Thing" inManagedObjectContext:self.managedObjectContext];
     [thing setValue:name forKey:@"thingName"];
     if (name == @"One" || name == @"Two") {
      [thing setValue:domainOne forKey:@"thingDomain"];
     } else {
      [thing setValue:domainTwo forKey:@"thingDomain"];
     }
    }
}

- (void)change {
    [domainTwo setValue:account forKey:@"domainCurrentAccount"];
    [domainOne setValue:nil forKey:@"domainCurrentAccount"];
    NSManagedObject *thing = [NSEntityDescription insertNewObjectForEntityForName:@"Thing" inManagedObjectContext:self.managedObjectContext];
    [thing setValue:@"Six" forKey:@"thingName"];
    [thing setValue:domainTwo forKey:@"thingDomain"];
}

- (void)refetch {
    self.fetchedResultsController = nil;
    NSError *error = nil;
    if (![self.fetchedResultsController performFetch:&error]) {
     NSLog(@"Error in refetch: %@",[error localizedDescription]);
     abort();
    }

    [self.tableView reloadData];
}
- (void)viewDidLoad {
    [super viewDidLoad];

    UIBarButtonItem *changeButton = [[UIBarButtonItem alloc] initWithTitle:@"Change" style:UIBarButtonItemStyleBordered target:self action:@selector(change)];
    self.navigationItem.leftBarButtonItem = changeButton;
    [changeButton release];

    UIBarButtonItem *refetchButton = [[UIBarButtonItem alloc] initWithTitle:@"Refetch" style:UIBarButtonItemStyleBordered target:self action:@selector(refetch)];
    self.navigationItem.rightBarButtonItem = refetchButton;
    [refetchButton release];

    [self addTestData];

    NSError *error = nil;
    if (![[self fetchedResultsController] performFetch:&error]) {
     NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
     abort();
    }
}
gerry3
I tried that with no effect. It should not be necessary anyway since the fetched results controller is keeping and eye on all changes on the MOC and does all the other table updates it needs on its collection. Reloading tableviews is a last resort since it it's not animated and kill any animation in progress.
Steve Weller
I explained why this isn't working the way we initially expected and added details for my original solution which does work.
gerry3
Interesting, but there is a key difference in your code: in refetch you throw away the current fetched result controller and create a new one. I'm specifically asking about using the same controller and asking it to do another fetch, thereby, I had assumed, making it reevaluate all the entities against the predicate. It is possible that this does nothing, but the docs don't say. Fetching may be a one-time event and once it has results, no more fetching. There is no "unfetch" to throw away the results it has.
Steve Weller
I thought that the fetched results controller was one-time use, but I also can not find any documentation saying that. And, if fact, you can comment out the line where I set it to nil AND the table reloads as expected. So, the answer to your question is YES, you can execute performFetch again and it will re-apply the predicate.
gerry3
I remember now, the fetch request itself can not be changed, so if you need to CHANGE the sorting or predicate, you have to allocate a new fetched results controller.
gerry3