views:

139

answers:

4

I have a situation here and losing my hair...

Very simple application driven by core data which has table view controller with add navigation button. It is very similar to Recipes sample, with couple differences

  • There is only single Entity in model with 2 attributes
  • The cell in main table is not customized, using default textLabel

The core data part is fine, since the new entry is added to underlying storage and retrieved using fetched results controller. The problem comes when I add new item which will be placed on the top of the list. This can be for instance new item on empty list or item with sort order taking it to the top. The item which is placed on top is not visible! The cell is there, with nil text label, however, I clearly saw the fetched results controller issued update notification and I configured cell, updating with new text.

The only way I can make that cell to be shown is scroll data table to invoke cellForRowAtIndexPath, then the item text is updated. I add another item which will be placed on top, again the new item text is not visible. I add some item which will be residing on bottom - no problem, the text in cell is visible. Also initially when I just start application there is no problem, all items are visible.

Not sure anyone can help to solve the mistery without the source code, but still hope to get some hints on how to debug... I tried to compare the delegate invocations with Receipe application and all is same.

UPD:

- (void)configureCell:(UITableViewCell *)cell 
        atIndexPath:(NSIndexPath *)indexPath 
{
    Word* word = (Word*)[fetchedResultsController objectAtIndexPath:indexPath];
    cell.textLabel.text = word.word;
    printf("configureCell: %d : \"%s\"\n", indexPath.row,
           [word.word cStringUsingEncoding:NSASCIIStringEncoding]);
}

- (UITableViewCell *)tableView:(UITableView *)tableView
                     cellForRowAtIndexPath:(NSIndexPath *)indexPath 
{
    static NSString *CellIdentifier = @"Cell";

    UITableViewCell *cell = [tableView
                      dequeueReusableCellWithIdentifier:CellIdentifier];
    if (cell == nil) 
    {
        cell = [[[UITableViewCell alloc]
                              initWithStyle:UITableViewCellStyleDefault
                              reuseIdentifier:CellIdentifier] autorelease];
    }

    [self configureCell:cell atIndexPath:indexPath];

    return cell;
}

- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller 
{
[self.tableView beginUpdates];
}

- (void)controllerDidChangeContent:(NSFetchedResultsController *)controller 
{
[self.tableView endUpdates];
}

