ListAdderViewController.m

/*
     File: ListAdderViewController.m
 Abstract: Main view controller.
  Version: 1.1
 
 Disclaimer: IMPORTANT:  This Apple software is supplied to you by Apple
 Inc. ("Apple") in consideration of your agreement to the following
 terms, and your use, installation, modification or redistribution of
 this Apple software constitutes acceptance of these terms.  If you do
 not agree with these terms, please do not use, install, modify or
 redistribute this Apple software.
 
 In consideration of your agreement to abide by the following terms, and
 subject to these terms, Apple grants you a personal, non-exclusive
 license, under Apple's copyrights in this original Apple software (the
 "Apple Software"), to use, reproduce, modify and redistribute the Apple
 Software, with or without modifications, in source and/or binary forms;
 provided that if you redistribute the Apple Software in its entirety and
 without modifications, you must retain this notice and the following
 text and disclaimers in all such redistributions of the Apple Software.
 Neither the name, trademarks, service marks or logos of Apple Inc. may
 be used to endorse or promote products derived from the Apple Software
 without specific prior written permission from Apple.  Except as
 expressly stated in this notice, no other rights or licenses, express or
 implied, are granted by Apple herein, including but not limited to any
 patent rights that may be infringed by your derivative works or by other
 works in which the Apple Software may be incorporated.
 
 The Apple Software is provided by Apple on an "AS IS" basis.  APPLE
 MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
 THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS
 FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND
 OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS.
 
 IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL
 OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION,
 MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED
 AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE),
 STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE
 POSSIBILITY OF SUCH DAMAGE.
 
 Copyright (C) 2014 Apple Inc. All Rights Reserved.
 
 */
 
#import "ListAdderViewController.h"
 
#import "NumberPickerController.h"
#import "OptionsController.h"
 
#import "AdderOperation.h"
 
@interface ListAdderViewController () <NumberPickerControllerDelegate, OptionsControllerDelegate>
 
// private properties
 
@property (nonatomic, strong, readwrite) NSMutableArray *   numbers;
@property (nonatomic, strong, readwrite) NSOperationQueue * queue;
@property (nonatomic, assign, readwrite) BOOL               recalculating;
@property (nonatomic, strong, readwrite) AdderOperation *   inProgressAdder;
@property (nonatomic, copy,   readwrite) NSString *         formattedTotal;
 
@end
 
static char CharForCurrentThread(void)
    // Returns 'M' if we're running on the main thread, or 'S' otherwies.
{
    return [NSThread isMainThread] ? 'M' : 'S';
}
 
@implementation ListAdderViewController
 
+ (NSArray *)defaultNumbers
    // Returns the default numbers that we initialise the view with.
{
    return @[ @7, @5, @8, @9, @7, @6 ];
}
 
- (void)awakeFromNib
{
    // Set up some private properties.
    
    self.numbers = [[[self class] defaultNumbers] mutableCopy];
    assert(self.numbers != nil);
 
    self.queue = [[NSOperationQueue alloc] init];
    assert(self.queue != nil);
    
    // Observe .recalculating to trigger reloads of the cell in the first 
    // section (kListAdderSectionIndexTotal).
    
    [self addObserver:self forKeyPath:@"recalculating" options:0 context:&self->_recalculating];
}
 
- (void)dealloc
{
    // This is the root view controller of our application, so it can never be 
    // deallocated.  Supporting -dealloc in a view controller in the presence of 
    // threading, even highly constrained threading as used by this example, is 
    // tricky.  I generally recommend that you avoid this, and confine your threads 
    // to your model layer code.  If that's not possible, you can use a technique 
    // like that shown in the QWatchedOperationQueue class in the LinkedImageFetcher 
    // sample code.
    //
    // <http://developer.apple.com/mac/library/samplecode/LinkedImageFetcher/>
    // 
    // However, I didn't want to drag parts of that sample into this sample (especially 
    // given that -dealloc can never be called in this sample and thus I can't test it), 
    // nor did I want to demonstrate an ad hoc, and potentially buggy, version of 
    // -dealloc.  So, for the moment, we just don't support -dealloc.
 
    assert(NO);
 
    // Despite the above, I've left in the following just as an example of how you 
    // manage self observation in a view controller. 
    
    [self removeObserver:self forKeyPath:@"recalculating" context:&self->_recalculating];
}
 
