15

I have a UITableView populated with a standard NSFetchedResultsController. However I'd like to prepend a row or a section (row preferably but either would works fine really.)

The only way I can possibly see doing this right now is to rewrite all the NSIndexPath's manually when dealing with the section/rows dealing with the data from NSFetchedResultsController to trick it into seeing section at index 0 and starting with row at index 0. This however seems like a really bad idea that would quickly get confusing so I'd like to preferable avoid that.

A good example of this would be in the official Twitter app when you start it up for the first time and I walks you through adding some people from your friends list.

Screenshot of Twitter app's suggestions page, with the relevant sections highlighted

The red section is pretty much what I'd like to achieve, and the yellow section I assume is the results from an NSFetchedResultsController in the same section (though with their custom styling it might be a separate section.)

Philip
  • 769
  • 7
  • 20
  • possible duplicate of [Add extra row to a UITableView managed by NSFetchedResultsController](http://stackoverflow.com/questions/9604410/add-extra-row-to-a-uitableview-managed-by-nsfetchedresultscontroller) – JosephH Aug 08 '12 at 16:18

2 Answers2

33

This is possible to do in a fairly clean way.

I'm assuming you're starting with a standard tableview set up with a standard NSFetchResultsController that uses Apple's sample code.

First you need two utility functions:

- (NSIndexPath *)mapIndexPathFromFetchResultsController:(NSIndexPath *)indexPath
{
    if (indexPath.section == 0)
        indexPath = [NSIndexPath indexPathForRow:indexPath.row+1 inSection:indexPath.section];

    return indexPath;
}

- (NSIndexPath *)mapIndexPathToFetchResultsController:(NSIndexPath *)indexPath
{
    if (indexPath.section == 0)
        indexPath = [NSIndexPath indexPathForRow:indexPath.row-1 inSection:indexPath.section];

    return indexPath;
}

These should be fairly self explanatory - they're just helpers to deal with adding the extra row when we want to use an index path from the fetched results controllers to access the table, or removing it when going the other way.

Then we need to create the extra cell:

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

    if (indexPath.section == 0 && indexPath.row == 0)
    {
        UITableViewCell *cell;
        cell = [[[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:nil] autorelease];
        cell.selectionStyle = UITableViewCellSelectionStyleGray;
        cell.textLabel.text = NSLocalizedString(@"Extra cell text", nil);

        return cell;
    }

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

    [self configureCell:cell atIndexPath:indexPath];

    return cell;
}

make sure we configure it correctly (configurecell will only be called for cells from the fetch results controller):

// the indexPath parameter here is the one for the table; ie. it's offset from the fetched result controller's indexes
- (void)configureCell:(SyncListViewCell *)cell atIndexPath:(NSIndexPath *)indexPath {
    indexPath = [self mapIndexPathToFetchResultsController:indexPath];

    id *obj = [fetchedResultsController objectAtIndexPath:indexPath];
    <... perform normal cell setup ...>
}

and tell the tableview it exists:

- (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];
    }

    if (section == 0)
        numberOfRows++;

    return numberOfRows;
}

and respond to selection:

- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    [tableView deselectRowAtIndexPath:indexPath animated:YES];

    if (indexPath.section == 0 && indexPath.row == 0)
    {
        [self doExtraAction];
        return;
    }

    ... deal with selection for other cells ...

and then remap any updates we get from the results controller:

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

    indexPath = [self mapIndexPathFromFetchResultsController:indexPath];
    newIndexPath = [self mapIndexPathFromFetchResultsController:newIndexPath];

    switch(type) {
        ... handle as normal ...
JosephH
  • 37,173
  • 19
  • 130
  • 154
  • If in this approach, I like to dynamically delete first row will it be possible. – cocoaNoob Aug 16 '14 at 21:50
  • @cocoaNoob Yes, that would work fine. So long as you map the indexPath correctly you can do essentially anything. – JosephH Aug 17 '14 at 16:45
  • Yes, I tried it instead of using 1, I am using instance variable. A very good and clear approach. Thank you for sharing your solution @JosepH. – cocoaNoob Aug 17 '14 at 17:08
  • Is it possible that this approach doesn't work with a fetchedResultsController that uses a transient property as parameter for `sectionNameKeyPath:`? I see my tableView containing 2 rows in section 0 (the first of them being the manually added one) and 4 rows in section 1; if I remove 1 row from section 0 I get this very strange error: *Invalid update: invalid number of rows in section 0. The number of rows contained in an existing section after the update (5) must be equal to the number of rows contained in that section before the update (4), plus or minus the number of rows*... – cdf1982 Aug 13 '15 at 21:13
  • ... *inserted or deleted from that section (0 inserted, 0 deleted) and plus or minus the number of rows moved into or out of that section (0 moved in, 0 moved out).* The fact that the fetchedResultsController thinks that I have 5 objects in section 0 instead of the 1 in section 0 + 4 in section 1, as I see in the tableView, makes me think that this approach doesn't work with sectionNameKeyPath generated sections. Am I right? – cdf1982 Aug 13 '15 at 21:15
  • @cdf1982 I don't see a reason why this code shouldn't work with transient properties for sectionNameKeyPath:. The code is a straightforward deterministic manipulation of the returned data. The two most likely options I can see are: 1) You've not implemented all the sections of my answer. 2) Your code had the same problem before you started trying to prepend rows/sections. I'd recommend stripping out the code in the answer, then test your app. If it works okay, add the code from the answer back in as an exact cut & paste. – JosephH Aug 14 '15 at 08:17
  • how would you do this for swift – Michael McKenna May 27 '16 at 19:29
3

I understand your concerns about complexity, but it is really just adding 1 to numberOfRowsInSection: and adding 1 to indexPath.row in cellForRowAtIndexPath: (beside adding the code for row 0).

Another solution would not have to be very elaborate to become even more cumbersome.

That being said, it really seems that the "heading" you are proposing is a typical candidate for a section header.

Mundi
  • 79,884
  • 17
  • 117
  • 140
  • That's a fair point, I was mostly just checking if there wasn't some method I didn't know about on NSFetchedResultsController that let you prepend or append a set of rows to the results it returns so you can customise them. – Philip Jun 10 '12 at 15:39