Editable UITableViewController

UITableView

A common use case in iOS is creating a form or an editable list of records directly inside a UITableView. We could send the user to a separate “detail” view controller for editing, but that complicates and slows down the user experience. In this post, I’ll show you how to create your own subclass of UITableViewController that can support any number of UITextFields inside your table cells and use them to update any backing data source. This code can then be reused in any project that needs an editable table view!

Editing text fields directly in a UITableView is challenging for a few reasons:

  • A table can have many cells and each cell can have many text fields. We need to map changes in a specific text field back to our data source.
  • UITableView is optimized to reuse UITableViewCell objects. As soon as you make a change to a UITextField‘s contents, it will be lost when you scroll off the screen unless saved immediately.

First create a new Objective-C class in Xcode called EditableTableViewController and make it a subclass of UITableViewController. Next, in the .h file, make this class implement UITextFieldDelegate, which is the delegate protocol for communicating with UITextFields. All of its methods are optional; we’ll only be using two.

First, when a user taps inside a UITextField, we call a method on the base controller’s UITableView that scrolls the selected cell to the middle of the screen.

- (void)textFieldDidBeginEditing:(UITextField*)textField
{
  UITableViewCell* cell = [self parentCellFor:textField];
  if (cell)
  {
    NSIndexPath* indexPath = [self.tableView indexPathForCell:cell];
    [self.tableView scrollToRowAtIndexPath:indexPath atScrollPosition:UITableViewScrollPositionMiddle animated:YES];
  }
}

Next, when a user ‘leaves’ a UITextField, we send the indexPath and UITextField pointer to a textFieldDidEndEditing method we will create shortly. The subclass that will receive that message is responsible for updating its data source accordingly. Finally, we tell the UITableView to reload the cell so that it renders just as the table developer intended.

- (void)textFieldDidEndEditing:(UITextField*)textField
{
  UITableViewCell* cell = [self parentCellFor:textField];
  if (cell)
  {
    NSIndexPath* indexPath = [self.tableView indexPathForCell:cell];
    [self textFieldDidEndEditing:textField inRowAtIndexPath:indexPath];
    [self.tableView reloadRowsAtIndexPaths:@[ indexPath ] withRowAnimation:UITableViewRowAnimationNone];
  }
}

In both methods we call a private method on self called parentCellFor: that returns the parent cell for our UITextField. Couldn’t we simply get the text field’s parent cell by calling this?

textField.superview.superview

After all, doesn’t the view hierarchy for a UIView in a custom UITableViewCell looks like this?

UITableViewCell > contentView > UIView

Yes, but what if  the text field happens to be nested inside another UIView?

UITableViewCell > contentView > UIView > textField

We need a more flexible solution that can handle any number of nested UIViews. Let’s try using recursion:

- (UITableViewCell*)parentCellFor:(UIView*)view
{
  if (!view)
    return nil;
  if ([view isKindOfClass:[UITableViewCell class]])
    return (UITableViewCell*)view;
  return [self parentCellFor:view.superview];
}

As with any recursive call, we need a base case that terminates the call chain, and that is when our UIView input parameter is nil. Next, we perform the meat of our method and actually check if the input UIView is a subclass of UITableViewCell. If so, our work is done! Return the view after casting it as a UITableViewCell object. If not, we keep digging by calling parentCellFor: on our UIView’s superview. The process continues until we either return nil or our parent UITableViewCell!

Now, in the .m file, add the following instance method stub:

- (void)textFieldDidEndEditing:(UITextField*)textField inRowAtIndexPath:(NSIndexPath*)indexPath;
{
}

This is the method that subclasses will override in order to capture the event. This form of event handling is known as the Template Method Pattern or simply “subclass and override.” It is used everywhere in OOP, and you use it all the time when you override init or viewDidLoad, for example.

Sample Usage

Now let’s use our new code! In our example, we’ll be creating a table view with cells that contain 2 text fields.

First create a simple class to serve as our data model. In Xcode, create a new class called Person that inherits from NSObject and has two string properties:

@interface Person : NSObject

@property (strong, nonatomic) NSString* firstName;
@property (strong, nonatomic) NSString* lastName;

@end

Now create a new class called TableViewController that inherits from our new EditableViewController class:

@interface TableViewController : EditableTableViewController

Create an NSArray to serve as our underlying data source. Make this a ‘private’ property by putting it into an anonymous category in the .m file:

@interface TableViewController ()

