What Happens When You Add a Table Cell to a UITableView?

Seasons change. People change. And user interfaces change, usually dynamically.

One dynamic change often seen in iOS apps is the addition or removal of a table cell to a UITableView. Often, you’ll see this when switching a UITableView from read-only to edit mode (for a dramatic example, try editing a contact in the Contacts app.) I’ve done this a few times in some of my iOS apps. I’ve found a few little tutorials out there that describe generally how to do this, but most contain some information gaps. Here, I’ll try to add some of the insight I gained.

I’ll use code examples from my most recent such app. In this app, I have a “Task Details” UITableView in which the user can view the details of a task (think: todo list item). They can also tap an Edit button to edit those details.

Tasks can contain, among other things, a detailed description. Whenever you’re editing a task, a large-ish UITableViewCell containing a UITextView is present, allowing the user to enter or modify a description. However, when in read-only mode, I wanted to remove the UITableViewCell that displays the task’s description if the task in fact has no description (otherwise, it’s wasted space).

The top part of the table view when we’re just looking at the Task. Note that the “Go Shopping” task has no description.

 

Tap the edit button, and the description cell appears. In the real app, of course, it’s animated.

One other important detail for our discussion is that tasks also have simple names. When in read-only mode, of course, I use a standard UITableViewCell (of style UITableViewCellStyleDefault) to display the task’s name. When in edit mode, however, I want to replace that default cell with a custom one containing a UITextField, in which the user can edit the task’s name.

The first thing I did was to create a method called isTaskDescriptionRow:

 

– (BOOL)isTaskDescriptionRow:(int) row {
    if (tableView.editing) {
        return row == TASK_BASICS_DESC_ROW;
    } else {
        if (taskDetailsCellIsShowing) {
            return row == TASK_BASICS_DESC_ROW;
        } else {
            return NO;
        }
    }
}
taskDetailsCellIsShowing is a BOOL that should be self-explanatory. It gets set during the setEditing: animated: call, as shown a little ways below.
Next, I added code like the following to tableView: cellForRowAtIndexPath:
 
if (row == TASK_BASICS_NAME_ROW) {
            if (tableView.editing) {
                // create and return a table view cell containing
                // a UITextField for editing the task’s name
            } else {
            // create and return a default table view cell
                // displaying the task’s name
            }

 

} else if ([self isTaskDescriptionRow:row]) {
            UITableViewCell *cell = [[UITableViewCell alloc]
                        initWithStyle: UITableViewCellStyleDefault 
                        reuseIdentifier: nil];
            …
            [descTextView release];
            descTextView = [[UITextView alloc]
                        initWithFrame:CGRectMake(0, 0, w, h)];
            // set descTextView to be non-editable and non-opaque
            descTextView.text = task.description;
            descTextView.userInteractionEnabled = !self.editing;
            [cell.contentView addSubview: descTextView];
            return cell;
} else…
Delegating to isTaskDescriptionRow: makes it easy to determine whether the current NSIndexPath should display the current task’s description. isTaskDescriptionRow: will return YES for the appropriate NSIndexPath if the UITableView is currently in edit mode, or if the current task has a description. If so, then I create a UITextView, configure it, and add it to the current table cell.
As I’d mentioned, when the current task has no description, I want the description row to be added when the table view is entering editing mode, and removed when reverting to read-only mode. This is done in the setEditing: animated: method:
– (void)setEditing:(BOOL)editing animated:(BOOL)animated {
        [super setEditing:editing animated:YES];
        [tableView setEditing:editing animated:YES];
    
        if (editing) {
                if (![self taskHasDescription]) {
                    [self addDescriptionRow];
                } else {
                     [tableView reloadData];
                }
        } else {
               if (![self taskHasDescription]) {
                    [self removeDescriptionRow];
                } else {
                    [tableView reloadData];
                 }
        }
    
        [self.navigationItem setHidesBackButton:editing animated:YES];
        …
}
After invoke setEditing: animated: on the super class as well as the UITableView itself, I check to see if in fact we’re entering editing mode (e.g. if editing = YES). If so, and if the current task does not have a description (i.e. if ![self taskHasDescription]), I invoke my addDescriptionRow method which I will show below. If we’re leaving editing mode, and the current task does not have a description, then I invoke removeDescriptionRow, also shown below:
-(void)addDescriptionRow {
    [tableView beginUpdates];
    taskDetailsCellIsShowing = YES;
    NSIndexPath *idxPath = 
        [NSIndexPath indexPathForRow:TASK_BASICS_DESC_ROW
                        inSection:TASK_BASICS_SECTION];
    NSArray *idxPaths = [NSArray arrayWithObject:idxPath];
    [tableView insertRowsAtIndexPaths:idxPaths
        withRowAnimation:UITableViewRowAnimationFade];
    [tableView endUpdates];
}
 
