The most annoying bug - Or Why Fast Lists 2.0 is late

TL;DR - Warning, long rambling description of the winding path of development of version 1.79

Trying to be bug free for iOS 5 and 6 users was harder than expected caused by a mixture of bad luck, Apple's bugs and my own mistakes. I felt a need to write it, you shouldn't feel the need to read it!

Background

Fast Lists was originally released support iOS 5 and later so that I could take advantage of the latest features when I initially developed it. None of the new iOS 6 features were adopted but when iOS 7 was announced some of the features were things that I really wanted to use, particularly the new design which I wanted to properly support along with Dynamic Text and the ability to change the size of the text with the system settings. This version will be Fast Lists 2.0

While the Apple App Store knows about the compatibility of updates with older versions and won't let users update to a version not built for that platform there is no way for any further updates to be made (without it being an update to the users of the latest version too).

After version 1.60 I was receiving two forms of feedback about issues:

  1. Crash reports including a stacktrace when the app actually crashes (these are received automatically from users who have not disabled them "Usage reporting" in the Settings.
  2. In the event of some error conditions that I have foreseen as possible (although unlikely) Fast Lists asks the user to send me some information by email. The email is largely pre-composed with some information about the error but also gives the opportunity to provide additional information AND importantly allows me to respond to the email and ask the user further questions about what happened and potentially offer assistance at times. For serious errors normally where there is risk of data corruption Fast Lists will quite (via calling abort) rather than risk continuing in an inconsistent state.

Both mechanisms have different advantages, the crash reports are automatic and can often give very exact locations where the crash occurs. The email allows follow up with the user about what was happening but provides less technical information and only a small proportion of those who suffer from issues send the email.

The Plan

I decided that the long term cost of maintaining compatibility and two code paths for all areas where iOS 7 features would not be worth it given the speed of adoption of new iOS versions historically. So from the time of announcement I started working on a branch intended to make use of the iOS 7 features. Despite this decision to go iOS 7 only for future versions I didn't want to leave existing users who couldn't or didn't want to update with a poor product so I was determined to crush all the bugs and at least leave a very stable version.

The split actually started with the 1.60 release on 24th June 2013 which I was actually hoping might be the last release for iOS 5 and 6. As it has turned out there were actually another 14 releases after that before I could get to a position where I am happy to release the Fast Lists 2.0 update. 

The Reality

1.60 was a major change that removed the use of the Parse platform for logging because it had been bought by Facebook (see previous post). It also added the use of PLCrashReporter and JIRAMobileConnect as a partial replacement for Parse, specifically the crash logging which it did somewhat better than my previous solution although without logging some non-crashing errors.

This meant several latent issues were revealed that led to a further sequence of updates to fix issues that were revealed (some of which were caused by a mistake in the way I added the crash reporting). After that 1.60 release bugs were squashed one or two at a time and in parallel I was working on version 2.0 to properly adopt the new design and handling the variable sized text.

The software was improving and number of crashes falling until when iOS 7 was released I was getting a significant number of issues saving on that platform with an error regarding conflicts when there was actually no reason a conflict should have been reported. I was not able to reproduce the problem on my test systems.

1.73 - Xcode 5.0 and NSMergeByPropertyObjectTrumpMergePolicy

To resolve the problem of conflicts during saves I changed the mode of saving to specify that the current in memory version should be prioritised in the save (NSMergeByPropertyObjectTrumpMergePolicy) rather than raising an error on save problems. I also used Xcode 5 to build this version which turned out to be a mistake for several reasons. The most obvious reason was that this introduced the iOS 7 look and feel but without the proper modifications that I had made to make the design really work well with Fast Lists (I should have realised that this would happen and probably retained Xcode 4.6). The other reason was that this exposed me to a bug but it would be some time before I discovered that.

As soon as it hit the App Store I found that it seemed to have resolved the issues that I was having on iOS 7 unfortunately it became apparent over the few days after the release that I was now having problems while saving on iOS 5 and 6. Again I could not reproduce this locally and additionally I no longer had any iOS 6 devices with which to test.

1.74 - Xcode 5.01

This was a very quick release that wasn't a major change but used Xcode 5.01 to build in case it resolved the crash reports I was suffering from on iOS 5 and 6. I was only receiving crash reports initially and they hadn't isolated the issue, (although I hadn't realised it they were in fact referencing the location where I was aborting on serious errors after the users had been offered the option of emailing me). 1.74 didn't help at all.

The Assumption

Quite naturally I assumed that the small change that I had made in 1.73 at exactly the place in the code where I was now seeing errors was to blame. It is usually the change you make in the software that is to blame when a problem is introduced. So while I couldn't reproduce the problem I did the obvious next step and reverted the behaviour except for iOS 7 (where this problem wasn't happening and the previous problem seemed to be resolved). As it turned out this didn't actually help at all!

1.75 - Make NSMergeByPropertyObjectTrumpMergePolicy iOS 7 Only

By this time I had received some emailed error reports showing validation errors errors on iOS 6 were happening. The errors I received were like this:

FATAL ERROR
Error UUID: 1510B4F6-0E85-44D0-8DCC-2D5A269EA66A
App UUID: 3E982994-4315-4E76-BB21-6BDF25E37BC6
Error Message: "Apologies but it has not been possible to save your last change.  Please inform support@human-friendly.com about this issue with any information that you have about what caused it."Error Details:
Error Domain=NSCocoaErrorDomain Code=1570 "The operation couldn’t be completed. (Cocoa error 1570.)" UserInfo=0x1d849b70 {NSValidationErrorObject=<TopLists: 0x1c54cd70> (entity: TopLists; id: 0x1c588d20 <x-coredata:///TopLists/tB6F75849-4766-4605-BE60-8AFB2BBC95B22> ; data: {
    list =    (
    );
    name = nil;
    parent = nil;
}), NSValidationErrorKey=name, NSLocalizedDescription=The operation couldn’t be completed. (Cocoa error 1570.)}
Model: iPad
Platform:iPad2,2
Model: K94AP
SystemVersion: 6.0.1
App Version: 1.74
Bundle Version: 1.74
Free Disk space: 7329200KiB
Now this report is more useful than I had realised. I save the updates that I have made from many different areas of the code (every update from a new item, a name change, a deletion or even ticking or unticking a checkbox) but I should have taken the error at face value and looked for where toplevel lists (the only time parent should be nil) are saved and particularly at their creation to verify any potential sources of nil names and try to reproduce in the simulator but I focussed too much on the the idea that it was a Core Data problem.

