No, I swear, my iOS app doesn’t crash on startup! A simple lesson I learned as an enterprise architect about indie iOS game development

Although I’m focusing on enterprise software architecture (a.k.a server side development) by day, I still write Swift-based iOS apps on the side. Though the two software engineering disciplines can be quite different, they sometimes share more in common than I give them credit for.

The other day I submitted a minor update of my Word Wad iOS game to the App Store. The changes were mostly cosmetic: adding back an image that I’d discovered had gone missing, and introducing auto layout in the app’s storyboard. Other than that, I hadn’t made much in the way of code changes.

I tested my changes out on a number of simulators, as well as on my physical iPhone. Everything looked great, so I upped my app’s version number, created a build, validated the build, and ultimately submitted it. Expecting a quick win, I was surprised when, a few days later, I received a rejection from Apple stating, among other things, “Your app crashed when we launch the app”.

Word Wad screenshot
My app, Word Wad, in its non-crashed form.

Feeling a bit foolish for somehow submitting such a buggy app to Apple, I quickly launched Word Wad in a simulator. It ran fine. Figuring that maybe a fresh install would crash, I reset the simulator’s settings and tried again. No crash.

I tried a few different simulators, all with the same result.

Growing a bit concerned, I then connected my iPhone to my MacBook, deleted the previous copy of Word Wad, and launched Word Wad on my phone out of Xcode. Still no problems! I tried my iPad. Same thing.

So far, I couldn’t reproduce the crash. So how was I to fix it? As it turns out, Apple included a crashlog along with the rejection notice. So I opened it up to see how helpful it would be. (Letter Shuffle, you might guess from the crashlog, was Word Wad‘s original name).

{"app_name":"Letter Shuffle","timestamp":"2019-07-31 11:32:24.34 -0700","app_version":"1.3","slice_uuid":"b4a1df49-91b3-3d1d-a64f-a8d6bc44f649","adam_id":1219410678,"build_version":"6","bundleID":"com.taubler.lettershuffle","share_with_app_devs":false,"is_first_party":false,"bug_type":"109","os_version":"iPhone OS 12.4 (16G77)","incident_id":"DAB41DAD-9143-42B0-9115-7AB25B8FC81F","name":"Letter Shuffle"}
Incident Identifier: DAB41DAD-9A48-42B0-9115-7AB25B8FC81F
CrashReporter Key:   2b4341567d2bf6ee1c2ada8d72b26769dd6a74ff
Hardware Model:      xxx
Process:             Letter Shuffle [57833]
Path:                /private/var/containers/Bundle/Application/98F17500-4B54-4D89-8E2A-8B4F4A6B7D163/Letter Shuffle.app/Letter Shuffle
Identifier:          com.taubler.lettershuffle
Version:             6 (1.3)
AppStoreTools:       10G3
Code Type:           ARM-64 (Native)
Role:                Non UI
Parent Process:      launchd [1]
Coalition:           com.taubler.lettershuffle [5459]
Date/Time:           2019-07-31 11:32:24.3585 -0700
Launch Time:         2019-07-31 11:32:24.1648 -0700
OS Version:          iPhone OS 12.4 (16G77)
Baseband Version:    7.80.04
Report Version:      104
Exception Type:  EXC_BREAKPOINT (SIGTRAP)
Exception Codes: 0x0000000000000001, 0x000000010024f19c
Termination Signal: Trace/BPT trap: 5
Termination Reason: Namespace SIGNAL, Code 0x5
Terminating Process: exc handler [57833]
Triggered by Thread:  0
Thread 0 name:  Dispatch queue: com.apple.main-thread
Thread 0 Crashed:
0   Letter Shuffle                0x000000010024f19c 0x100228000 + 160156
1   Letter Shuffle                0x00000001002474e0 0x100228000 + 128224
2   libdispatch.dylib             0x00000001c64cc7d4 0x1c646c000 + 395220
3   libdispatch.dylib             0x00000001c646feb8 0x1c646c000 + 16056
4   libswiftCore.dylib            0x00000001f44b3e40 0x1f420d000 + 2780736
5   Letter Shuffle                0x0000000100245798 0x100228000 + 120728
6   Letter Shuffle                0x000000010024563c 0x100228000 + 120380
7   Letter Shuffle                0x00000001002453f4 0x100228000 + 119796
8   Letter Shuffle                0x0000000100245520 0x100228000 + 120096
9   UIKitCore                     0x00000001f2b23224 0x1f2810000 + 3224100
10  UIKitCore                     0x00000001f2b23628 0x1f2810000 + 3225128

