Swiping to delete rows from a table

Following on from the previous post on creating a table view with a “Load More…” option I wanted to further enhance my feed reader app with a couple of extra features. I will add a new property to the model to allow a post to be marked as read and modify the view controller to change the user presentation for read and unread posts. Also I will look at a common table operation - swiping to delete a row from the table.

Marking Posts as Read

One of the nice things about the MVC architecture is that it makes easy to see how to add new features to an application. To allow my pretend feed reader to mark posts as read when the user has viewed them I know that I need to add a read flag to my model and then update the view controller to make use of that flag.

Modifying the Post model class is trivial. Our new class looks like this:

@interface Post : NSObject {
    BOOL isRead;
    NSString *postTitle;
    NSString *postDescr;
    NSDate *pubDate;
}

The boolean flag will default to false when we create a new post object indicating an unread post. In our user interface we will show the post title in bold when it is unread and in a normal font when it is read. The place to make that change is in the tableView:cellForRowAtIndexPath: method in FeedViewController.m. I will not show the whole method again (check yesterdays post). The relevant lines that set the post title are as follows:

Post *currentPost = [posts objectAtIndex:row];
cell.textLabel.text = [currentPost postTitle];
cell.textLabel.font = [UIFont systemFontOfSize:14];

Given the post object it is easy to check the isRead flag and set the font weight accordingly:

if (currentPost.isRead) {
  cell.textLabel.font = [UIFont systemFontOfSize:14];
} else {
  cell.textLabel.font = [UIFont boldSystemFontOfSize:14];
}

Now when we push the PostViewController onto the navigation stack in the tableView:didSelectRowAtIndexPath: to view a post we can also update the model object for the post to mark it as read:

- (void)tableView:(UITableView *)tableView 
                   didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
  
  NSUInteger row = [indexPath row];
  NSUInteger count = [posts count];
  
  if (row == count) {
    ...		
  } else {

    Post *currentPost = [posts objectAtIndex:row];
    currentPost.isRead = YES;

    PostViewController *postController = [[PostViewController alloc] 
                                           initWithNibName:@"PostView" 
                                           bundle:nil];
    postController.post = currentPost;
    [[self navigationController] pushViewController:postController 
                                 animated:YES];
    [postController release];
  }
}

To ensure that when we return from the detailed post view we update the table to show the new post status we also need to implement the viewWillAppear method to allow us to reload the table:

- (void)viewWillAppear:(BOOL)animated {
  [self.tableView reloadData];
}

One other nice feature that we can quickly add is to update the view title to show the total number of unread posts. First a method to count the number of unread posts:

- (NSUInteger)countUnread {
  NSUInteger count = 0;
  for (Post *post in self.posts) {
    if (post.isRead == NO) {
      count++;
    }
  }
  return count;
}

Since we will be performing the title update in several places I will also add a method to set the view title:

- (void)updateViewTitle {
  NSUInteger count = [self countUnreadPosts];
  
  if (count) {
    self.title = [NSString stringWithFormat:@"Posts (%u unread)", count];
  } else {
    self.title = @"Posts";
  }
}

Then we need to update the viewDidLoad method to set the initial view title and viewWillAppear to refresh the title each time we read a post:

- (void)viewDidLoad {
  ...	
  [self updateViewTitle];
}

- (void)viewWillAppear:(BOOL)animated {
  [self updateViewTitle];
  [self.tableView reloadData];
}

We also need to update the view when the user touches the Load More… row. We only need to update the title when new rows are actually added:

- (void)tableView:(UITableView *)tableView 
                   didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
 
  NSUInteger row = [indexPath row];
  NSUInteger count = [posts count];
 
  if (row == count) {
 
    NSArray *newPosts = [feed newPosts];
    NSUInteger newCount = [newPosts count];
 
    if (newCount) {
      ...
      ...
      [self updateViewTitle];
    }
 
  } else {
    ...
  }
}

Deleting Rows from the Table

One other common feature of iPhone table views is to allow the user to delete a row with a swipe gesture. In this case we will use the swipe gesture to remove the post from the view. A better approach might be to actually just mark the post as read and provide the user with the ability to choose between viewing all posts or only unread posts. For now we will stick with just removing the post from the feed to illustrate the interaction with the table view.

To support the delete action for a UITableView we need to implement three methods. The first method determines for which rows the delete gesture is enabled by setting the editing style. We want to allow all rows to be deleted except the last one (which has our Load More… message):

- (UITableViewCellEditingStyle)tableView:(UITableView *)tableView 
       editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath {
  
  NSUInteger row = [indexPath row];
  NSUInteger count = [posts count];
  
  if (row < count) {
    return UITableViewCellEditingStyleDelete;
  } else {
    return UITableViewCellEditingStyleNone;
  }
}

So for alls rows except the last row we return UITableViewCellEditingStyleDelete to allow the swipe to delete gesture. When a user swipes a row and taps the delete button we get a call to a method to allow the data source to update which we can use to remove the post from the array holding all of the posts:

- (void)tableView:(UITableView *)tableView 
        commitEditingStyle:(UITableViewCellEditingStyle)editingStyle
        forRowAtIndexPath:(NSIndexPath *)indexPath {

  NSUInteger row = [indexPath row];
  NSUInteger count = [posts count];

  if (row < count) {
    [posts removeObjectAtIndex:row];
  }
}

Finally we can use a call to another tableView delegate method that indicates the editing of the table has ended to force a reload and update the view. As well as updating the table we can also ensure that the view title is updated:

- (void)tableView:(UITableView *)tableView 
                   didEndEditingRowAtIndexPath:(NSIndexPath *)indexPath {
 
  [self updateViewTitle];
  [tableView reloadData];
}

Wrapping Up

I guess that will do for today. I am still experimenting with developing an example app post by post so I am not sure where this will lead but I hope it proves useful to somebody. The next area that I really want to get into is actually retrieving an RSS feed over the network and parsing it to produce some real post data.