-(void)removeDescriptionRow {
    [tableView beginUpdates];
    taskDetailsCellIsShowing = NO;
    NSIndexPath *idxPath = 
        [NSIndexPath indexPathForRow:TASK_BASICS_DESC_ROW
                        inSection:TASK_BASICS_SECTION];
    NSArray *idxPaths = [NSArray arrayWithObject:idxPath];
    [tableView deleteRowsAtIndexPaths:idxPaths
        withRowAnimation:UITableViewRowAnimationFade];
    [tableView endUpdates];
}

 

The basic approach in the add and remove methods shown above is to create an array of indexPaths representing the cell(s) to be added or removed. You then pass that array to either insertRowsAtIndexPaths: withRowAnimation: or deleteRowsAtIndexPaths: withRowAnimation:. Finally, Apple’s documentation encourages insertions/deletions to be wrapped within beginUpdates and endUpdates calls, so I do that as well.
That all worked, for the most part; the description cell appeared as desired. But I noticed a bit of a wrinkle. The first tablecell–the one which displays the task’s name–is supposed to transform into an editable UITextField when the Edit button is tapped. But that wasn’t happening.
You can see in the code blocks above that this should be done in tableView: cellForRowAtIndexPath:. When the indexPath represents the task name row (section == TASK_BASICS_SECTION, row == TASK_BASICS_NAME_ROW), I then simply check the value of tableView.editing. If YES, I create a UITextField and add it to the UITableViewCell; if NO, I simply return a default UITableViewCell. Therefore, all I need to do is to add a call to tableView.reloadData in the setEditing: animated: method, right?
Wrong. When I did that, I received the following error:
Terminating app due to uncaught exception ‘NSInternalInconsistencyException’, reason: ‘Invalid update: invalid number of rows in section 0.  The number of rows contained in an existing section after the update (3) must be equal to the number of rows contained in that section before the update (3), plus or minus the number of rows inserted or deleted from that section (1 inserted, 0 deleted).’
Hmm… time to look at my tableView: numberOfRowsInSection: method :
– (NSInteger)tableView:(UITableView *)tv numberOfRowsInSection:(NSInteger)section {
    if (section == TASK_BASICS_SECTION) {
        return [self numTaskDetailsBasicSectionTableRows];
    } else …
    }
}
Which delegates to this method:
– (int)numTaskDetailsBasicSectionTableRows {
    if (tableView.isEditing) {
        return NUM_TASK_DETAILS_BASIC_TABLE_ROWS;
    } else {
        if (taskDetailsCellIsShowing) {
            return NUM_TASK_DETAILS_BASIC_TABLE_ROWS;
        } else {
            return NUM_TASK_DETAILS_BASIC_TABLE_ROWS – 1;
        }
    }
}

 

NUM_TASK_DETAILS_BASIC_TABLE_ROWS represents the number of rows in that top section when the description cell is being shown (i.e. 3). At the point where I call tableView.reloadData, however, it appears that the description cell hasn’t actually been added yet, at least as far as UIKit was concerned.
This led to my primary question: what exactly is UIKit doing when it adds a table view cell? Clearly, it needs to obtain information about the cell being added. This is especially apparent in this case, since the new cell is an atypical, custom cell, both in terms of its content and its height. Yet because the task name cell wasn’t being updated, it didn’t appear to be invoking tableView: cellForRowAtIndexPath: at any point while adding the new cell. So I set a breakpoint at the top of tableView: cellForRowAtIndexPath: to test my theory.
It turns out, UIKit was calling tableView: cellForRowAtIndexPath:. But only once. Specifically, it was calling it for the indexPath I provided in the addDescriptionRow method; i.e. the indexPath of the new description cell.
Pretty clever, actually.
But in my case, it was preventing that task-name cell from being updated. Thus, I still needed to call tableView.reloadData, but I needed to do it after the table cell addition was complete. At this point, I started scouring the documentation, as well as various online discussion forums. What I was looking for was some way to register a callback to be invoked when the table cell was completely added. Unfortunately, I couldn’t find any.
I played around a bit with reordering certain calls and and performing some calls asynchronously. I was able to get the functionality to work, but the animation wound up being far from smooth–quite jarring, in fact. I was also able to get the functionality to work without animation at all, but I really wanted the animation effect. I finally settled on the following:
– (void)setEditing:(BOOL)editing animated:(BOOL)animated {
    … all the other stuff
    if (![self taskHasDescription]) {
        [self performSelector:@selector(reloadNameCell:) 
                   withObject:[NSNumber numberWithInt:0]
                   afterDelay:0.33];
    }
}
 