@property (strong, nonatomic) NSArray* people;

@end

Initialize the array with some test data by overriding the designated initializer, initWithStyle. Use the new literal syntax for brevity. Bonus points if you recognize the names!

- (id)initWithStyle:(UITableViewStyle)style
{
  self = [super initWithStyle:style];
  if (self)
  {
    Person* person1 = [[Person alloc] init];
    person1.firstName = "Erich";
    person1.lastName = "Gamma";

    Person* person2 = [[Person alloc] init];
    person2.firstName = "Richard";
    person2.lastName = "Helm";

    Person* person3 = [[Person alloc] init];
    person3.firstName = "Ralph";
    person3.lastName = "Johnson";

    self.people = @[ person1, person2, person3 ];
  }
  return self;
}

Implement the relevant UITableViewDataSource delegate methods:

- (NSInteger)numberOfSectionsInTableView:(UITableView*)tableView
{
  return 1;
}

- (NSInteger)tableView:(UITableView*)tableView numberOfRowsInSection:(NSInteger)section
{
  return self.people.count;
}

- (UITableViewCell*)tableView:(UITableView*)tableView cellForRowAtIndexPath:(NSIndexPath*)indexPath
{
  UITextField* firstNameField;
  UITextField* lastNameField;

  UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"CellIdentifier"];
  if (cell == nil)
  {
    //Create a new cell
    cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"CellIdentifier"];

    //Create a new first name field
    CGRect firstNameFrame = CGRectMake(0, 0, 150, 44);
    firstNameField = [[UITextField alloc] initWithFrame:firstNameFrame];
    firstNameField.delegate = self;
    firstNameField.tag = 1;
    [cell.contentView addSubview:firstNameField];

    //Create a new last name field
    CGRect lastNameFrame = CGRectMake(150, 0, 150, 44);
    lastNameField = [[UITextField alloc] initWithFrame:lastNameFrame];
    lastNameField.delegate = self;
    lastNameField.tag = 2;
    [cell.contentView addSubview:lastNameField;
  }
  else
  {
    //Reuse the old text fields
    firstNameField = (UITextField*)[cell.contentView viewWithTag:1];
    lastNameField = (UITextField*)[cell.contentView viewWithTag:2];
  }

  //Update the text field values for this cell
  Person* person = self.people[indexPath.row];
  firstNameField.text = person.firstName;
  lastNameField.text = person.lastName;

  return cell;
}

A couple of things to point out here. First, we only create the UITextField objects when dequeueReusableCellWithIdentifier: returns nil. Otherwise we would keep creating text fields that would overlap the old ones. However, we do update the actual text field values every time. Also, we assign each text field a unique tag property so that we can identify it later when updating the data source. Here is our override of the textFieldDidEndEditing: method we created earlier!

- (void)textFieldDidEndEditing:(UITextField*)textField inRowAtIndexPath:(NSIndexPath*)indexPath
{
  CQPerson* person = self.people[indexPath.row];
  switch (textField.tag)
  {
    case 1:
      person.firstName = textField.text;
      break;
    case 2:
      person.lastName = textField.text;
      break;
  }
}

That was easy! We simply use the tag property to identify what text field was updated and the indexPath to identify the cell it came from. Then we update our data source, and our EditableTableViewController takes care of the rest!

Hope this was helpful to you! Download the source file for this post.

About these ads

About Martin Rybak

I am a New York area software developer and MBA with 10+ years of server-side experience on the Microsoft stack. I've also been a native iOS developer since before the days of ARC. I architect and develop full-stack web applications, iOS apps, database systems, and backend services.

4 responses to “Editable UITableViewController

  1. Zav

    Real nice, but how do you accommodate text character changes in a field? I’m changing the color of the textField’s view border based on the validity of the input as the user types or clears data. How are you getting the start and stop editing notifications and not the UITextFieldTextDidChangeNotification notification?

    • Good point Zav. For this simple solution to work, you must set MRTableViewController as the delegate for your text fields. That means you don’t have access to the UITextFieldDelegate methods. You would have to use NSNotificationCenter instead (UIKit is not guaranteed to be KVO compliant). In the future, I’ll update my code to pass through delegate events.

  2. Great code! If you change the following line in parentCellFor:

    if ([view isMemberOfClass:[UITableViewCell class]])

    into

    if ([view isKindOfClass:[UITableViewCell class]])

    it will also work with custom UITableViewCell’s.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

Join 30 other followers

%d bloggers like this: