开发者

Keeping a placeholder cell in a UITableView

开发者 https://www.devze.com 2023-02-02 05:50 出处:网络
I have a UITableView that I never wa开发者_运维百科nt to fall below 1 cell: It\'s a directory readout, and if there\'s no files in the directory, it has a single cell that says \"No Files\". (In edit

I have a UITableView that I never wa开发者_运维百科nt to fall below 1 cell: It's a directory readout, and if there's no files in the directory, it has a single cell that says "No Files". (In edit mode, there's a bonus cell to Create a File, so edit mode never falls below two cells.)

It's probably just lack of sleep keeping me from thinking my way out of a paper bag right now, but I keep tripping up on errors like this:

*** Terminating app due to uncaught exception 
'NSInternalInconsistencyException', reason: 'Invalid update: 
invalid number of sections.  The number of sections contained 
in the table view after the update (2) must be equal to 
the number of sections contained in the table view before 
the update (2), plus or minus the number of sections inserted 
or deleted (1 inserted, 0 deleted).'

This is happening as I'm preemptively adding the "No Files" placeholder before deleting the last file. There's a like crash if I delete the last file cell before adding the placeholder. Either way, the cell count gets out of sync with the return from numberOfRowsInSection, and that triggers a crash.

Surely there's a design pattern for this situation. Clue me in?


Do something along the lines shown in the code snippet below:

  • first delete the data for the row from the array
  • if array items has not dropped to zero then delete row from table
  • if array items has dropped to zero then reload table - note: your code should now provide 1 for number of rows and configure the cell for row 0 to show "No Files" when the tableview delegate methods are called.
 
- (void)tableView:(UITableView *)tableView commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath {

    if (editingStyle == UITableViewCellEditingStyleDelete) {
        // First delete the row from the data source
        [self deleteTableData: indexPath.row];  // method that deletes data from 
        if ([self.tableDataArray count] != 0) {
            [tableView deleteRowsAtIndexPaths:[NSArray arrayWithObject:indexPath] withRowAnimation:UITableViewRowAnimationFade];
        } else {
            [self.tableView reloadData];
        }

    }   
}


I've got another solution to this (it seems like there are many ways to do this). I found the other solutions available didn't suit my needs, so I came up with this.

This method is very simple, and good for those of you that want to use two (or more) placeholder cells, don't want to mess around with your data model or multiple table views, and particularly - need to allow cells to be deleted and inserted in your table. It also allows for a very nice in-and-out transition for showing and hiding the placeholder.

Sounds great? Good! Let's look at the basic gist of it:

The real issue with most of the placeholder cell solutions out there is that they will typically fail when you get around to allowing editing - deletions and insertions - on your table. That, or you have to start messing around with the code that handles editing, which can make everything more confusing. The problem here typically arises with returning inconsistent values in the numberOfRowsInSectionmethod. The tableview usually takes issue with say, deleting a cell in a table that has one cell left, and still having one cell left after deletion (or vice-versa with inserting).

The simple solution? We always have a consistent number of cells - the number of entries in our data source, plus one for the placeholder. The placeholder cell is always there, and just shows or hides its content based on whether it's technically supposed to be there.

Despite the long write-up, implementing this is actually very simple. Let's get started:

1. Setup your placeholder cell prototypes: This is fairly straightforward. Set up the prototype cell(s) in your storyboard for your placeholder(s). In my case I'm using two: one to display "Loading..." while the table is getting its data from a server, and one to display "Tap + above to add an item" for when there actually is nothing in the table.

Setup your cells visually however you like (I just used the Subtitle cell style, and put my placeholder text in the subtitle label. Don't forget to delete the other label's text if you do this). Make sure to assign a reuse-identifier, and set the selection style to 'None'.

2. Setup the numberOfRowsInSection delegate method: This method will now do two basic things: First is to return the number of rows in the data source (plus one for our placeholder), and second is to show/hide our placeholder's text as necessary. This is a good place to initiate this, as this method is called every time a cell is deleted or inserted (twice actually; once before editing and once after). To avoid running our animations every time the method is called, we'll use a BOOL placeholderIsHiddento keep track of our placeholder's current status. We'll also perform our switch after a short delay, to allow for the cell editing animation to get started. Add this code to your class:

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    int count = [self.dataSource count];

    // Hide/Show placeholder cell
    if (count == 0) {   // Placeholder should be shown
        if (self.placeholderIsHidden) {
            [self performSelector:@selector(animatePlaceholderCellChangeForIndexPath:) withObject:[NSIndexPath indexPathForRow:count inSection:0] afterDelay:0.1];
        }
    }
    else {   // Placeholder should be hidden
        if (!self.placeholderIsHidden) {
            [self performSelector:@selector(animatePlaceholderCellChangeForIndexPath:) withObject:[NSIndexPath indexPathForRow:count inSection:0] afterDelay:0.1];
        }
    }

    return count + 1;
}