- (void)syncLeftBarButtonTitle
{
    if ([self.numbers count] <= 1) {
        self.navigationItem.leftBarButtonItem.title = @"Defaults";
    } else {
        self.navigationItem.leftBarButtonItem.title = @"Minimum";
    }
}
 
#pragma mark * View controller stuff
 
- (void)viewDidLoad
{
    [super viewDidLoad];
    
    // Configure our table view.
    
    self.tableView.editing = YES;
}
 
- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    
    // When we come on screen, if we don't have a current value and we're not 
    // already calculating a value, kick off an operation to calculate the initial 
    // value of the total.
    
    if ( (self.formattedTotal == nil) && ! self.recalculating ) {
        [self recalculateTotal];
    }
}
 
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender
{
    #pragma unused(sender)
    
    // Dispatch to our various segue-specific methods.
    
    if ([segue.identifier isEqual:@"numberPicker"]) {
        [self prepareForNumberPickerSegue:segue];
    } else if ([segue.identifier isEqual:@"options"]) {
        [self prepareForOptionsSegue:segue];
    } else {
        assert(NO);     // What segue?
    }
}
 
#pragma mark * Table view callbacks
 
enum {
    kListAdderSectionIndexTotal = 0,
    kListAdderSectionIndexAddNumber,
    kListAdderSectionIndexNumbers,
    kListAdderSectionIndexCount
};
 
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tv
{
    #pragma unused(tv)
    assert(tv == self.tableView);
    return kListAdderSectionIndexCount;
}
 
- (NSInteger)tableView:(UITableView *)tv numberOfRowsInSection:(NSInteger)section
{
    #pragma unused(tv)
    #pragma unused(section)
    assert(tv == self.tableView);
    assert(section < kListAdderSectionIndexCount);
 
    return (section == kListAdderSectionIndexNumbers) ? (NSInteger) [self.numbers count] : 1;
}
 
- (BOOL)isValidIndexPath:(NSIndexPath *)indexPath
{
    return (indexPath != NULL) &&
        ((indexPath.section >= 0) && (indexPath.section < kListAdderSectionIndexCount)) &&
        (indexPath.row >= 0) &&
        (((NSUInteger) indexPath.row) < ((indexPath.section == kListAdderSectionIndexNumbers) ? [self.numbers count] : 1));
}
 
- (UITableViewCell *)tableView:(UITableView *)tv cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    #pragma unused(tv)
    #pragma unused(indexPath)
    UITableViewCell *   cell;
 
    assert(tv == self.tableView);
    assert([self isValidIndexPath:indexPath]);
 
    // Previously this code used a single prototype cell and configured it as needed. 
    // This breaks on iOS 8.0, where the detailed text doesn't show up in some 
    // circumstances <rdar://problem/17682058>.  To work around this I now use 
    // multiple cell prototypes, one for each class of cell.  This had the added 
    // advantage of making the code smaller, putting all the UI strings in the 
    // storyboard, and so on.
    
    // Set it up based on the section and row.
    
    switch (indexPath.section) {
        case kListAdderSectionIndexTotal: {
            if (self.recalculating) {
                UIActivityIndicatorView *   activityView;
                
                cell = [self.tableView dequeueReusableCellWithIdentifier:@"totalBusy"];
                assert(cell != nil);
 
                activityView = (UIActivityIndicatorView *) cell.editingAccessoryView;
                assert([activityView isKindOfClass:[UIActivityIndicatorView class]]);
                [activityView startAnimating];
            } else {
                cell = [self.tableView dequeueReusableCellWithIdentifier:@"total"];
                assert(cell != nil);
                cell.detailTextLabel.text = self.formattedTotal;
            }
        } break;
        default:
            assert(NO);
            // fall through
        case kListAdderSectionIndexAddNumber: {
            cell = [self.tableView dequeueReusableCellWithIdentifier:@"add"];
            assert(cell != nil);
        } break;
        case kListAdderSectionIndexNumbers: {
            cell = [self.tableView dequeueReusableCellWithIdentifier:@"number"];
            assert(cell != nil);
            cell.textLabel.text = [NSNumberFormatter localizedStringFromNumber:self.numbers[(NSUInteger) indexPath.row] numberStyle:NSNumberFormatterDecimalStyle];
        } break;
    }
 
    return cell;
}
 