<snip>

Binary Images:
0x100228000 - 0x10026bfff Letter Shuffle arm64  <a4b1de4970b3301da64458d6bc22f513> /var/containers/Bundle/Application/96F17500-2B52-4D89-8E2A-B4F4A6B7D76E/Letter Shuffle.app/Letter Shuffle
0x100688000 - 0x1006dffff dyld arm64  <06f3d9add3a233cea57df42b73686817> /usr/lib/dyld
0x100748000 - 0x100753fff libobjc-trampolines.dylib arm64  <065bd8006d513c358dc14e2a8ff1ba31> /usr/lib/libobjc-trampolines.dylib
0x1c5bf6000 - 0x1c5bf7fff libSystem.B.dylib arm64  <8a05d5f48f0a376abe6bd1caf4fc8138> /usr/lib/libSystem.B.dylib

<snip>

Completely unsymbolicated, and nearly useless as-is. So I went online to figure out how to make the logs more useful. I found this Apple tech note which discusses a command-line tool called atos. atos  helps symbolicate crashlogs, via one long terminal command.

So I opened up a text editor and got to work in crafting the command. The tech note provides the details, but based on my crashlog, I created the following:

atos -arch arm64 -o /Users/dave/Library/Developer/Xcode/Archives/2019-07-31/Letter\ Shuffle\ 7-31-19\,\ 10.32\ PM.xcarchive/dSYMs/Letter\ Shuffle.app.dSYM/Contents/Resources/DWARF/Letter\ Shuffle -l 0x100228000 0x000000010024f19c

To figure out the path to the executable, I went to Xcode and opened the Organizer (Window > Organizer). Under the Archives tab, I control-clicked the troublesome build and selected Show in Finder. Then I control-clicked the .xarchive file that appeared in the Finder and selected Open With > Terminal. A that point, simply entering the pwd command in the terminal got me the path to the file; I needed to add the escape characters myself, unfortunately.

Running the above atos command yielded this:

GameRound.init(number:letters:phrase:numRows:numCols:mixMoves:) (in Letter Shuffle) (<compiler-generated>:0)

Okay. So I had the offending function (GameRound.init) but not the exact location within that function. At that point, it was a matter of looking through the code and guessing where the problem could be. One aspect struck me in particular: an array of items is passed to the init method, along with a number of rows and columns used to turn the array into a matrix. However unlikely, it was conceivable that the array length wouldn’t match the number of rows times the number of columns, which would result in an array out of bounds exception. Figuring that could be the problem, I added some defensive code, and throughly tested my changes in a number of simulators and devices. Encountering no errors, I created a new build and submitted it.

Apple reviewed the build a few days later. And they encountered the exact same thing:“Your app crashed when we launch the app”.

sigh

At this point, I was flying blind. I had limited insight as to where the problem was occurring, with no insight as to what the problem actually was. Worse, try as I might, I was unable to reproduce the problem myself. I was worried that I’d have to use Apple’s review process to test out my blind attempts at solving the problem. Which, clearly, was not a sustainable approach.

The it occurred to me: rather than plugging my iPhone into my laptop and then launching an executable from Xcode, maybe I ought to run the build itself that I’d submitted to Apple on my device. So I did, via the following steps:

  1. Open up Xcode’s Organizer, as described above
  2. Under the Archives tab, select the troublesome build and click the Distribute App button
  3. Choose Ad Hoc as the option
  4. Follow the remaining prompts, ultimately saving the archive file locally
  5. Plug the iPhone into the laptop, then open the Xcode’s devices window (Window > Devices and Simulators)
  6. Select the iPhone in the left-hand pane.
  7. Drag the .ipa file from the Finder into the INSTALLED APPS section in the right-hand pane.

Doing that installed the specific build on my iPhone. I launched it, and… bam! The app crashed.

Woo hoo! I could now get my app to crash reliably.

So the problem seemed to be not with the code itself, but with the compilation process.