That's good to go! Now let's add our animatePlaceholderCellChange method:

- (void)animatePlaceholderCellChangeForIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];

    [UIView beginAnimations:nil context:NULL];
    [UIView setAnimationDuration:0.8];
    [UIView setAnimationDelegate:self];
    [UIView setAnimationBeginsFromCurrentState:YES];

    if (indexPath.row == 0) {
        cell.detailTextLabel.hidden = NO;
        self.placeholderIsHidden = NO;
    }
    else {
        cell.detailTextLabel.hidden = YES;
        self.placeholderIsHidden = YES;
    }

    [UIView commitAnimations];
}

This method takes advantage of an animation block, to smooth over the transition between showing and hiding the placeholder (and make it fit well with the cell editing animation).

Second to last, we need to setup our cellForRowAtIndexPath method to return the correct cells.

3. Setup cellForRowAtIndexPath: This is fairly simple. Place this code into your method, after your prototype cell identifier declarations, and before you do your normal cell setup:

// Add a placeholder cell while waiting on table data
int nodeCount = [self.dataSource count];

if (indexPath.row == nodeCount) {
     // Place the appropriate type of placeholder cell in the first row
     if (self.isLoading) {
         return [tableView dequeueReusableCellWithIdentifier:LoadingCellIdentifier];
     }
     else {
         return [tableView dequeueReusableCellWithIdentifier:PlaceholderCellIdentifier];
     }
 }

Finally, let's setup our BOOL properties for tracking whether the data is loading, and whether the placeholder is hidden or not.

4. Setup isLoading and placeholderIsHidden:

First, add the two declarations to your class's .h file:

@property (assign, nonatomic) BOOL isLoading;
@property (assign, nonatomic) BOOL placeholderIsHidden;

Now, setup their initial values in your viewDidLoad method:

self.isLoading = YES;
self.placeholderIsHidden = NO;

That's it for the placeholderIsHidden property! As for the isLoading property, you'll want to set it to YES anywhere your code starts loading data from your server, and NO wherever that operation completes. In my case it was fairly simple as I'm using an operation with callbacks for when the loading operation is complete.

That's it! Run your code, and see the neat, smoothly animated, and editing-safe placeholder cell!

Hope this helps someone!

EDIT: One more important thing! It's important to note that there is one more thing you should do with your code, so as to avoid any nasty uncalled for crashes: go through your code, and anywhere you access elements from your data source (typically using objectAtIndex:), make sure you never try and pull an element that doesn't exist.

This could be an issue in a few rare cases, where for instance you have code that accesses the elements in the data source for all the visible rows on screen (since the placeholder could be visible, you may have an indexPath that doesn't relate to any element in your data source). (In my case this was a particular bit of code that started image downloads on all visible rows, once the table view is done scrolling). Solving this is pretty easy: anywhere you access elements from your data source, put an if-statement around the code like this:

int count = [self.dataSource count];

if (indexPath.row != count) {

   // Code that accesses the element
   // ie:
   MyDataItem *dataItem = [self.dataSource objectAtIndex:indexPath.row];
}

Good luck!


The absolute easiest way to accomplish this is to actually have two UITableViews

e.g.

UITableView * mainTable; 
UITableView * placeholderTable;

Then you do stuff in your delegate like

tableView:(UITableView*)tableview cellForRowAtIndexPath:(NSIndexPath*)indexPath {
  if (tableview == placeholderTable){
    // code for rendering placeholder cell
  }
  else {
    // code for rendering actual table content
  }
}

Whenever you insert/remove entities or initialize the view, check for the empty condition and hide/unhide your tables:

// something changed the number of cells
    if (tableIsEmpty){
      mainTable.hidden = YES;
      placeholderTable.hidden = NO;
    }
    else {
      mainTable.hidden = NO;
      placeholderTable.hidden = YES;
    }

Doing it this way will save you A TON of grief; especially when working with Core Data NSFetchedResultsController and table view animations.

0

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号