- (UITableViewCellEditingStyle)tableView:(UITableView *)tv editingStyleForRowAtIndexPath:(NSIndexPath *)indexPath
{
    UITableViewCellEditingStyle result;
 
    #pragma unused(tv)
    assert(tv == self.tableView);
    assert([self isValidIndexPath:indexPath]);
    
    switch (indexPath.section) {
        default:
            assert(NO);
            // fall through
        case kListAdderSectionIndexTotal: {
            result = UITableViewCellEditingStyleNone;
        } break;
        case kListAdderSectionIndexAddNumber: {
            result = UITableViewCellEditingStyleInsert;
        } break;
        case kListAdderSectionIndexNumbers: {
            // We don't allow the user to delete the last cell.
            if ([self.numbers count] == 1) {
                result = UITableViewCellEditingStyleNone;
            } else {
                result = UITableViewCellEditingStyleDelete;
            }
        } break;
    }
    return result;
}
 
// I would like to suppress the delete confirmation button but I don't think there's a 
// supported way to do this.
 
- (void)tableView:(UITableView *)tv commitEditingStyle:(UITableViewCellEditingStyle)editingStyle forRowAtIndexPath:(NSIndexPath *)indexPath
{
    #pragma unused(tv)
    assert(tv == self.tableView);
    assert([self isValidIndexPath:indexPath]);
 
    switch (indexPath.section) {
        default:
            assert(NO);
            // fall through
        case kListAdderSectionIndexTotal: {
            assert(NO);
        } break;
        case kListAdderSectionIndexAddNumber: {
            #pragma unused(editingStyle)
            assert(editingStyle == UITableViewCellEditingStyleInsert);
            
            // The user has tapped on the plus button itself (as opposed to the body 
            // of that cell).  Bring up the number picker.
            
            [self presentNumberPickerModally];
        } break;
        case kListAdderSectionIndexNumbers: {
            #pragma unused(editingStyle)
            assert(editingStyle == UITableViewCellEditingStyleDelete);
            assert([self.numbers count] != 0);      // because otherwise we'd have no delete button
 
            // Remove the row from our model and the table view.
            
            [self.numbers removeObjectAtIndex:(NSUInteger) indexPath.row];
            [self.tableView deleteRowsAtIndexPaths:@[ indexPath ] withRowAnimation:UITableViewRowAnimationNone];
            
            // If we've transitioned from 2 rows to 1 row, remove the delete button for the 
            // remaining row; we don't want folks deleting that now, do we?  Also, set the 
            // title of the left bar button to "Defaults" to reflect its updated function.
            
            if ([self.numbers count] == 1) {
                [self.tableView reloadRowsAtIndexPaths:@[ [NSIndexPath indexPathForRow:0 inSection:kListAdderSectionIndexNumbers] ] withRowAnimation:UITableViewRowAnimationNone];
                
                [self syncLeftBarButtonTitle];
            }
            
            // We've modified numbers, so kick off a recalculation.
            
            [self recalculateTotal];
        } break;
    }
}
 