Now that I could get my app to crash on my own device, the next step was to look at the newly-generated crashlogs. Although I was able to access the new crashlog directly from my phone, it didn’t provide me with any more information as to the precise cause of the crash. However, I would be able to take some guesses at how to fix the problem, and test those fixes out locally before submitting the new builds to the App Store.

The crash still seemed to be occurring around the GameRound.init method, so I went back through that code. It turns out I was performing some superfluous operations that I really didn’t need to be doing, so I removed that code, streamlining it a bit. Then I created a new build, and distributed it locally as a new Ad Hoc archive. I installed the archive on my phone again, launched it, and… it crashed again.

But looking at the new crashlog, a different class—NullGameRound—was referenced at the top of the call stack. NullGameRound is an implementation of the “null object” design pattern, extending my app’s GameRound class and effectively overriding certain functions with no-op implementations.

Was there something about this pattern that the compiler didn’t like?

Although it’s a handy pattern, I was easily able to remove the use of NullGameRound in my game. So I did that, created and installed a new build on my phone, and ran it.

And it worked.

The moral of this story isn’t to avoid using the null object pattern in your Swift projects (although… really? Why should that have caused a problem?) It’s that sometimes, the Swift compiler will introduce problems into your distribution builds that won’t be present in your test builds.

For me, Swift and iOS development is something I do for fun, after hours. As I mentioned earlier, I work on enterprise services by day. With the advent of container technologies (e.g. Docker and Kubernetes) with which to deploy our backend services, it’s become a no-brainer that we test the same exact executable that will ultimately be deployed to production.

In hindsight it seems obvious, but the same should be true of mobile development, even for part-time indie developers like me. In retrospect, I was probably lucky that the compiler introduced a full-blown crash scenario that the Apple review team ran into, rather than a less apparent but more insidious bug that would’ve passed App Store approval, only to plague my app’s users later down the line.

I’d since submitted the fixed build to Apple, and the reviewed and approved it. But if I’d run the same build that I’d planned to submit to Apple before actually submitting it, I could’ve saved a lot of time and stress, and not inadvertently treated Apple’s reviewers as beta-testers of my app.

Profiles and certs and devices… oh hell!

Has it really been more than two years since I’ve written a post? It’s time to start writing again!

I thought that I would kick things off with something that’s cropped up again and again over those past 2+ years. And that is dealing with provisioning profiles when testing and deploying iOS apps. There may seasoned, full-time iOS engineers out there that have all of this down-pat. But for me, someone who doesn’t focus all of his attention on iOS anymore, this process can be perpetually confusing. I’ve tended to learn just enough to get things working, and when things would later mysteriously break, I’d flail around for awhile until things started working again.

If you’re like me, then read on. It’s time to figure this stuff out once and for all.

Provisioning Profiles

The first thing you learn is that everything centers around Provisioning Profiles. Provisioning profiles are really an aggregation of other elements that together determine whether an app can be built and run on a device. There are two types of provisioning profiles: Development and Distribution. As their names suggest, Development profiles are used to test ad hoc builds as you’re developing you app; Distribution profiles are used to distribute apps to the App Store.

We’ll focus on Development profiles here. Development profiles aggregate three things:

  1. Your App ID; for example, com.mycompany.mymobileapp. Each profile can contain only one app ID.
  2. A list of devices; specifically, device UUIDs. To install a build onto a device for ad hoc testing, that device’s UUID must be present in the development profile.
  3. A list of developers certificates that can be used to sign the profile.

App IDs

As mentioned above, each profile can contain only one app ID, so if you have multiple app IDs, you’ll need a separate profile for each. For example, my current company uses two app IDs: one for use when hitting our staging/test server, and one for our production server. So we need two separate development profiles.
Well, that’s not entirely true. You can get away with using wildcards if your app does not use certain features such as Apple Push Notifications. For example, if you have a simple app that uses the App ID com.mycompany.mymobileapp.testing when hitting a testing server, and com.mycompany.prod when hitting a production server, you can create a single Development profile with the App ID of
com.mycompany.*

Devices

To help enforce the “walled garden” App Store distribution model, development builds can only be installed on a limited number of devices. Apple allows each developer account up to 100 devices–identified by their UUIDs–to be registered. These UUIDs are stored in your development profile, ensuring that the profile can only be used on one of those devices.