1.76, (unreleased 1.77) and 1.78

These releases fixed a number of other issues (related dragging lists on iPads, iOS 7 layout issue, and a fix for deletions) and added a stacktrace to the emailed error reports. Then I had to wait for an email report with the new error information.

It finally arrived on 29th November (and actually it is the only emailed report form the 1.78 version I have received even now since 1.78 was released on 21st November) so I am very grateful to the reporter.

FATAL ERROR
Error UUID: F4935501-D278-46CE-8257-DADB5A3CD7FD
App UUID: 6B900CD9-7FD4-42A6-A9D9-D0DEBECA2F6E
Error Message: "Apologies but it has not been possible to save your last change.  Please inform support@human-friendly.com about this issue with any information that you have about what caused it."Error Details:
Error Domain=NSCocoaErrorDomain Code=1570 "The operation couldn’t be completed. (Cocoa error 1570.)" UserInfo=0x1ddad030 {NSValidationErrorObject=<TopLists: 0x1ddb9f50> (entity: TopLists; id: 0x1ddb3250 <x-coredata:///TopLists/tAC51B92C-A320-44BA-9D91-27EBA6E0C4322> ; data: {
    list =    (
    );
    name = nil;
    parent = nil;
}), NSValidationErrorKey=name, callStackAddresses=(0x35785 0x3565d 0x333f9 0x3664d0c5 0x3664d077 0x3664d055 0x3664c90b 0x3664ce01 0x3656b421 0x3472f6cd 0x3472d9c1 0x3472dd17 0x346a0ebd 0x346a0d49 0x382532eb 0x365b6301 0x2f823 0x2f7d8), NSLocalizedDescription=The operation couldn’t be completed. (Cocoa error 1570.), callStackSymbols=(
0  Fast Lists                          0x00035745 Fast Lists + 55109
1  Fast Lists                          0x0003565d Fast Lists + 54877
2  Fast Lists                          0x000333f9 Fast Lists + 46073
3  UIKit                              0x3664d0c5 <redacted> + 72
4  UIKit                              0x3664d077 <redacted> + 30
5  UIKit                              0x3664d055 <redacted> + 44
6  UIKit                              0x3664c90b <redacted> + 502
7  UIKit                              0x3664ce01 <redacted> + 488
8  UIKit                              0x3656b421 <redacted> + 5768
9  CoreFoundation                      0x3472f6cd <redacted> + 20
10  CoreFoundation                      0x3472d9c1 <redacted> + 276
11  CoreFoundation                      0x3472dd17 <redacted> + 742
12  CoreFoundation                      0x346a0ebd CFRunLoopRunSpecific + 356
13  CoreFoundation                      0x346a0d49 CFRunLoopRunInMode + 104
14  GraphicsServices                    0x382532eb GSEventRunModal + 74
15  UIKit                              0x365b6301 UIApplicationMain + 1120
16  Fast Lists                          0x0002f823 Fast Lists + 30755
17  Fast Lists                          0x0002f7d8 Fast Lists + 30680
)}
Model: iPod touch
Platform:iPod4,1
Model: N81AP
SystemVersion: 6.1.5
App Version: 1.78
Bundle Version: 1.78
Free Disk space: 17439224KiB
I could copy the stacktrace into the crash report that matched this incident (identified by the timing of the reports) to symbolicate it. The result was clear that this was happening when the toplevel  '+' button was being used to create new top level lists.