- (void)tableView:(UITableView *)tv didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    #pragma unused(tv)
    #pragma unused(indexPath)
 
    assert(tv == self.tableView);
    assert([self isValidIndexPath:indexPath]);
 
    [self.tableView deselectRowAtIndexPath:indexPath animated:YES];
 
    switch (indexPath.section) {
        default:
            assert(NO);
            // fall through
        case kListAdderSectionIndexTotal: {
            // do nothing
        } break;
        case kListAdderSectionIndexAddNumber: {
 
            // The user has tapped on the body of the cell associated with plus button.  
            // Bring up the number picker.
 
            [self presentNumberPickerModally];
        } break;
        case kListAdderSectionIndexNumbers: {
            // do nothing
        } break;
    }
}
 
#pragma mark * Number picker management
 
- (void)presentNumberPickerModally
{
    [self performSegueWithIdentifier:@"numberPicker" sender:self];
}
 
- (void)prepareForNumberPickerSegue:(UIStoryboardSegue *)segue
{
    UINavigationController *    nav;
    NumberPickerController *    numberPicker;
 
    nav = segue.destinationViewController;
    assert([nav isKindOfClass:[UINavigationController class]]);
 
    numberPicker = nav.viewControllers[0];
    assert([numberPicker isKindOfClass:[NumberPickerController class]]);
 
    numberPicker.delegate = self;
}
 
- (void)numberPicker:(NumberPickerController *)controller didChooseNumber:(NSNumber *)number
    // Called by the number picker when the user chooses a number or taps cancel.
{
    #pragma unused(controller)
    assert(controller != nil);
    
    // If it wasn't cancelled...
    
    if (number != nil) {
    
        // Add the number to our model and the table view.
        
        [self.numbers addObject:number];
        [self.tableView insertRowsAtIndexPaths:@[ [NSIndexPath indexPathForRow:(NSInteger) [self.numbers count] - 1 inSection:kListAdderSectionIndexNumbers] ] withRowAnimation:UITableViewRowAnimationNone];
 
        // If we've transitioned from 1 row to 2 rows, add the delete button back for 
        // the first row.  Also change the left bar button item back to "Minimum".
        
        if ([self.numbers count] == 2) {
            [self.tableView reloadRowsAtIndexPaths:@[ [NSIndexPath indexPathForRow:0 inSection:kListAdderSectionIndexNumbers] ] withRowAnimation:UITableViewRowAnimationNone];
            
            [self syncLeftBarButtonTitle];
        }
 
        // We've modified numbers, so kick off a recalculation.
        
        [self recalculateTotal];
    }
    [self dismissViewControllerAnimated:YES completion:nil];
}
 
#pragma mark * Options management
 
- (void)prepareForOptionsSegue:(UIStoryboardSegue *)segue
{
    UINavigationController *    nav;
    OptionsController *         options;
 
    nav = segue.destinationViewController;
    assert([nav isKindOfClass:[UINavigationController class]]);
 
    options = nav.viewControllers[0];
    assert([options isKindOfClass:[OptionsController class]]);
 
    options.delegate = self;
}
 
- (void)didSaveOptions:(OptionsController *)controller
    // Called when the user taps Save in the options view.  The options 
    // view has already saved the options, so we have nothing to do other 
    // than to tear down the view.
{
    #pragma unused(controller)
    assert(controller != nil);
    [self dismissViewControllerAnimated:YES completion:nil];
}
 
- (void)didCancelOptions:(OptionsController *)controller
    // Called when the user taps Cancel in the options view.
{
    #pragma unused(controller)
    assert(controller != nil);
    [self dismissViewControllerAnimated:YES completion:nil];
}
 
#pragma mark * Async recalculation
 
- (void)recalculateTotal
    // Starts a recalculation using either the thread- or NSOperation-based code.
{
    if ( [[NSUserDefaults standardUserDefaults] boolForKey:@"useThreadsDirectly"] ) {
        [self recalculateTotalUsingThread];
    } else {
        [self recalculateTotalUsingOperation];
    }
}
 
#pragma mark - NSThread
 