– (void)reloadNameCell:(NSNumber *)count {
    @try {
        NSIndexPath *ip = [NSIndexPath
                indexPathForRow:TASK_BASICS_NAME_ROW
                        inSection:TASK_BASICS_SECTION];
        [tableView reloadRowsAtIndexPaths:
                [NSArray arrayWithObject:ip]
                withRowAnimation:UITableViewRowAnimationNone];
    }
    @catch (NSException *exception) {
        int i = [count intValue];
        NSLog(@”Exception (%d) in reloading name cell, %@”, 
                        i, [exception description]);
        if (i < 5) {
            [self performSelector:@selector(reloadNameCell:) 
                       withObject:[NSNumber numberWithInt:i + 1]
                       afterDelay:0.125];
        } else {
            NSLog(@”Too many retries; aborting”);
        }
    }
}
If that looks like a hack, that’s because it is. Basically, we wait for 0.33 seconds, which seems to be just enough time for the call-adding animation to complete. We then invoke reloadNameCell: which at its core simply reloads the UITableViewCell that corresponds with the task name. We are also prepared to catch the abovementioned NSInternalInconsistencyException if we didn’t quite wait long enough. If we do catch that exception, we wait again for a short period of time and then try again. We track the number of retries and abort after 5 attempts; at that point, something really went wrong.
So there you have it. Hopefully that provides a little bit of insight as to what goes on under the hood while your UITableView’s structure is smoothly changing.

jQuery Mobile, AJAX, and the DOM

I’ve been messing around with jQuery and jQuery Mobile lately, and ran into an issue that’s worth exploring. It might be a newbie issue–I’m sure seasoned JQM developers don’t fall into this trap much–but I figure that I might be able to save new developers some frustration.

Let me first describe the app I was working on. The app basically provides online utilities that I and other engineers might find useful from time to time in day to day work. The utilities are grouped into different categories (for example, Date Conversion tools, Data Encryption tools, etc). While I started out developing these tools for desktop browsers, I figured I also ought to port them to a mobile web app as well. While the desktop tools are all displayed on one single page, however, the mobile app would present each category of tools (e.g. Date Conversion) on its own page. The “homepage” would present a simple listview, allowing the user to select a category of tools to use.

The homepage is contained in its own HTML page (named, unsurprisingly, index.html). Each tool category also has its own page; for example, the Date Conversion tools are located on dc.html. It was with this page that I ran into problems. The primary date conversion tool is one that takes milliseconds and converts them to a formatted date, and vice-versa. Given this, I’d decided to allow users to choose the date format they want to work with. To implement this, I used a multi-page template in dc.html  The first page div consisted of the tool itself; the second div consisted of a group of radio buttons from which the user could select a date format.

Stripped of irrelevant code, dc.html looks like this:

<!DOCTYPE html>
<html>
<head>

function addDfChoices() {
        // This method populates the radio buttons. Its existence becomes important later
}
$(document).ready(function() {
        …
        addDfChoices();
        …
});

</head>
<body>


<!– Start of main page –>
<div data-role=”page” id=”main”>

  <div data-role=”content” id=”main”>
    <span class=”section_content”>
      <label style=”text-align:baseline”>Millis since 1970</label>
      <a href=”#page_dateformat” style=”width:100px;float:right”>Format</a>
      
    </span>
  </div><!– /main content –>
</div><!– /main page –>


<div data-role=”page” id=”page_dateformat”>
  …
  <div data-role=”content”>
    <div data-role=”fieldcontain” id=”df_option_container”>
      <fieldset data-role=”controlgroup” id=”df_options”>
      </fieldset>
    </div>
  </div><!– /dateformat content –>