- (void)controller:(NSFetchedResultsController *)controller didChangeObject:(id)anObject atIndexPath:(NSIndexPath *)indexPath forChangeType:(NSFetchedResultsChangeType)type newIndexPath:(NSIndexPath *)newIndexPath {
UITableView *tableView = self.tableView;

switch(type) {
          case NSFetchedResultsChangeInsert:
        printf("didChangeObjectAtRow: %d: insert\n", newIndexPath.row);
        [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
        break;

    case NSFetchedResultsChangeDelete:
        printf("didChangeObjectAtRow: %d : delete\n", indexPath.row);
        [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
        break;

    case NSFetchedResultsChangeUpdate:
        printf("didChangeObjectAtRow: %d : update\n", indexPath.row);
        [self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
        break;

    case NSFetchedResultsChangeMove:
        printf("didChangeObjectAtRow: %d / %d : move\n", indexPath.row, newIndexPath.row);
        [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
        [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
        break;
}
}

- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView 
{
        NSInteger count = [[fetchedResultsController sections] count];

        if (count == 0) 
        {
            count = 1;
        }
printf("number of sections: %d\n", count);
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];
}

printf("number of rows in sections: %d is %d\n", section, numberOfRows);
return numberOfRows;
}

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

// This is autoreleased as the name implies
NSEntityDescription* entity = [NSEntityDescription entityForName:@"Word" inManagedObjectContext:managedObjectContext];

NSFetchRequest* fetchRequest = [[NSFetchRequest alloc] init];
[fetchRequest setEntity:entity];

NSSortDescriptor* sortByWordDescriptor = [[NSSortDescriptor alloc] initWithKey:@"word" ascending:YES];
NSArray* sortArray = [[NSArray alloc] initWithObjects:sortByWordDescriptor, nil];
[fetchRequest setSortDescriptors:sortArray];


NSFetchedResultsController* controller = [[NSFetchedResultsController alloc] initWithFetchRequest:fetchRequest managedObjectContext:managedObjectContext sectionNameKeyPath:nil cacheName:@"Hmm"];
controller.delegate = self;
self.fetchedResultsController = controller;

[fetchRequest release];
[sortByWordDescriptor release];
[sortArray release];
[controller release];

return fetchedResultsController;
}

- (void) add:(id)sender
{
WordzWordEditView *wordEditViewController = [[WordzWordEditView alloc] initWithNibName:@"WordzWordEditView" bundle:nil];
wordEditViewController.delegate = self;

Word* word = [NSEntityDescription insertNewObjectForEntityForName:@"Word" inManagedObjectContext:self.managedObjectContext];
wordEditViewController.word = word;

UINavigationController *editWordNavigationController = [[UINavigationController alloc] initWithRootViewController:wordEditViewController];

[self presentModalViewController:editWordNavigationController animated:YES];

[wordEditViewController release];
[editWordNavigationController release];

}

UPD: output

controllerWillChangeContent
didChangeObjectAtRow: 0: insert
controllerDidChangeContent
number of sections: 1
number of sections: 1
number of rows in sections: 0 is 2
cellForRowAtIndexPath: 0
configureCell: 0 : "(null)"
post configuring: "(null)"
controllerWillChangeContent
configureCell: 0 : "asd"
didChangeObjectAtRow: 0 : update with 'asd'
controllerDidChangeContent
number of sections: 1
number of sections: 1
number of rows in sections: 0 is 2
A: 

Without the source code it is difficult to pinpoint, when you are adding the data are you calling

– insertRowsAtIndexPaths:withRowAnimation:

If you call [self.tableview reloadData] after you add it, does it appear

Liam
Yes, the whole `didChangeObject` for fetchedResultsController delegate is copied from Recipe sample. In fact I mentioned that the row is added, just the textLabel is `nil` so I can see an empty row at the top. Again, once I scroll table the value of cell is appearing.The insert operation engaged when I first create an empty object in context and when user is saving input data, update is called for that cell. So the cell inserted into table with empty string value. If I reloadData from update the cell is appearing, but it's not solution since this is expensive operation and not like in sample
Michael
The call to reloadData is correct. It is not as expensive as you think as core data caches data. It is unlikely that the disk is being accessed to refetch your objects.
falconcreek
Also if I hardcode my cell with some value instead of nil, then I the item appears well. So seems like `nil` is confusing the cell somehow...
Michael
@falconcreek: in apple CoreDataReceipes sample code reloadData is not called, I need to know why I should call.
Michael
Do you have the 'configureCell' method as well? When/if it is called does it return the correct data? Do you have a custom cell? Is it being called correctly? You could call 'reloadRowsAtIndexPaths:withRowAnimation:' also
Liam
@Liam: it is done exactly same way as in sample code, except that I'm not subclassing the cell. Updated with code.
Michael
The sample uses [tableView beginUpdates] and [tableView endUpdates]. Are you? This will cause the tableview to ask it's datasource for the section and cell details again. Same as reloadData.
falconcreek
@ falconcreek: Yes I do, like I mentioned all delegates are same as in sample. Note that the item is not shown only for indexPath.row `0`. I don't have sections.
Michael
You will have to post more code then. Otherwise we are all at risk of losing hair.
falconcreek
@falconcreek: pretty much all the code that might be responsible from my point of view. If you need more routines let me know I'll update. Don't want you guys to lose your hair :)
Michael
do you have the output also?
Liam
@Liam: yep, updated. The output is exactly same as from Apple sample. On empty table I've just added 1 item. It added but did not updated the text label.
Michael
I can't see where the problem is, last few guesses ... I presume you are also getting `[didChangeObjectAtRow: 0 : insert]` logged? Are the `beginUpdates` and `endUpdates` both being called?
Liam
Try setting a breakpoint on `- (void)controllerWillChangeContent:(NSFetchedResultsController *)controller` to ensure that `beginUdates` is being called
falconcreek
@Liam, insert is there.
Michael
@falconcreek: both are called, see the updated output log.
Michael
A: 

Please show the code on how you are adding the cell as that is quite relevant.

Marcus S. Zarra
Marcus S. Zarra: updated. Thanks!
Michael
A: 

You are casting to the wrong type. (copy/paste error) You are using UITableViewCell in tableView:cellForRowatIndexPath

case NSFetchedResultsChangeUpdate:
    printf("didChangeObjectAtRow: %d : update\n", indexPath.row);
    [self configureCell:(RecipeTableViewCell *)[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
    break;

Replace:

[self configureCell:(RecipeTableViewCell *)[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];

With:

[self configureCell:[tableView cellForRowAtIndexPath:indexPath] atIndexPath:indexPath];
falconcreek
Not really, wrong copy/paste to here, in the code it's correct. Sorry, updated my original post.
Michael
A: 

Some observations:

This line in the action method:

Word* word = [NSEntityDescription insertNewObjectForEntityForName:@"Word" inManagedObjectContext:self.managedObjectContext];

...triggers:

case NSFetchedResultsChangeInsert:
    printf("didChangeObjectAtRow: %d: insert\n", newIndexPath.row);
    [tableView insertRowsAtIndexPaths:[NSArray arrayWithObject:newIndexPath] withRowAnimation:UITableViewRowAnimationFade];
    break;

... before the dialog completes and before the Word managedObject has a value for its word attribute. This creates the null populated cell.

(I logged the methods in the Recipie app and got:

2010-07-07 18:56:08.848 Recipes[53880:207] NSFetchedResultsChangeInsert:=<NSIndexPath 0x11286a0> 2 indexes [0, 0]
2010-07-07 18:56:22.872 Recipes[53880:207] NSFetchedResultsChangeUpdate=<NSIndexPath 0x11286a0> 2 indexes [0, 0]

... in sequence.)

Interestingly enough, you don't log a NSFetchedResultsChangeInsert. Which makes me think something is missing. Possibly in your logging.

I think the problem might actually be caused by using a modal controller used to change the the Word.word value. The modal presentation prevents the table from responding to the NSFetchedResultsChangeUpdate when it occurs so it keeps the initial null value until you scroll which causes it to reload the cell from the fetched results controller. Just a guess.

You could test by inserting a Word object and then changing the word property in code without the modal view. Perhaps use a timer to give a little lag. If that works, then its definitely the modal view causing the problem.

If not I think you should take care to log the entire index path object to make sure you don't have some section wackiness going on.

TechZen