- (void)recalculateTotalUsingThread
    // Starts a recalculation using a thread.
{
    if ( [[NSUserDefaults standardUserDefaults] boolForKey:@"retainNotCopy"] ) {
        self.recalculating = YES;
 
        [self performSelectorInBackground:@selector(threadRecalculateNumbers:) withObject:self.numbers];
    } else {
        NSArray *       immutableNumbers;
 
        self.recalculating = YES;
        
        immutableNumbers = [self.numbers copy];
        assert(immutableNumbers != nil);
        [self performSelectorInBackground:@selector(threadRecalculateNumbers:) withObject:immutableNumbers];
    }
}
 
- (void)threadRecalculateNumbers:(NSArray *)immutableNumbers
    // Does the actual recalculation when we're in threaded mode.  Always 
    // called on a secondary thread.
{
    @autoreleasepool {
        NSInteger               total;
        NSUInteger              numberCount;
        NSUInteger              numberIndex;
        NSString *              totalStr;
 
        assert( ! [NSThread isMainThread] );
 
        total = 0;
        numberCount = [immutableNumbers count];
        for (numberIndex = 0; numberIndex < numberCount; numberIndex++) {
            NSNumber *  numberObj;
            
            // Sleep for a while.  This makes it easiest to test various problematic cases.
            
            [NSThread sleepForTimeInterval:1.0];
            
            // Do the maths.
            
            numberObj = immutableNumbers[numberIndex];
            assert([numberObj isKindOfClass:[NSNumber class]]);
            
            total += [numberObj integerValue];
        }
        
        totalStr = [NSString stringWithFormat:@"%ld", (long) total];
        if ( [[NSUserDefaults standardUserDefaults] boolForKey:@"applyResultsFromThread"] ) {
            self.formattedTotal = totalStr;
            self.recalculating = NO;
        } else {
            [self performSelectorOnMainThread:@selector(threadRecalculateDone:) withObject:totalStr waitUntilDone:NO];
        }
    }
}
 
- (void)threadRecalculateDone:(NSString *)result
    // In threaded mode, called on the main thread to apply the results to the UI.
{
    assert([NSThread isMainThread]);
 
    // The user interface is adjusted by a KVO observer on recalculating.
 
    self.formattedTotal = result;
    self.recalculating = NO;
}
 
- (void)threadRecalculateNumbers
{
    @autoreleasepool {
        NSInteger               total;
        NSUInteger              numberCount;
        NSUInteger              numberIndex;
        NSString *              totalStr;
 
        // IMPORTANT: This method is not actually used.  It is here because it's a code 
        // snippet in the technote, and i wanted to make sure it compiles.
        
        assert(NO);
        
        total = 0;
        numberCount = [self.numbers count];
        for (numberIndex = 0; numberIndex < numberCount; numberIndex++) {
            NSNumber *  numberObj;
            
            // Sleep for a while.  This makes it easiest to test various problematic cases.
            
            [NSThread sleepForTimeInterval:1.0];
            
            // Do the maths.
            
            numberObj = self.numbers[numberIndex];
            assert([numberObj isKindOfClass:[NSNumber class]]);
            
            total += [numberObj integerValue];
        }
        
        // The user interface is adjusted by a KVO observer on recalculating.
 
        totalStr = [NSString stringWithFormat:@"%ld", (long) total];
        self.formattedTotal = totalStr;
        self.recalculating = NO;
    }
}
 
- (void)threadRecalculateNumbers2
{
    @autoreleasepool {
        NSInteger               total;
        NSUInteger              numberCount;
        NSUInteger              numberIndex;
        NSString *              totalStr;
 
        // IMPORTANT: This method is not actually used.  It is here because it's a code 
        // snippet in the technote, and i wanted to make sure it compiles.
        
        assert(NO);
        
        total = 0;
        numberCount = [self.numbers count];
        for (numberIndex = 0; numberIndex < numberCount; numberIndex++) {
            NSNumber *  numberObj;
            
            // Sleep for a while.  This makes it easiest to test various problematic cases.
            
            [NSThread sleepForTimeInterval:1.0];
            
            // Do the maths.
            
            numberObj = self.numbers[numberIndex];
            assert([numberObj isKindOfClass:[NSNumber class]]);
            
            total += [numberObj integerValue];
        }
        
        // Update the user interface on the main thread.
 
        totalStr = [NSString stringWithFormat:@"%ld", (long) total];
        [self performSelectorOnMainThread:@selector(threadRecalculateDone:) withObject:totalStr waitUntilDone:NO];
    }
}
 