Certificates

In going back over some earlier notes I had made, I saw that I had stated that profiles contain a list of developers who are allowed to sign the profile. This is not entirely true. Generally, each developer will have his or her own certificate–associated with his or her own private key–installed in his/her own OS X Keychain, for use in signing iOS apps. But this is not technically required. An organization could, for example, have a single certificate/private-key combination that every developer in the organization imports into his/her Keychain. This can make life easier, assuming of course that no developer ever leaves the organization.

The sum of the parts

Looking at development profiles as an aggregation of those three parts helps to make sense of why Apple established its requirements. A given iOS app (identified by the App ID), is allowed to be installed on a limited set of devices for testing purposes, and only certain developers identified by their certificates should be allowed to build that app.

What do you need to do?

So how does these concepts translate into practice? Well, let’s assume that you’ve set up your iOS developer account, but aside from that, you’re starting from scratch.
First, you’ll visit https://developer.apple.com and after logging in, make your way to the Identifiers section in your account. Create an App ID for you app. App IDs generally take the reverse-domain-name form, and incorporate the name of the product. For example, yours might look like com.yourcompany.yourproduct  As mentioned above, if you need to use more than one App ID for your product, you can either create multiple App IDs, or else create one using a wildcard (e.g. com.yourcompany.*)
Next, you’ll go to the Certificates section. Follow the instructions to create a new certificate, ensuring at the end that you download the certificate to you Mac. For starters, you’ll want to create a certificate of type iOS App Development. Once finished, double-click the certificate to install it, along with the associated private key, into your Keychain.
Then go to the Devices section, and enter the UUID of any devices on which you’ll want to test your app. There are plenty of resources online, such as this one, to help you find your devices’ UUIDs. Be sure to enter the UUIDs correctly; once entered, you won’t be able to remove any UUIDs for a year.
Finally, you’ll go to the Provisioning Profiles section. Create one of type Development / iOS App Development. When prompted, choose the App ID that you’d just created, select your new certificate, and the UUIDs of any devices on which you want to be able to test.
When finished, be sure to download the profile and double-click it to install it in your copy of Xcode. Alternatively, you can tell Xcode download and install the profile. How to do this seems to change from release to release. To do this in the current release of Xcode 5, you need to open the Preferences window, go to Accounts, enter your account info, then highlight your account name, click View Details, the finally click the refresh button at the bottom-left of the dialog.

When things change

Typically you’ll get this far, and after a bit of stumbling and tweaking things, you’ll get stuff working. But things will change. Here are some common scenarios you’ll need to deal with:

You get a new tester in your organization

This means that you’ll need to add your tester’s UUID to your account. Visit your developer account online, go to the Devices section as you did above, and enter the new UUID(s). Next, you’ll need to revisit the Provisioning Profiles section and edit your profile(s). Add the new devices to the profile. Then either re-download the profile and double-click it to re-import it into Xcode, or just tell Xcode to grab it and re-import it for you (as described above, using the Preferences window).

You get a new developer in your organization

That developer will need to have a certificate/private-key pair installed on their system that can be used to sign your app. As mentioned above, you have two options. The new developer can either create his/her own certificate, and s/he can use an existing one.
For the latter, the developer will have to access your organization’s developer account (you did give them access, right?) and visit the Certificates section to create his/her certificate. S/he will need to download the certificate and install it into his/her Keychain. The provisioning profile(s) will then need to be edited in the Provisioning Profiles section so that the new certificate is added. The developer–and ideally all of the developers in your organization–should then install the certificate into their Xcode instances, either by downloading and double-clicking the certificate, or by refreshing it from within Xcode.

What about Distribution Provisioning Profiles?

Distribution profiles are similar to development profiles, except that they don’t define the devices that are allowed to install the app. This makes sense, since presumably more than 100 pre-defined people will be using your app! Other than that, the concept is the same; your distribution profile must be tied to an app ID, and it must define the certificates that can be used to sign it.

That’s all for now

This article was intended to help shed light on the concepts associated with building iOS apps, rather than providing practical steps. But hopefully it will provide that little bit of of extra insight that will help you keep control over your iOS development process.

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.