With that information and the validation errors about the nil name I tried it in the simulator and managed to reproduce the issue. It proved to be that instead of enabling the text input field when the '+' was pressed with no name in the field it was trying to create the list with a nil name. This is the relevant function:

-(void)cellButtonAdd:(UIButton *) addButton
{
    if([[newListCell.nameField text] isEqual:@""])
    {
        [newListCell.nameField becomeFirstResponder];
        [[HFUsageLogger getSharedLogger] incrementCounterForName:@"topListCellButtonAddEmptyField"];
        return;
    }
    // Create a new instance of the entity managed by the fetched results controller.
    NSManagedObjectContext *context = [self.fetchedResultsController managedObjectContext];
    TopLists *newTopList  = (TopLists *)[NSEntityDescription insertNewObjectForEntityForName:@"TopLists" inManagedObjectContext:context];
       
    newTopList.name = [[newListCell nameField]text];
    newTopList.newItem = YES;
    [newListCell.nameField resignFirstResponder];
    newListCellString = @"";
    newListCell.nameField.text = @"";
    NSIndexPath * selected = [self.tableView indexPathForSelectedRow];
    if(selected)
        [self.tableView deselectRowAtIndexPath:selected animated:YES];
    [newTopList save];
    if(addButton)
    {
        [[HFUsageLogger getSharedLogger] incrementCounterForName:@"topListCellButtonAddSucceeded"];
    }
    else
    {
        [[HFUsageLogger getSharedLogger] incrementCounterForName:@"topListReturnKeyAddSucceeded"];
    }
}
[newTopList save] eventually calls [managedContext save] which is when the error occurs.

This code is actually correct but it was clear that the check at the beginning was not working:
if([[newListCell.nameField text] isEqual:@""])
In the debugger I could clearly see[that `[newListCell.nameField text]` was returning nil (which is clearly not equal to the empty string @""). This is not the correct behaviour; the UITextField class reference for the text says: This string is @"" by default. Testing on iOS 7 shows the correct behaviour (as do iOS 5 and 6 when the app is built with Xcode 4).

Once I had found this the fix was a fairly simple replacement of the test:
if([[newListCell.nameField text] length ] == 0)
In Objective C if you send a message to nil it is safe but does nothing except return nil (0) so whether we have an empty string or a nil value instead of the string this test handles it correctly.

Conclusion

I should have assumed less about the bug and tested more on the simulator (as I don't currently have an iOS 6 device), maybe I could have found it sooner. However I was VERY unlucky to hit a bug affecting a particular combination of build tool and iOS version at exactly the time I made a change that affected the exact functionality where I was seeing errors.
Apologies for such a long and dull post but I needed to get it down.