#pragma mark - NSOperation
 
- (void)recalculateTotalUsingOperation
    // Starts a recalculation using an NSOperation.
{
    // If we're already calculating, cancel that operation.  It's going to 
    // yield stale results.  We don't remove the observer here, but rather 
    // remove the observer when it completes.  Also, we don't nil out 
    // inProgressAdder because it'll just get replaced in the next line 
    // and changing the value triggers an unnecessary KVO notification of 
    // recalculating.
    
    if (self.inProgressAdder != nil) {
        fprintf(stderr, "%c %3lu cancelled\n", CharForCurrentThread(), (unsigned long) self.inProgressAdder.sequenceNumber);
        [self.inProgressAdder cancel];
    }
 
    // Start up a replacement operation.
    
    self.inProgressAdder = [[AdderOperation alloc] initWithNumbers:self.numbers];
    assert(self.inProgressAdder != nil);
    
    if ([[NSUserDefaults standardUserDefaults] boolForKey:@"addFaster"]) {
        self.inProgressAdder.interNumberDelay = 0.2;
    }
    
    [self.inProgressAdder addObserver:self forKeyPath:@"isFinished"  options:0 context:&self->_formattedTotal];
    [self.inProgressAdder addObserver:self forKeyPath:@"isExecuting" options:0 context:&self->_queue];
    
    fprintf(stderr, "%c %3lu queuing\n", CharForCurrentThread(), (unsigned long) self.inProgressAdder.sequenceNumber);
    [self.queue addOperation:self.inProgressAdder];
 
    // The user interface is adjusted by a KVO observer on recalculating.
    
    self.recalculating = YES;
}
 
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
    if (context == &self->_formattedTotal) {
        AdderOperation *    op;
 
        // If the operation has finished, call -adderOperationDone: on the main thread to deal 
        // with the results.
 
        // can be running on any thread
        assert([keyPath isEqual:@"isFinished"]);
        op = (AdderOperation *) object;
        assert([op isKindOfClass:[AdderOperation class]]);
        assert([op isFinished]);
 
        fprintf(stderr, "%c %3lu finished\n", CharForCurrentThread(), (unsigned long) op.sequenceNumber);
        if ([[NSUserDefaults standardUserDefaults] boolForKey:@"applyResultsFromThread"]) {
            [self adderOperationDone:op];
        } else {
            if ([[NSUserDefaults standardUserDefaults] boolForKey:@"allowStale"]) {
                [self performSelectorOnMainThread:@selector(adderOperationDoneWrong:) withObject:op waitUntilDone:NO];
            } else {
                [self performSelectorOnMainThread:@selector(adderOperationDone:)      withObject:op waitUntilDone:NO];
            }
        }
    } else if (context == &self->_queue) {
        AdderOperation *    op;
        
        // We observe -isExecuting purely for logging purposes.
        
        // can be running on any thread
        assert([keyPath isEqual:@"isExecuting"]);
        op = (AdderOperation *) object;
        assert([op isKindOfClass:[AdderOperation class]]);
        if ([op isExecuting]) {
            fprintf(stderr, "%c %3lu executing\n", CharForCurrentThread(), (unsigned long) op.sequenceNumber);
        } else {
            fprintf(stderr, "%c %3lu stopped\n", CharForCurrentThread(), (unsigned long) op.sequenceNumber);
        }
    } else if (context == &self->_recalculating) {
    
        // If recalculating changes, reload the first section (kListAdderSectionIndexTotal) 
        // which causes the activity indicator to come or go.
        
        assert([NSThread isMainThread]);
        assert([keyPath isEqual:@"recalculating"]);
        assert(object == self);
        if (self.isViewLoaded) {
            [self.tableView reloadRowsAtIndexPaths:@[ [NSIndexPath indexPathForRow:0 inSection:kListAdderSectionIndexTotal] ] withRowAnimation:UITableViewRowAnimationNone];
        }
    } else {
        [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
    }
}
 
