NSFetchedResultsController numberOfObjects returns additional (duplicate) when run on device
I have implemented a UITableView with the NSFetchedResultsController to load data from an sqllite database. I downloaded the database from the device to the simulator, so it is the same for both.
I observe the strange behavior that, when run on the simulator, the tableview is populated with the correct number of cells (in the initial base-case: one cell); however when I run the same code on the device, numberOfObjects returns 2, and the tableview displays two (identical/repeated) cells.
When I inspect the sqllite file there is indeed just 1 object/row in it...
I followed the example code for my implementation and am not doing anything special.
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
NSInteger count = [[fetchedResultsController sections] count];
if (count == 0) {
count = 1;
}
return count;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
NSInteger numberOfRows = 0;
if ([[fetchedResultsController sections] count] > 0) {
id <NSFetchedResultsSectionInfo> sectionInfo = [[fetchedResultsController sections] objectAtIndex:section];
numberOfRows = [sectionInfo numberOfObjects];
}
NSLog(@"SwoopListTableViewController::numberOfRowsInSection - numberOfRows:%d", numberOfRows);
return numberOfRows;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
// Dequeue or if necessary create a SwoopTableViewCell, then set its Swoop to the Swoop for the current row.
static NSString *SwoopCellIdentifier = @"SwoopCellIdentifier";
SwoopTableViewCell *SwoopCell = (SwoopTableViewCell *)[tableView dequeueReusableCellWithIdentifier:SwoopCellIdentifier];
if (SwoopCell == nil) {
SwoopCell = [[[SwoopTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:SwoopCellIdentifier] autorelease];
SwoopCell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
}
[self configureCell:SwoopCell atIndexPath:indexPath];
return SwoopCell;
}
and
- (NSFetchedResultsController *)fetchedResultsController {
// Set up the fetched results controller if needed.
NSLog(@"SwoopListTableViewController::fetchedResultsController - started");
if (fetchedResultsController == nil) {
if (managedObjectContext == nil)
{
managedObjectContext = [(SwoopAppDelegate *)[[UIApplication sharedApplication] delegate] managedObjectContext];
NSLog(@"SwoopListTableViewController::fetchedResultsController - set managedObjectContext");
}
// Create the fetch request for the entity.
NSFetchRequest *fetchRequest = [[NSFetchRequest alloc] init];
NSEntityDescription *entity = [NSEntityDescription entityForName:@"Swoop" inManagedObjectContext:managedObjectContext];
[fetchRequest setEntity:entity];
NSSortDescriptor *sortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"creationDate" ascending:NO];
NSArray *sortDescriptors = [[NSArray alloc] initWithObjects:sortDescriptor, nil];
[fetchRequest setSortDescriptors:sortDescriptors];
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 puzzled why the same code & same database would exhibit different behavior on the simulator (works as expected) vs. the device (3GS - duplicate table cells displayed). Can anyone help / explain / lend some insight as to what I should be looking at?
Many thanks, Eric
** EDIT 1: ** I have done a bit more debugging into NSCoreData and NSFetchedResultsController. It seems that the fetchRequest is indeed returning a duplicate objects from the managedContext. Here is a the code and the corresponding console output:
Controller Code:
- (void)viewDidLoad {
NSError *error = nil;
if (![[self fetchedResultsController] performFetch:&error]) {
NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
abort();
}
id <NSFetchedResultsSectionInfo> sectionInfo = [[fetchedResultsController sections] objectAtIndex:0];
NSLog(@"SwoopListTableViewController::viewDidLoad - sectionInfo objects:%@", [sectionInfo objects] );
}
Console Output:
2011-05-14 17:54:53.388 Swoop[1471:307] SwoopListTableViewController::viewDidLoad - sectionInfo objects:(
"<Swoop: 0x187d60> (entity: Swoop; id: 0x186480 <x-coredata://A9FF2CC0-77EE-4EFF-A3A6-5F085AA9CCAC/Swoop/p2> ; da开发者_Python百科ta: <fault>)",
"<Swoop: 0x187d60> (entity: Swoop; id: 0x186480 <x-coredata://A9FF2CC0-77EE-4EFF-A3A6-5F085AA9CCAC/Swoop/p2> ; data: <fault>)",
"<Swoop: 0x188180> (entity: Swoop; id: 0x143a60 <x-coredata://A9FF2CC0-77EE-4EFF-A3A6-5F085AA9CCAC/Swoop/p1> ; data: <fault>)"
)
I find it strange that the first two objects in sectionInfo both have the same memory address "0x187d60" and both have the same x-coredata 'path': "//A9FF2CC0-77EE-4EFF-A3A6-5F085AA9CCAC/Swoop/p2"... can someone explain what this means, or what might be going on?
Thanks, Eric
After more reading and digging around with NSCoreData, it seems the issue is due to a peculiarity of how memory is managed on the device (I would have assumed the same memory constraints/management would be applied to the simulator, but I guess there are differences between the two) -- specifically, if a view is unloaded on a low-memory warning, a fetch will be performed when the view is once again reloaded, which results in possible duplicate entries.
I found that the problem/solution described at the following link solves the problem I was experiencing:
Duplicate NSManagedObject with NSFetchedResultsController
On another note, it was also helpful to learn about being able to enable different levels of NSCoreData logging by passing this argument to the application:
-com.apple.CoreData.SQLDebug 1
Read more here:
From the Apple Developer library, see "Troubleshooting Core Data: Debugging Fetching "
To add an argument via xcode, see "Supplying Launch Arguments for Command-Line Programs" on this page http://www.meandmark.com/xcodetips.html
I've been busy on (SQLite3-)databases as well. Have you tried cleaning the device (using Product -> Clean, while iOS Device selected to run on)? When building and running, if there is a database existing, it won't get renewed... So, you might be using an old database!
A second thing programmers tend to forget, is that the database in your bundle isn't editable! So first, you have to copy your database from your bundle (the list of the items on the left in Xcode) to a path on your device/simulator.
NSError *error;
NSString *defaultDBPath = [[[NSBundle mainBundle] resourcePath] stringByAppendingPathComponent:@"database.sqlite3"];
NSString *writableDBPath = [docsDir stringByAppendingPathComponent:@"fdatabase.sqlite3"];
NSFileManager *filemgr = [NSFileManager defaultManager];
BOOL succes = [filemgr fileExistsAtPath:writableDBPath];
if (!succes)
{
//writable database does not exist, so copy default nonwritable from bundle
succes = [filemgr copyItemAtPath:defaultDBPath toPath:writableDBPath error:&error];
//NSLog(@"Database file copied from bundle to %@", dbPath);
if (!succes)
NSLog(@"Failed to create writable database file with message '%@'.", [error localizedDescription]);
//return NO;
}
databasePath = [[NSString alloc] initWithFormat:@"%@", writableDBPath];
I created two paths here. The first is your defaultPath
. This is the one you should have included in your project. The one included in your project isn't editable, so I created the writableDBPath
. The rest of the code is explained in comments, so it should be self-explaining, but write me back if it isn't.
精彩评论