</div><!– /dateformat page –>


</body>
</html>

I loaded dc.html into my browser (I did a lot of testing in Chrome, but also of course on mobile browsers such as mobile Safari on my iPhone). The page worked as expected. On page load, the main page div was displayed, while a small Javascript instantiated different date formats and inserted them into the df_options fieldset located within the hidden page_dateformat page div. Tapping the Format link invoked a slide animation which loaded the radio buttons. Additional Javascript code (not shown above) registered any radio button selections and accordingly modified the date format used in the main page.
Except… sometimes things didn’t work. Specifically, sometimes tapping on the Format link took me to the homepage instead.
Playing around a bit, I learned that if I manually loaded dc.html into my browser, things worked fine. If I started on index.html and then linked to dc.html, and then tapped the Format link, that’s when things went wrong.
This is the relevant code within index.html:
<ul data-role=”listview” data-inset=”true” data-theme=”d”>
  <li><a href=”./dc.html”>Date Conversion</a></li>
  <li><a href=”./de.html”>Data Encoding</a></li>
</ul>
Nothing too special there.
Well, I decided to use Chrome’s DOM inspector to get a look at what was happening. I loaded dc.html and looked in the DOM inspector, and indeed I saw the two major page divs, main and page_dateformat. Then I loaded index.html and tapped on the Date Conversion link, and then took a look again at the DOM inspector after the date conversion tools page loaded. There was the main page div… but where was the page_dateformat div?
Here’s what jQuery was doing. When dc.html itself was loaded, the entire contents of the HTML file were loaded into the DOM. Since I was using a multi-page template, any div declaring data-role=”page” besides the first one (in my case, the page_dateformat div) was hidden… but it was still there in the DOM. The DOM looked roughly like this:
html
  head/
  body
    div data-role=”page” id=”main”/
    div data-role=”page” id=”page_dateformat” display=”hidden”/
  /body
/html
Now, when I started by loading index.html and then tapping the Date Conversion link, dc.html was retrieved but only the first data-role=”page” div was loaded into the DOM. Here, roughly, was the DOM at this point:
html
  head/
  body

    div data-role=”page”/  

            ^– this was the content div for index.html; it’s hidden at this point

    div data-role=”page” id=”main”/  
            ^– this is the first data-role=”page” div in dc.html, loaded via AJAX then inserted into the DOM
  /body
/html
Thus, when I tapped on the Format link (which, if you recall is marked up as <a href=”#page_dateformat”>Format</a>), the browser searched the DOM for a block with an ID of page_dateformat, couldn’t find one, and effectively gave up and reloaded the current page… which at this point was still index.html
At this point I reviewed the jQuery Mobile docs. The following little blurb provides a bit of a clue:

It’s important to note if you are linking from a mobile page that was loaded via Ajax to a page that contains multiple internal pages, you need to add a rel="external" or data-ajax="false" to the link. This tells the framework to do a full page reload to clear out the Ajax hash in the URL. This is critical because Ajax pages use the hash (#) to track the Ajax history, while multiple internal pages use the hash to indicate internal pages so there will be conflicts in the hash between these two modes.

I added data-ajax=”false” to the links on index.html  This solved the problem; dc.html was subsequently loaded completely into the browser’s DOM, allowing the page_dateformat div to be found. However, declaring data-ajax=”false” causes links to lose their animation effects. The pages are simply loaded the old-fashioned way.
I wasn’t too keen about that, so I removed the data-ajax=”false” declarations. I then changed the approach I took to the date format selection. Instead of a new “page” with radio buttons, I simply presented the user with a popup select list. For various reasons, I kept the dfChoices() Javascript function depicted above; the method was now invoked on page load to populate the select with date format options.
And immediately, I encountered the same success rate: when I started on dc.html, things worked great. When I started on index.html and linked to dc.html, the dfChoices() function was never invoked. And of course, the reason was the same as well. Much as the page_dateformat div hadn’t been loaded into the DOM, the dfChoices() function similarly was nowhere to be found.
At this point, I added data-ajax=”false”  back into the homepage’s links; for that matter, I also went back to using the multi-page template with radio buttons. There are certainly other solutions (separating out the radio buttons into their own html page and using some form of local storage to share state? putting Javascript functions into data-role=”page” divs instead of in the page header?) worth exploring, which I’ll do at some point. But just understanding the general issue is a good starting point.