- (void)adderOperationDone:(AdderOperation *)op
{
    assert([NSThread isMainThread]);
    assert([op isKindOfClass:[AdderOperation class]]);
    
    assert(self.recalculating);
    
    // Always remove our observer, regardless of whether we care about 
    // the results of this operation.
    
    fprintf(stderr, "%c %3lu done\n", CharForCurrentThread(), (unsigned long) op.sequenceNumber);
    [op removeObserver:self forKeyPath:@"isFinished"  context:&self->_formattedTotal];
    [op removeObserver:self forKeyPath:@"isExecuting" context:&self->_queue];
 
    // Check to see whether these are the results we're looking for. 
    // If not, we just discard the results; later on we'll be notified 
    // of the latest add operation completing.
    
    if (op == self.inProgressAdder) {
        assert( ! [op isCancelled] );
 
        // Commit the value to our model.
 
        fprintf(stderr, "%c %3lu commit\n", CharForCurrentThread(), (unsigned long) op.sequenceNumber);
 
        self.formattedTotal = op.formattedTotal;
 
        // Clear out our record of the operation.  The user interface is adjusted 
        // by a KVO observer on recalculating.
        
        self.inProgressAdder = nil;
        self.recalculating = NO;
    } else {
        fprintf(stderr, "%c %3lu discard\n", CharForCurrentThread(), (unsigned long) op.sequenceNumber);
    }
}
 
- (void)adderOperationDoneWrong:(AdderOperation *)op
{
    assert([NSThread isMainThread]);
    assert([op isKindOfClass:[AdderOperation class]]);
    
    // Because we're ignoring stale operations, the following assert will 
    // trips.
    //
    // assert(self.recalculating);
    
    // Always remove our observer, regardless of whether we care about 
    // the results of this operation.
    
    fprintf(stderr, "%c %3lu done\n", CharForCurrentThread(), (unsigned long) op.sequenceNumber);
    [op removeObserver:self forKeyPath:@"isFinished"  context:&self->_formattedTotal];
    [op removeObserver:self forKeyPath:@"isExecuting" context:&self->_queue];
 
    // Check to see whether these are the results we're looking for. 
    // If not, we just discard the results; later on we'll be notified 
    // of the latest add operation completing.
    
    // Because we're ignoring stale operations, the following assert will 
    // trips.
    // 
    // assert( ! [op isCancelled] );
 
    // Commit the value to our model.
 
    fprintf(stderr, "%c %3lu commit\n", CharForCurrentThread(), (unsigned long) op.sequenceNumber);
 
    self.formattedTotal = op.formattedTotal;
 
    // Clear out our record of the operation.  The user interface is adjusted 
    // by a KVO observer on recalculating.
    
    self.inProgressAdder = nil;
    self.recalculating = NO;
}
 
#pragma mark * UI actions
 
- (IBAction)defaultsOrMinimumAction:(id)sender
    // Called when the user taps the left bar button ("Defaults" or "Minimum").  
    // If we have lots of numbers, set the list to contain a sigle entry.  If we have 
    // just one number, reset the list back to the defaults.  This allows us to easily 
    // test cancellation and the discard of stale results.
{
    #pragma unused(sender)
    if ([self.numbers count] > 1) {
        [self.numbers removeAllObjects];
        [self.numbers addObject:@41];
    } else {
        [self.numbers replaceObjectsInRange:NSMakeRange(0, [self.numbers count]) withObjectsFromArray:[[self class] defaultNumbers]];
    }
    [self syncLeftBarButtonTitle];
    if (self.isViewLoaded) {
        [self.tableView reloadData];
    }
    [self recalculateTotal];
}
 
@end