To preface, this is a follow up to an inquiry made a few days ago:
https://stackoverflow.com/questions/2981803/iphone-app-crashes-when-merging-managed-object-contexts
Short Version: EXC_BAD_ACCESS is crashing my app, and zombie-mode revealed the culprit to be my predicate embedded within the fetch request embedded in my Fetched Results Controller. How does an object within an object get released without an explicit command to do so?
Long Version:
Application Structure
Platforms View Controller -> Games View Controller (Predicated upon platform selection) -> Add Game View Controller
When a row gets clicked on the Platforms view, it sets an instance variable in Games View for that platform, then the Games Fetched Results Controller builds a fetch request in the normal way:
- (NSFetchedResultsController *)fetchedResultsController{
if (fetchedResultsController != nil) {
return fetchedResultsController;
}
//build the fetch request for Games
NSFetchRequest *request = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription
entityForName:#"Game"
inManagedObjectContext:context];
[request setEntity:entity];
//predicate
NSPredicate *predicate = [NSPredicate predicateWithFormat:#"platform == %#",
selectedPlatform];
[request setPredicate:predicate];
//sort based on name
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"name"
ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
[request setSortDescriptors:sortDescriptors];
//fetch and build fetched results controller
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc]
initWithFetchRequest:request
managedObjectContext:context
sectionNameKeyPath:nil
cacheName:#"Root"];
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
[sortDescriptor release];
[sortDescriptors release];
[predicate release];
[request release];
[aFetchedResultsController release];
return fetchedResultsController;
}
At the end of this method, the fetchedResultsController's _fetch_request -> _predicate member is set to an NSComparisonPredicate object. All is well in the world.
By the time - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section gets called, the _predicate is now a Zombie, which will eventually crash the application when the table attempts to update itself.
I'm more or less flummoxed. I'm not releasing the fetched results controller or any of it's parts, and the only part getting dealloc'd is the predicate. Any ideas?
EDIT:
As a test, I added this line to the Fetched Results Controller method:
[fetchedResultsController.fetchRequest.predicate retain];
And now it doesn't crash, but that seems like a patch, not something I should be doing.
You shouldn't be releasing your predicate variable. You didn't invoke new, alloc, retain, or copy (This is the "narc" rule) to create the predicate, so you are not responsible for releasing it. That's where your zombie is coming from.
Related
I'm facing a strange issue. I have a method which populates an array with some data (fetchData) (quite a lot actually and it's a bit slow). I'm using the array to build the rows of the table.
I've tried calling fetchData in a number of places in my code, at various times in the construction of the view and I always seem to get the following: a black screen which is shown until the data from the array is loaded. I've called fetchData from the following:
(void)viewDidLoad;
(void)viewWillAppear:(BOOL)animated;
(void)viewDidAppear:(BOOL)animated;
Since I'm using a navigation view controller, having the app appear to hang is pretty bad looking since it gets stuck on a black screen. What I was hoping my code would achieve was displaying an empty table, with a progress indicator until the data is loaded - then refresh. Unfortunately I'm not getting this far since the view isn't being loaded no matter where I call fetchData.
Help appreciated!
P.S. To get around this problem I even tried using a TTTableViewController, but the Loading view is never displayed. Typical. sigh
Your load method must be blocking the UI. You should move it to another thread and let the data load there. You can instantiate the thread in viewDidLoad.
This is a skeleton code for that you need to (using GCD)
dispatch_queue_t downloadQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(downloadQueue, ^{
... Move all the loading part here.
dispatch_async(dispatch_get_main_queue(),^{
... Do all the UI updates, mostly, [tableView reloadData];
})
})
It possible that you could add a timer to delay the call somewhere in your viewDidAppear method. For example:
- (void)viewDidAppear:(BOOL)animated {
[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:#selector(fetchData) userInfo:nil repeats:NO];
}
This will give your app time to load the UI and start your loading screen, then start fetching the data later. You can also try fetching the data in a background thread if you would prefer to go that route
I was having the same issue with a table view not loading initially, but it worked n another .m file I had. Here is the one that worked:
add this to your viewDidLoad:
NSError *error = nil;
if (![[self fetchedResultsController] performFetch:&error]) {
NSLog(#"Unresolved error %#, %#", error, [error userInfo]);
abort();
}
and this to the implementation block:
#pragma mark -
#pragma mark Fetched results controller
- (NSFetchedResultsController *)fetchedResultsController {
if (fetchedResultsController == nil) {
// Create the fetch request for the entity.
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// Edit the entity name as appropriate.
NSEntityDescription *entity = [NSEntityDescription entityForName:#"OMFrackinG" inManagedObjectContext:managedObjectContext];
[fetchRequest setEntity:entity];
// grouping and sorting optional
//NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"country" ascending:YES];
NSSortDescriptor *sortDescriptor1 = [[NSSortDescriptor alloc] initWithKey:#"state" ascending:YES];// was name
NSSortDescriptor *sortDescriptor2 = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor1,sortDescriptor2, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
propriate.
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:managedObjectContext sectionNameKeyPath:#"state" cacheName:nil];//#"state"
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
[aFetchedResultsController release];
[fetchRequest release];
[sortDescriptor1 release];
[sortDescriptor2 release];
[sortDescriptors release];
}
return fetchedResultsController;
}
in my iPhone application I am using simple Core Data Model with two entities (Item and Property):
Item
name
properties
Property
name
value
item
Item has one attribute (name) and one one-to-many-relationship (properties). Its inverse relationship is item. Property has two attributes the according inverse relationship.
Now I want to show my data in table views on two levels. The first one lists all items; when one row is selected, a new UITableViewController is pushed onto my UINavigationController's stack. The new UITableView is supposed to show all properties (i.e. their names) of the selected item.
To achieve this, I use a NSFetchedResultsController stored in an instance variable. On the first level, everything works fine when setting up the NSFetchedResultsController like this:
-(NSFetchedResultsController *) fetchedResultsController {
if (fetchedResultsController) return fetchedResultsController;
// goal: tell the FRC to fetch all item objects.
NSFetchRequest *fetch = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Item" inManagedObjectContext:self.moContext];
[fetch setEntity:entity];
NSSortDescriptor *sort = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:YES];
[fetch setSortDescriptors:[NSArray arrayWithObject:sort]];
[fetch setFetchBatchSize:10];
NSFetchedResultsController *frController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetch managedObjectContext:self.moContext sectionNameKeyPath:nil cacheName:#"cache"];
self.fetchedResultsController = frController;
fetchedResultsController.delegate = self;
[sort release];
[frController release];
[fetch release];
return fetchedResultsController;
}
However, on the second-level UITableView, I seem to do something wrong. I implemented the fetchedresultsController in a similar way:
-(NSFetchedResultsController *) fetchedResultsController {
if (fetchedResultsController) return fetchedResultsController;
// goal: tell the FRC to fetch all property objects that belong to the previously selected item
NSFetchRequest *fetch = [[NSFetchRequest alloc] init];
// fetch all Property entities.
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Property" inManagedObjectContext:self.moContext];
[fetch setEntity:entity];
// limit to those entities that belong to the particular item
NSPredicate *predicate = [NSPredicate predicateWithFormat:[NSString stringWithFormat:#"item.name like '%#'",self.item.name]];
[fetch setPredicate:predicate];
// sort it. Boring.
NSSortDescriptor *sort = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:YES];
[fetch setSortDescriptors:[NSArray arrayWithObject:sort]];
NSError *error = nil;
NSLog(#"%d entities found.",[self.moContext countForFetchRequest:fetch error:&error]);
// logs "3 entities found."; I added those properties before. See below for my saving "problem".
if (error) NSLog("%#",error);
// no error, thus nothing logged.
[fetch setFetchBatchSize:20];
NSFetchedResultsController *frController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetch managedObjectContext:self.moContext sectionNameKeyPath:nil cacheName:#"cache"];
self.fetchedResultsController = frController;
fetchedResultsController.delegate = self;
[sort release];
[frController release];
[fetch release];
return fetchedResultsController;
}
Now it's getting weird. The above NSLog statement returns me the correct number of properties for the selected item. However, the UITableViewDelegate method tells me that there are no properties:
-(NSInteger) tableView:(UITableView *)table numberOfRowsInSection:(NSInteger)section {
id <NSFetchedResultsSectionInfo> sectionInfo = [[self.fetchedResultsController sections] objectAtIndex:section];
NSLog(#"Found %d properties for item \"%#\". Should have found %d.",[sectionInfo numberOfObjects], self.item.name, [self.item.properties count]);
// logs "Found 0 properties for item "item". Should have found 3."
return [sectionInfo numberOfObjects];
}
The same implementation works fine on the first level.
It's getting even weirder. I implemented some kind of UI to add properties. I create a new Property instance via Property *p = [NSEntityDescription insertNewObjectForEntityForName:#"Property" inManagedObjectContext:self.moContext];, set up the relationships and call [self.moContext save:&error]. This seems to work, as error is still nil and the object gets saved (I can see the number of properties when logging the Item instance, see above). However, the delegate methods are not fired. This seems to me due to the possibly messed up fetchRequest(Controller).
Any ideas? Did I mess up the second fetch request? Is this the right way to fetch all entities in a to-many-relationship for a particular instance at all?
You need to actually perform the fetch for the table view controller:
// ...create the fetch results controller...
NSError *fetchRequestError;
BOOL success = [fetchedResultsController performFetch:&fetchRequestError];
I am working with a UITableView that gets its data from an NSFetchedResultsController. I would like to add a UISegmentedControl to my navigation bar that would toggle the table between displaying all of the records and only the records where starred == YES.
I have read some other SO posts indicating that one way to do this is to create a second NSFetchedResultsController that has an NSPredicate with starred == YES, but it seems awfully overkill to create a second NSFetchedResultsController.
Is there a simpler way to do this?
Not according to the docs on NSFetchedResultsController. If you take a look at the documentation on the fetchRequests property, you'll see the following note:
Important: You must not modify the
fetch request. For example, you must
not change its predicate or the sort
orderings.
Since the fetchRequest property is read-only, the only option is creating a new fetched results controller.
It might be possible to change the predicate and have things work, but it's generally a bad idea to do stuff that goes explicitly against things Apple says in the documentation, because it could break in a future release.
And, beware of premature optimization! Unless you've tested it out and found out that creating a whole new fetched results controller is a big performance drain, it's not worth trying to do something in a non-recommended way.
Here's how I set a new predicate on my fetched results controller. fetchedResultsController is a property of my view controller. predicate is a private ivar of the view controller.
I already had all of the code for creating the fetched results controller on demand, so to set the predicate it's just a matter of deleted the cached one.
- (void)setPredicate:(NSPredicate *)newPredicate {
predicate = [newPredicate copy];
// Make sure to delete the cache
// (using the name from when you created the fetched results controller)
[NSFetchedResultsController deleteCacheWithName:#"Root"];
// Delete the old fetched results controller
self.fetchedResultsController = nil;
// TODO: Handle error!
// This will cause the fetched results controller to be created
// with the new predicate
[self.fetchedResultsController performFetch:nil];
[self.tableView reloadData];
}
This code if based on the boilerplate XCode generates when you start a project that uses Core Data.
- (NSFetchedResultsController *)fetchedResultsController {
if (fetchedResultsController != nil) {
return fetchedResultsController;
}
// Create the fetch request for the entity.
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// Edit the entity name as appropriate.
NSEntityDescription *entity = [NSEntityDescription entityForName:[MyEntity entityName] inManagedObjectContext:managedObjectContext];
[fetchRequest setEntity:entity];
[fetchRequest setFetchBatchSize:20];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"name" ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
[fetchRequest setPredicate:predicate];
// nil for section name key path means "no sections".
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:managedObjectContext sectionNameKeyPath:nil cacheName:#"Root"];
aFetchedResultsController.delegate = self;
self.fetchedResultsController = aFetchedResultsController;
[aFetchedResultsController release];
[fetchRequest release];
[sortDescriptor release];
[sortDescriptors release];
return fetchedResultsController;
}
I'm implementing an UITableViewController in conjunction with NSFetchedResultsController.
When the UITableViewController is instantiated the NSFetchedResultsController is constructed (substantially in the same way as CoreDataBooks example) with a different predicate, based on selection the user made on the previous controller.
Sometimes I set the predicate in this way:
predicate = [NSPredicate predicateWithFormat:#"container = %#", aManagedObject];
sometimes in this way:
predicate = [NSPredicate predicateWithFormat:#"container IN (%#)", aNSMutableSetOfObjectsID];
Both predicates works fine because they fetch correctly all my objects as expected.
The problem occur when I try to insert a new managed object into the list when the second predicate is setted. Infact in this case the FRC delegate methods are never called, so the tableview is not automatically updated.
I'm sure the new object is correctly added to the same context used by the FRC, and that its "container" relation is set so that the delegate should be triggered, but it doesn't!
What I have tried:
I say that the second predicate is working fine because if I add a new object and then pop the UITableViewController and push it again, forcing all data to be refetched, the new object appear in the list without problem.
Another test I made is to force the FRC to be recalculated immediatelly after the new object is added:
NSError *error = nil;
if (![[self fetchedResultsController] performFetch:&error]) {
exit(-1);
}
[self.tableView reloadData];
the new object appear correctly.
Code:
- (NSFetchedResultsController *)fetchedResultsController {
if (fetchedResultsController == nil) {
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSManagedObjectContext *managedObjectContext = realEstate.managedObjectContext;
[fetchRequest setEntity:[NSEntityDescription entityForName:#"Item" inManagedObjectContext:managedObjectContext]];
[fetchRequest setPredicate:[self getBasePredicate]];
NSSortDescriptor *nameDescriptor = [[NSSortDescriptor alloc] initWithKey:#"name"
ascending:YES
selector:#selector(localizedCaseInsensitiveCompare:)];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:nameDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
[nameDescriptor release];
[sortDescriptors release];
NSFetchedResultsController *aFetchedResultsController = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest
managedObjectContext:managedObjectContext
sectionNameKeyPath:#"sectionIndex"
cacheName:nil];
self.fetchedResultsController = aFetchedResultsController;
fetchedResultsController.delegate = self;
[aFetchedResultsController release];
[fetchRequest release];
}
return fetchedResultsController;
}
// return a predicate for the fetchedResultsController
- (NSPredicate *)getBasePredicate {
NSPredicate *predicate = nil;
if ([[managedObject valueForKey:#"subcontainersCount"] intValue] == 0)
// container without sons **(FIRST PREDICATE)**
predicate = [NSPredicate predicateWithFormat:#"container = %#", managedObject];
else {
// container with sons **(SECOND PREDICATE)**
predicate = [NSPredicate predicateWithFormat:#"container IN (%#)", [Container allDescendantsObjectID:(Container *)managedObject includeSource:YES]];
}
return predicate;
}
- (void)addNewItem {
Item *newItem = (Item *)[NSEntityDescription insertNewObjectForEntityForName:#"Item"
inManagedObjectContext:realEstate.managedObjectContext];
newItem.name = #"my new item";
newItem.container = aContainerManagedObject;
NSError *error = nil;
if (![realEstate.managedObjectContext save:&error]) {
abort();
}
}
The NSFetchedResultsControllerDelegate methods I'm using are taken from the "More iPhone 3 Development". The complete code can be see here (pages 30-31, 35).
I can post here if needed.
Solved replacing the objects ID with NSManagedObject into the predicate.
Before: [NSPredicate predicateWithFormat:#"container IN (%#)", aNSMutableSetOfObjectsID];
After: [NSPredicate predicateWithFormat:#"container IN (%#)", aNSMutableSetOfNSManagedObject];
For some reasons the FRC delegate is not called if predicate fetch objects using ID instead of exact NSManagedObjects.
I'm testing on iPhoneOS 3.1.3 and 3.2. However iPhoneOS 4.0 should support both objectsID and NSManagedObject and correctly call the FRC delegate methods, but I can't test on 4.0 for now.
I am trying to resolve this for days at this stage and I'm hoping you can help.
I have two ViewControllers which query two different tables from the same database using Core Data. The first ViewController is opened with the app and displays fine. The second is called from within the first ViewController, using a pretty standard fetch setup:
- (NSFetchedResultsController *)fetchedClients {
// Set up the fetched results controller if needed.
if (fetchedClients == nil) {
// Create the fetch request for the entity.
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
// Edit the entity name as appropriate.
NSEntityDescription *entity = [NSEntityDescription entityForName:#"Clients"
inManagedObjectContext:managedObjectContext];
[fetchRequest setEntity:entity];
// Edit the sort key as appropriate.
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:#"clientsName" ascending:YES];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, 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:managedObjectContext
sectionNameKeyPath:nil
cacheName:#"Root"];
aFetchedResultsController.delegate = self;
self.fetchedClients = aFetchedResultsController;
[aFetchedResultsController release];
[fetchRequest release];
[sortDescriptor release];
[sortDescriptors release];
}
return fetchedClients;
}
When I call [self.fetchedClients sections], I get a nil (0x0) return.
I have examined the database using an external application to ensure data exists in the "Clients" table.
Are you compiling against 3.0? Thee is a bug in 3.0 that was fixed in 3.1 that can cause this issue.
update
Are you calling -performFetch: on the returned NSFetchedResultsController? I dont't see it in the code you posted.