Advanced programming tips, tricks and hacks for Mac development in C/Objective-C and Cocoa.

A sample iPhone application with complete unit tests

In this post, I present a complete Cocoa Touch iPhone application implemented with unit tests for all created code. I'll look at setting up build and debug targets and executables for both Application and Logic tests, and show you some of the differences between Application and Logic tests.

The code for this post is an iPhone version of the Mac post I presented last week.

Xcode unit testing targets for the iPhone

Like Mac unit testing, unit testing targets on the iPhone are generally divided into two types:

  • Logic tests — these are run in a executable that is separate from your application. The separate build can be easier to manage, faster to build and is easier to run objects in isolation since it avoids the application setup. However, you cannot test components which rely on the application (which is most user interface components). Generally, this type of test target is intended for libraries, frameworks and testing the back-end (model components of model-view-controller) of your application.

    Logic tests are easily run at build-time or from the command-line, which is helpful for continuous integration or automated test processes.

  • Application tests — these tests let the application load first and are subsequently loaded into the existing application. This means that the full application environment is available to your tests. In many cases, controller tests and view tests need to be run as application tests since they are reliant on the full environment.

    Application tests allow your application to be tested in a more realistic environment, reducing the chance that environment or integration level issues will be missed. They are normally run as a separate step (not as part of the build) and therefore may be less convenient for tests that need to be run every time.

In last week's post on Mac development, I covered the Application Tests exclusively. For the iPhone, I will cover both. The reason for this is:

iPhone Application Tests will only run on the device.

This is annoying. It means that you must have a device connected to your test machine via USB to run Application Tests and endure the slow copy-to-device and slow gdb interaction speeds. Apple could have allowed the simulator to perform the same bundle injection but they have not.

There are alternatives to this approach. You could probably run a regular iPhone application in the simulator and invoke SenTestingKit manually after your application starts. There's also Google's Toolbox for Mac has an iPhone Unit Testing approach that runs in the simulator.

But I'm going to keep to the default OCUnit approaches used in Xcode, so I'll show you how to configure both Application Tests and Logic Tests and I'll show you the sorts of tests that will run in both and which will require the full Application Test to run.

To be clear: if you want to run Logic Tests for your Mac applications, you can follow almost exactly the same approach that I present here. For Mac applications though, since you can invoke the ApplicationTests from the command line (as a post-build step or integration step) I would not normally consider the hassle of maintaining an extra build target and executable worth the effort.

iPhone project configuration

Unit testing targets

After you've created a blank project, use the "Project→New Target..." menu item to add two new "Cocoa Touch→Unit Testing Bundle" Targets to the project. These will be the ApplicationTests and LogicTests bundles.

Be wary of target memberships: When you add bundle resources and sources-to-compile to the original project, also add these to the LogicTests target but do not add them to the ApplicationTests target. Testing-specific files should be added to the LogicTests target and the ApplicationTests target but not to the original project.

As with the Mac project configuration, that's sadly not enough to make it work.

First, drag your original application's target onto the ApplicationTests and LogicTests targets to create a dependency (force the application to build before the unit tests).

Then delete the Run Script phase of the ApplicationTests testing target. It's only for logic tests and won't work for application tests.

Then edit the ApplicationTests target's settings (Right click→Get Info) and set the "Build→Linking→Bundle Loader" for all configurations to:

$(CONFIGURATION_BUILD_DIR)/WhereIsMyPhone.app/WhereIsMyPhone

where "WhereIsMyPhone" is the name of the application you're unit testing. As with the Mac setup, this step lets the testing target link against the application (the ApplicationTests target doesn't include the original source files and we don't want linker errors when compiling). Unlike the Mac setup, we don't need to set the "Unit Testing→Test Host" (since we can't run using the build-time step).

On the build configuration, also add APPLICATION_TESTS to the "GCC 4.2 - Preprocessing→Preprocessor Macros". This will let us flag certain tests for running in the ApplicationTests bundle but not the LogicTests.

It'd be nice to use the OCMock framework but the iPhone doesn't allow dynamic linking. Instead we need to take the OCMock source code from, OCMock on GitHub, take the non-test-related files and add them directly to both unit testing targets' "Compile Sources" phases. To get it to compile for the iPhone you'll need to replace isEqualTo: in OCMConstraint.m with isEqual:, the import of <objc/objc-runtime.h> in OCPartialMockObject.m with <objc/runtime.h> and the typeof in OCMArg.h with __typeof.

To ensure that the compiler will find the OCMock headers you will also need to add $SRCROOT (non-recursive) to the Project Settings ("Project→Edit Project Settings") under "Build→Search Paths→Header Search Paths" (for all Configurations).

Debugging setup for the ApplicationTests target

To run the application tests, we need to make a copy of the original iPhone application's target (Right Click→Duplicate) — give it a name like "WhereIsMyPhoneTesting" to distinguish from the original "WhereIsMyPhone" target.

Separate targets again: You will need to keep the duplicate in sync with the original. This means that all source files and resources added to the application should also be added to the testing target.

With all these separate targets, it's worth noting that you don't need to add to each target separately — when you're adding a new file to the project, you are normally given the list of targets and you select the checkboxes for the appropriate targets all at once. Don't add test files to the application and don't add application files to the ApplicationTests target and everything will work.

Drag the ApplicationTests target onto this duplicate (to make the duplicate dependent on the application testing bundle). Also drag the application testing bundle (in the Group Tree under Products named ApplicationTests.octest) to the new "WhereIsMyPhoneTesting" target's "Copy Bundle Resources" phase.

Finally, in the settings for the "WhereIsMyPhoneTesting" target, for all Configurations set the "Build→Packaging→Product Name" to something distinct from the original target (e.g. change "WhereIsMyPhone" to "WhereIsMyPhoneTesting").

iPhone application tests only work on the device.
To run the iPhone's application unit tests, you must build and install onto a device. They will not load in the simulator. Yes, this is annoying. Please request simulator support for Application Tests in future iPhone SDK releases if this bothers you.

To set breakpoints and interactively debug, you'll need to build the WhereIsMyPhoneTesting target before running the WhereIsMyPhoneTesting executable on your device. If the project contains the custom executable (from the LogicTests setup below) you may need to set the project's Executable (from the "Project→Set Active Exectuable..." menu) to WhereIsMyPhoneTesting manually.

Debugging setup for the LogicTests target

For the logic tests, we don't create another target but we do need to create a custom executable ("Project→New Custom Executable...") named "DebugLogicTests" or similar with a path relative to the Current SDK of:

Developer/usr/bin/otest

This is Apple's test harness and will handle the work of running the LogicTests bundle headlessly in the iPhone Simulator.

You also need to set the Arguments and Environment for this executable to work. Double click the DebugLogicTests item in the Executables section of the Group Tree and edit the Arguments tab to look like this:

debuglogictests.png
Warning: order of arguments is important (the "-SenTest Self" must be first).

Note: If you're in a hurry, the DLYD_ROOT_PATH and DYLD_FORCE_FLAT_NAMESPACE are normally enough to get it running. The rest relate to minor configuration points that you may want or need eventually but which aren't critical — however, they may cause unexpected runtime behavior if missing.

To set breakpoints and interactively debug, you'll need to build the LogicTests target before running the DebugLogicTests executable in the simulator. Since the project contains a custom executable you may need to set the project's Executable (from the "Project→Set Active Exectuable..." menu) to DebugLogicTests first.

projectconfig.png

The project on the left is configured to run Application tests.
The project on the right is configured to run Logic tests.
Set Target and Executable to "WhereIsMyPhone" to run the application as normal.

If you have tests running at build-time that fail but the actual compile has succeeded, you can Debug without rebuilding by pressing Command-Option-Return ("Run→Debug").

The sample iPhone application

As with last week, the sample application is a recreation of my earlier post, WhereIsMyMac, this time written for the iPhone using a test-first approach to development.

With so much discussion about how to set up the Xcode project, I might keep the discussion of the tests themselves quite brief. If you want to know more about the implementation of the tests, they are almost identical to last week's — I deliberately chose this application because the implementation is very similar on the Mac and iPhone — so I encourage you to read through the writing of the tests from that post.

The only significant differences between the iPhone and Mac applications are that the Window Controller from last week is a View Controller in the iPhone version, NSTextFields are now UILabels and the UIWebView that the iPhone version contains is simpler than the WebView on the Mac, so the mainFrame access never occurs on the iPhone.

There is no application for the LogicTests

What I'll focus on this time, are the differences between the Mac and iPhone tests and implementations. Here's an example:

#ifdef APPLICATION_TESTS

- (void)testAppDelegate
{
   id appDelegate = [[UIApplication sharedApplication] delegate];
   STAssertNotNil(appDelegate, @"Cannot find the application delegate.");
}

#endif

On the Mac, this code was almost the same (NSApplication instead of UIApplication). The big difference is that I've wrapped the entire test in #ifdef APPLICATION_TESTS since the application delegate does not exist in the logic tests, we must ensure this test is only run for application tests (we added the APPLICATION_TESTS preprocessor macro to the ApplicationTests target only).

LogicTests are a different bundle

The next big difference is that the actual application code, which runs out of the mainBundle in the regular application, runs out of the LogicTests.octest bundle that otest loads in the LogicTests target. To make code that accesses the bundle work the same, independent of where it is running, all NSBundle access is handled by class.

For example, the code in WhereIsMyPhoneViewController.m that generates the HTML to send to the UIWebView fetches the structure of the HTML from a file with:

[[NSBundle bundleForClass:[self class]]
    pathForResource:@"HTMLFormatString"
    ofType:@"html"]

Previously, I would have used [NSBundle mainBundle] here but that code failed when run in the LogicTests. The [NSBundle bundleForClass:[self class]] will find the bundle that contains the class whether it is the main bundle or the LogicTests.octest bundle loaded by otest.

Many UIKit classes won't work without the UIApplication

Trying to add code to pass the tests in this project revealed that other tests would have to be conditionally excluded from LogicTests as well:

  • The default loadView method will fail — so the testLoadView method is #ifdef'd out.
  • Any attempt to alloc/init a UILabel will fail. I don't know why but because of this, all tests on the labels must be #ifdef'd.

Generally, don't expect anything from the UIKit framework to work in your Logic tests. Other frameworks will almost always work (in this application, the CoreLocation and Foundation frameworks work without issue).

Within UIKit, some elements will work — for example, UIWebView didn't have any issues in my testing. Exactly which user-interface objects will fail in the LogicTests bundle (without an actual running application) is never really clear until you try but these failures are why you still need to run the Application Tests to verify against — the Application Tests are a more authoritative result for anything in UIKit.

Conclusion

Download the complete WhereIsMyPhone-WithUnitTests.zip (85kb).

Warning: custom executables (like the DebugLogicTests executable) are user data in the project file. If you make changes that you want to share with someone else, you will need to rename the (your username).pbxuser file in the .xcodeproj bundle to default.pbxuser (so that it applies to all users).

This project includes code from the OCMock framework, which is Copyright (c) 2004-2009 by Mulle Kybernetik. OCMock is covered by its own license (contained in the OCUnit/License.txt file in the download).

The code for this post is largely been a repeat of last week's post for the benefit of iPhone programmers. You can have a look at the differences for yourself but I deliberately chose this project because the iPhone and Mac implentations are so similar.

I have taken the time though to show you how you can maintain a LogicTests target (in addition to the application-wide ApplicationTests target) that can test most of your project outside the application environment — including most of the user-interface controller code. The purpose is to sidestep the "device-only" limitations of the current OCUnit testing for the iPhone but also to show how you could set up build-time unit tests for part of your application, without necessarily applying this approach to your entire project.

A sample Mac application with complete unit tests

In this post, I present a complete Cocoa Mac application implemented with unit tests for all created code. I'll create the tests first and then only add the code required to make the tests pass, largely following a test-driven development (TDD) methodology.

Next week I'll show the configuration and implementation of this project as an iPhone application for the benefit of Cocoa Touch developers.

Introduction

A few days ago, I spent some time searching for a Cocoa application with source code and full unit tests but I was only able to find Apple's trivial iPhoneUnitTests sample project.

With more than four years of official support (Apple added OCUnit to the Developer Tools in 2005) and a large number of articles and blog posts by Mac programmers endorsing unit testing and offering solutions to problems within unit testing, it is surprising to me that there are so few code examples showing Cocoa applications with full unit tests.

So I decided to re-implement one of my own projects (the WhereIsMyMac application from an earlier post) with complete unit tests using a test-first approach — I'll create failing tests and only add code to the project to pass those failing tests.

I'll be cheating a bit relative to proper test-first development (since this is a re-implementation of an existing project, I already know what the final code should be) but I'll keep to the spirit of the process by only adding as much code as I need to make the tests pass.

I know people will want to see how this works on the iPhone but since this post is already large, I've deferred that implementation until next week.

Xcode unit testing targets

There are two different ways of configuring Xcode for unit testing: logic test targets and application test targets.

  • Logic tests — these are run in a executable that is separate from your application. The separate build can be easier to manage, faster to build and is easier to run objects in isolation since it avoids the application setup. However, you cannot test components which rely on the application (which is most user interface components). Generally, this type of test target is intended for libraries, frameworks and testing the back-end (model components of model-view-controller) of your application.

    Logic tests are easily run at build-time or from the command-line, which is helpful for continuous integration or automated test processes.

  • Application tests — these tests let the application load first and are subsequently loaded into the existing application. This means that the full application environment is available to your tests. In many cases, controller tests and view tests need to be run as application tests since they are reliant on the full environment.

    Application tests allow your application to be tested in a more realistic environment, reducing the chance that environment or integration level issues will be missed. They are normally run as a separate step (not as part of the build) and therefore may be less convenient for tests that need to be run every time.

I'll focus exclusively on the second type of testing target, since it will allow full testing of the application.

It may seem strange to talk about "unit tests" which are supposed to be run in isolation but then talk about "Application tests" which provide the environment in which to run tests. The reality is that you can only isolate user-interface unit tests from your own code — there will always be some interaction (in hidden and not controllable ways) with the application framework code. Windows, views and controls simply won't work if there's no application around them.

Mac project configuration

One of the best sources of information on configuring Xcode for Unit Tests is Chris Hanson's series on unit testing. I'll follow many of the steps that he describes and further add OCMock integration to the procedure.

After you've created a blank project, use the "Project→New Target..." menu item to add a new "Cocoa→Unit Testing Bundle" Target to the project.

Note: The testing target is a separate target. This means that you need to be careful of target membership. All application source files should be added to the application target only. Test code files should be added to the testing target only.

It'd be great if that's all that you needed but there's more.

First, drag your application's target onto the unit testing target to create a dependency (force the application to build before the unit tests).

Then edit the unit testing target's settings (Right click→Get Info) and set the "Build→Linking→Bundle Loader" for all configurations to:

$(CONFIGURATION_BUILD_DIR)/WhereIsMyMac.app/Contents/MacOS/WhereIsMyMac

where "WhereIsMyMac" is the name of the application you're unit testing. This will let the testing target link against the application (so you don't get linker errors when compiling).

Also set the "Build→Unit Testing→Test Host" to $(BUNDLE_LOADER) (this will give this property the same value as the above setting). This property lets the automated build-time script know to launch the application and inject the unit testing bundle into it to start the tests.

Logic tests note: If you want to do logic tests instead of application tests, leave the Bundle Loader and Test Host fields empty and add all files you want to test to the test target (so the test target becomes a separate, self-contained target instead of linking against the main application).

Finally, download a copy of OCMock, place the OCMock.framework in the same directory as your .xcodeproj file and set the "Build→Search Paths→Framework Search Paths" to:

  • "$(SDKROOT/Developer/Library/Frameworks"
  • "$(SRCROOT)"

Neither need to be recursive. The quotes are to handle potential spaces in your paths. The first search path will probably already exist in the settings but we add the second search path so that the OCUnit framework will be found in the project's directory.

The configuration so far will run all tests as a build step (the Cocoa Unit Testing target includes a Run Script build step that runs all the unit tests).

To allow debugging as well as build-time execution, add a new executable to the project ("Project→New Custom Executable...") with a path relative to the Build Product of:

WhereIsMyMac.app/Contents/MacOS/WhereIsMyMac

and in the custom application's settings (Right click→Get Info) on the Argument tab, set the arguments and environment variables as follows:

executable_settings.png

Most of these settings configure the application to load our test bundle into itself when it runs. The :$(SRCROOT) at the end of fallback framework path is to allow the application to find the OCUnit framework in our project directory at runtime.

Separate executable: since this test debugging executable is a separate executable, you will need to switch to the debugging executable for debugging tests and switch back when you want to run the application normally.

AppDelegate Tests

Startup integration

The application will start with the WhereIsMyMacAppDelegate and we'll use the the WhereIsMyMacAppDelegate to load the main window.

The first test is therefore to ensure that application's delegate is an instance of WhereIsMyMacAppDelegate on startup.

- (void)testAppDelegate
{
    id appDelegate = [[NSApplication sharedApplication] delegate];
    STAssertTrue([appDelegate isKindOfClass:[WhereIsMyMacAppDelegate class]],
        @"Cannot find the application delegate.");
}

This is a short and simple test but it isn't a unit test. It's actually an integration test (since it is testing the fully connected +[NSApplication sharedApplication] in place.

An ideal unit test would test the NSApplication in isolation and ensure that it creates and sets its delegate property correctly. This is infeasible since NSApplication can't be isolated so we simply accept the nature of the class and test the instance of sharedApplication that should be created on startup. Of course, unit testing NSApplication itself shouldn't be necessary but an integration test to ensure that the startup of the program leads correctly to the creation and setting of the WhereIsMyMacAppDelegate is still a good idea.

applicationDidFinishLaunching:

The next test is to ensure that applicationDidFinishLaunching: on the delegate will:

  1. create the WhereIsMyMacWindowController and set it on the delegate
  2. load the WhereIsMyMacWindowController's window
  3. make the WhereIsMyMacWindowController's window the main and key window
- (void)testApplicationDidFinishLaunching
{
    WhereIsMyMacAppDelegate *appDelegate =
        [[[WhereIsMyMacAppDelegate alloc] init] autorelease];

    id mockWindow = [OCMockObject mockForClass:[NSWindow class]];
    [[mockWindow expect] makeKeyAndOrderFront:appDelegate];

    mockWindowController = [OCMockObject mockForClass:[WhereIsMyMacWindowController class]];
    [[[mockWindowController expect] andReturn:mockWindow] window];
    NSUInteger preRetainCount = [mockWindowController retainCount];

    [appDelegate applicationDidFinishLaunching:nil];
    
    [mockWindowController verify];
    [mockWindow verify];
    
    NSUInteger postRetainCount = [mockWindowController retainCount];
    STAssertEquals(postRetainCount, preRetainCount + 1, @"Window controller not retained");

    id windowController;
    object_getInstanceVariable(appDelegate, "windowController", (void **)&windowController);
    STAssertEqualObjects(windowController, mockWindowController,
        @"windowController not set on appDelegate");

    object_setInstanceVariable(appDelegate, "windowController", nil);
    mockWindowController = nil;
}

This test is a near perfectly decoupled unit test of the -[WhereIsMyMacAppDelegate applicationDidFinishLaunching:] method. However, the approaches used to decouple it from the rest of the program probably make it tricky to understand.

The first two lines create a clean WhereIsMyMacAppDelegate to test.

The second two lines create a mock NSWindow that we use to check that the window is brought to the front by makeKeyAndOrderFront:. In conjunction with the later [mockWindow verify] this will test the 3rd requirement in the list above.

The next three lines create a WhereIsMyMacWindowController that will be swapped in place of any WhereIsMyMacWindowController that the appDelegate tries to create (more on how this works in the next paragraph). The mockWindowController is told to expect window to be invoked and when it does, will return the mockWindow. In conjunction with the [mockWindowController verify], the retain count checks and the STAssertEqualObjects will verify the first two requirements in the list.

I mentioned that mockWindowController will be substituted in place of a real WhereIsMyMacWindowController any time the appDelegate tries to create a WhereIsMyMacWindowController. This works because mockWindowController is a global variable that affects the following category:

id mockWindowController = nil;

@implementation WhereIsMyMacWindowController (WhereIsMyMacAppDelegateTests)

- (id)init
{
    if (mockWindowController)
    {
        [self release];
        return mockWindowController;
    }
    
    return invokeSupersequent();
}

@end

This category overrides the -[WhereIsMyMacWindowController init] method to return the mockWindowController if it exists, otherwise the default behavior. The invokeSupersequent() comes from my old Supersequent implementation post (it's like invoking the super method but will invoke the current class' base or earlier category implementation, not just a genuine super method and is more flexible — though slower — than method swizzling).

Of course, we only want this override to return the mock object at specific times. This is why the mockWindowController must be explicitly set back to nil at the end of the method.

Notice that the category overrides init, not alloc: technically, overriding alloc would prevent any method being invoked on WhereIsMyMacWindowController (making the test perfectly decoupled from other classes) however this would mean that we'd need to invoke init on the mock object and OCClassMockObject does not let you mock any methods that it implements for itself (this includes all of the NSProxy methods plus initWithClass: and mockedClass). So instead, we override init and make the acceptable tradeoff to allow +[WhereIsMyMacWindowController alloc] to be invoked.

A related point is that OCClassMockObject can't mock retain or release. This is why manual checks on the retainCount are used instead of asking the mock object to expect a retain. If you expect autorelease to be used instead of release, you'd need to wrap the tested method invocation in an NSAutoreleasePool to flush the autorelease before testing the retainCount.

A final point about this test: it uses object_getInstanceVariable and object_setInstanceVariable to get the windowController from the appDelegate instead of the property accessor. The reason for this is that object_getInstanceVariable directly reads the value from the object without invoking any accessor methods that might have secondary effects. Some code presented elsewhere uses valueForKey: to achieve the same effect but the problem with this is that valueForKey: will use the window getter method if it exists — we want to directly test that the actual instance variable is set on the class without interference.

applicationWillTerminate:

The final tests for the WhereIsMyMacAppDelegate are for the applicationWillTerminate: method. This method should close window and release it.

- (void)testApplicationWillTerminate
{
    WhereIsMyMacAppDelegate *appDelegate =
        [[[WhereIsMyMacAppDelegate alloc] init] autorelease];
    
    id mockWindowController = [OCMockObject mockForClass:[WhereIsMyMacWindowController class]];
    [mockWindowController retain];
    object_setInstanceVariable(appDelegate, "windowController", mockWindowController);
    
    NSUInteger preRetainCount = [mockWindowController retainCount];
    [[mockWindowController expect] close];

    [appDelegate applicationWillTerminate:nil];
    
    [mockWindowController verify];

    NSUInteger postRetainCount = [mockWindowController retainCount];
    STAssertEquals(postRetainCount, preRetainCount, @"Window controller not released");

    id windowController;
    object_getInstanceVariable(appDelegate, "windowController", (void **)&windowController);
    STAssertNil(windowController, @"Window controller property not set to nil");
}

After creating an appDelegate to test, this method creates a mock WhereIsMyMacWindowController, tells it to expect a close invocation, sets it as the windowController on the appDelegate to this new mock object, invokes applicationWillTerminate: on the appDelegate, verifies that the close method was invoked, ensures that the retainCount is decremented by one and that the windowController instance variable is set to nil.

That's three failing tests. One is more of an integration test than a unit test (since it relies on the NSApplcation operating in place) but the others are genuine isolated unit tests on the WhereIsMyMacAppDelegate. The implementation to pass these tests is less interesting but you can have a look at the sample project to see how I added code to pass these tests.

Window Controller Tests

loadWindow integration

As with the application delegate, the first test is an integration test. We need to test that the loadWindow method will load the required user interface elements from files in the bundle. This is similar to Chris Hanson's "Trust by verify" approach except that I test the loading of the window separately from the windowDidLoad and window methods.

- (void)testLoadWindow
{
    [windowController loadWindow];

    WebView *webView;
    object_getInstanceVariable(windowController, "webView", (void **)&webView);
    CLLocationManager *locationManager;
    object_getInstanceVariable(windowController, "locationManager", (void **)&locationManager);
    NSTextField *locationLabel;
    object_getInstanceVariable(windowController, "locationLabel", (void **)&locationLabel);
    NSTextField *accuracyLabel;
    object_getInstanceVariable(windowController, "accuracyLabel", (void **)&accuracyLabel);
    NSButton *openInBrowserButton;
    object_getInstanceVariable(windowController, "openInBrowserButton", (void **)&openInBrowserButton);
    
    STAssertTrue([windowController isWindowLoaded], @"Window failed to load");
    STAssertNotNil(webView, @"webView ivar not set on load");
    STAssertNotNil(locationLabel, @"locationLabel ivar not set on load");
    STAssertNotNil(accuracyLabel, @"accuracyLabel ivar not set on load");
    STAssertNotNil(openInBrowserButton, @"openInBrowserButton ivar not set on load");
    STAssertEqualObjects(windowController, [openInBrowserButton target],
        @"openInBrowserButton button doesn't target window controller");
    STAssertTrue([openInBrowserButton action] == @selector(openInDefaultBrowser:),
        @"openInBrowserButton button doesn't invoke openInDefaultBrowser:");
}

The windowController is a fixture that's allocated in the setUp method. The majority of this method then gets the instance variables from the windowController after the loadWindow method is invoked and tests that the required properties are all set.

We can now test the windowDidLoad method as a separate step to the loadWindow. The windowDidLoad should create a CLLocationManager object, set the window controller as the delegate and start location updates.

windowDidLoad
- (void)testWindowDidLoad
{
    mockLocationManager = [OCMockObject mockForClass:[CLLocationManager class]];
    [[mockLocationManager expect] setDelegate:windowController];
    [[mockLocationManager expect] startUpdatingLocation];

    [windowController windowDidLoad];

    [mockLocationManager verify];
    
    object_setInstanceVariable(windowController, "locationManager", nil);
    mockLocationManager = nil;
}

This uses the same category override approach that was used in the testApplicationDidFinishLaunching method to swap in a mock CLLocationManager and ensure that the appropriate methods are invoked.

locationManager:didUpdateToLocation:fromLocation:

Next step: verify that the correct Google Maps location is loaded in the WebView's mainFrame when a given coordinate is passed to the WhereIsMyMacWindowController implementation of the -[CLLocationManagerDelegate locationManager:didUpdateToLocation:fromLocation:] method.

This test method is pretty big but the essence is that:

  • The web view and its main frame are mocked.
  • The main frame is told to expect specific a specific HTML string that's loaded from a pre-created file.
  • The method is then passed the exact location that should trigger that HTML string.
  • The mock web frame is then asked to verify that the expected HTML string was loaded.
  • The window's labels are also tested to ensure they receive the correct values.
- (void)testUpdateToLocation
{
    NSString *htmlString =
        [NSString 
            stringWithContentsOfFile:
                [[NSBundle bundleWithIdentifier:@"com.yourcompany.UnitTests"]
                    pathForResource:@"WebPageTestContent" ofType:@"html"]
            encoding:NSUTF8StringEncoding
            error:NULL];
    id mockWebView = [OCMockObject mockForClass:[WebView class]];
    id mockWebFrame = [OCMockObject mockForClass:[WebFrame class]];
    [[[mockWebView stub] andReturn:mockWebFrame] mainFrame];
    [[mockWebFrame expect]
        loadHTMLString:htmlString
        baseURL:nil];
    object_setInstanceVariable(windowController, "webView", mockWebView);

    NSTextField *locationLabel = [[[NSTextField alloc] init] autorelease];
    NSTextField *accuracyLabel = [[[NSTextField alloc] init] autorelease];
    object_setInstanceVariable(windowController, "locationLabel", locationLabel);
    object_setInstanceVariable(windowController, "accuracyLabel", accuracyLabel);

    CLLocationCoordinate2D coord;
    coord.longitude = 144.96326388;
    coord.latitude = -37.80996889;
    CLLocation *location =
        [[[CLLocation alloc]
            initWithCoordinate:coord
            altitude:0
            horizontalAccuracy:kCLLocationAccuracyBest
            verticalAccuracy:kCLLocationAccuracyHundredMeters
            timestamp:[NSDate date]]
        autorelease];
    
    [windowController
        locationManager:nil
        didUpdateToLocation:location
        fromLocation:nil];
    [windowController
        locationManager:nil
        didUpdateToLocation:location
        fromLocation:location];
    
    [mockWebFrame verify];
    
    STAssertEqualObjects(
        ([locationLabel stringValue]),
        ([NSString stringWithFormat:@"%f, %f", coord.latitude, coord.longitude]),
        @"Location label not set.");
    STAssertEqualObjects(
        ([accuracyLabel stringValue]),
        ([NSString stringWithFormat:@"%f", kCLLocationAccuracyBest]),
        @"Location label not set.");
}

An interesting point to notice here is that the WebPageTestContent file is loaded from a bundle that isn't the mainBundle. This is because the test resides in the UnitTests.octest bundle, not the application's bundle (which is the "main" bundle).

locationManager:didFailWithError: and openInDefaultBrowser:

I'll leave out the code for the locationManager:didFailWithError: test — it's largely the same as the previous test but with a different HTML string to load in the web view's main frame. You can see it in the downloaded project if you wish.

Similarly, openInDefaultBrowser: is just a URL, generated from the current location and sent to the shared NSWorkspace (which we mock through category overrides) so the test contains largely the same elements — so I'll omit the code for this test too. Again, check the downloaded project if you're interested.

dealloc:

All that remains is to create tests for the dealloc method to ensure that the locationManager receives a stopUpdatingLocation and a release message.

- (void)testDealloc
{
    id mockLocationManager = [OCMockObject mockForClass:[CLLocationManager class]];
    NSUInteger preRetainCount = [mockLocationManager retainCount];
    [mockLocationManager retain];
    object_setInstanceVariable(windowController, "locationManager", mockLocationManager);
    
    [[mockLocationManager expect] stopUpdatingLocation];

    [windowController dealloc];
    
    [mockLocationManager verify];

    NSUInteger postRetainCount = [mockLocationManager retainCount];
    STAssertEquals(postRetainCount, preRetainCount, @"Location manager not released");
    
    windowController = nil;
}

Notice that we have to set windowController to nil at the end. This is because invoking dealloc on the windowController fixture has deallocated it and we need to ensure that the tearDown method doesn't try to invoke release on it.

Six failing tests for the WhereIsMyMacWindowController. You can look at the downloadable project to see the code added to pass these tests.

Conclusion

Download the complete WhereIsMyMac-WithUnitTests.zip (139kb).

Warning: Custom executables (like the UnitTestWhereIsMyMac discussed in this post) are part of user data in the project file. If you make changes that you want to share with someone else, you will need to rename the (your username).pbxuser file in the .xcodeproj bundle to default.pbxuser (so that it applies to all users).

This project includes OCMock.framework, which is Copyright (c) 2004-2009 by Mulle Kybernetik. OCMock is covered by its own license (contained in the OCMock.framework/Versions/A/Resources/License.txt file).

In this post, I created a Mac project, created unit tests for all required functionality (plus two integration tests) and ultimately added code to make the project pass those tests. I've shown the configuration required for unit testing targets in Xcode and the implementation of the tests themselves, which show how to isolate units of a program (using mock objects and category overrides) for properly decoupled unit testing.

Unit tests for all created code does not mean that the project is "fully tested". There are lots of aspects associated with integration, runtime and memory behaviors, user events and the behaviors of the Cocoa frameworks that are not covered by these tests. Of course, this is the limit of "unit testing" for classes that operate in an application framework — to test these other aspects requires different kinds of tests.

Next week, I'll be presenting the same post developed for the iPhone. I apologize that this will result in two posts that almost the same but there are enough differences between the two that I couldn't squeeze both into a reasonable sized post. I also hope that by separating the posts, the result will be easier for dedicated Mac or iPhone programmers to follow.

Finally, a disclaimer: please don't consider the existence of this post as a recommendation that you should necessarily write your applications with full unit tests. It is important to look at the work involved and weigh the associated time costs and against your project's need for unit level validation. There are alternative approaches for maintaining code quality that have different associated costs and benefits which I hope to discuss and compare in a later post.

Multiple copy buffers, cursor and tab key tricks in Xcode

This week, I'll show you a few keyboard related tricks for editing in Xcode, including setting up multiple copy-and-paste buffers by adjusting the key mappings, switching tabs on or off for autocomplete and other text editing quirks in Xcode like understanding why the "End" key doesn't move to the end of the line on the Mac.

Key bindings, the cursor and Emacs

Standard text editing on the Mac obeys many more keyboard commands than most casual users realize. This is because the key bindings on the Mac derive from NeXTStep which chose to implement (where possible) a large number of the Emacs key bindings.

First developed in the 1970's, Emacs was designed for keyboards that lacked cursor and forward delete keys and computers that lacked a mouse. And, like its spiritual enemy in the world of text editing, Vi, Emacs was never a program to update its key bindings to stay current.

What this means is that text editing applications that use the standard Cocoa text editor — like TextEdit.app — and other more sophisticated text editors that support the operating system's default key bindings — like Xcode — support most of the Emacs key controls for navigating and editing — like Ctrl-F and Ctrl-B for forward and backward by one character or Ctrl-A and Ctrl-E for jump to the start or end of the paragraph.

Of course, most of this is pointless to ordinary users since the standard cursor and home keys will do this in a far less cryptic way.

For those of you who still scream at Xcode when the Home key doesn't move to the start of the line or get annoyed that the cursor doesn't move when using PageUp/PageDown, here's a quick refresher:

Key or combinationAction
cursor up
cursor down
cursor left
cursor right
Command ←cursor to start of line
Command →cursor to end of line
Option ←cursor to start of previous word
Option →cursor to end of previous word
Control ←cursor to start of previous camel-case component (Xcode only — otherwise it works like Command ←)
Control →cursor to end of previous camel-case component (Xcode only — otherwise it works like Command →)
PageUp or Control ↑scroll up one page, without moving the cursor
PageDown or Control ↓scroll down one page, without moving the cursor
Homescroll to top of document, without moving the cursor
Endscroll to end of document, without moving the cursor
Option-PageUpscroll up one page, moving cursor to center of new location
Option-PageDownscroll down one page, moving cursor to center of new location
Command ↑move cursor to start of document
Command ↓move cursor to end of document
Option ↑move cursor to start of the paragraph (except in Xcode where it does nothing)
Option ↓move cursor to end of the paragraph (except in Xcode where it does nothing)

Due to the importance of the Home/End/PageUp/PageDown keys in text editing, I have no respect for Apple's choice to start shipping keyboards without these keys by default on new computers.

I wish there was a little more sense to these bindings. There almost are a few rules — like Control, Option and Command represent small, medium and largest increments in any direction and combinations involving the cursor keys move the cursor but Home/End/PageUp/PageDown combinations just scroll without moving the cursor — but sadly there are enough violations of these rules that you must simple learn them all by rote.

The ability to scroll the document without moving the cursor when desired on the Mac is an ability that I sorely miss when I'm stuck using Visual Studio or other development environments — in these other environment, my cursor always gets stranded when I quickly glance at another point in the document, whereas on the Mac I can just tap a left or right arrow to return to where I was.

Kill and yank

A side effect of bringing Emacs key bindings into Xcode though is that a few Emacs commands that don't have common Macintosh equivalents are brought in as well.

This includes kill and yank. Kill and yank are Ctrl-K and Ctrl-Y by default.

In most respects, kill and yank work like cut and paste. Kill something and it gets stored in your kill buffer, yank and the contents of your kill buffer are inserted at the current insertion point. If you have no selection, kill will delete from the cursor to the end of the line, otherwise it will only delete the selection. There are a few differences compared to cut-and-paste: you can't yank between applications (each application has its own, non-shared kill ring) and they store pure text only (no styled text, images or other copyable data).

In their default setup, kill and yank are like a second-rate extra cut-and-paste option. But correctly configured, you can have as many separate cut buffers as you want.

Setting up kill-and-yank

The first configuration change you need to make is to set up a binding for yankAndSelect: instead of ordinary yank:. We do this because yank: will only ever insert the most recently killed text whereas yankAndSelect: allows you to cycle through all stored buffers. You can do this in the "Xcode Preferences→Key Bindings→Text Key Bindings" but I prefer to make the binding System-wide by using the "/Developer/Applications/Utilities/Property List Editor.app" to create a file at "~/Library/KeyBindings/DefaultKeyBinding.dict" that overrides the yank: command with a yankAndSelect: command.

yankAndSelectBinding.png

The key string is actually caret-y, not Ctrl-Y. You may need to create the KeyBindings
directory in your Library directory as it is not there by default.

Both yank: and yankAndSelect: are standard commands on NSReponder, so this just exchanges one for another. In a totally bizarre twist of documentation, yankAndSelect: is an undocumented method (it's not in the Cocoa.h headers) that appears in the documentation where you are encouraged to used it as required.

The other step to make multiple kill buffers work is to specify how many buffers you want. By default, there is 1 but we want multiple buffers.

To do this, just add the key NSTextKillRingSize with a string or number value greater than 1 to any .plist file. For example, if you only want this behavior added to Xcode, add the line to the Xcode .plist file.

I like to add this like to my Global Preferences. To do this, type:

open ~/Library/Preferences/.GlobalPreferences.plist

in a Terminal window (the file is hidden by default in the Finder so you need to open it like this unless you've turned the display of hidden files on).

killbuffers.png

The NSTextKillRingSize setting in my .GlobalPreferences.plist file.

With these changes, you can use Control-K to copy something to a kill buffer and then Control-Y to cycle through the last 3 items stored in your kill buffers (the collection of kill buffers is called the "kill ring"). You can use any number or kill buffers but large numbers may make cycling through the complete set harder (since you can only cycle through in one direction).

The Tab key binding in Xcode

While I'm talking about key bindings, one of the biggest shocks in Xcode 3.2 for me was the fact that the Tab key is configured by default to select the next Code Sense Completion, instead of inserting a Tab character.

This meant that if I had code like this:

autocomplete.png

and I hit the Tab key to indent the second line, it wouldn't indent — instead the autocomplete field would be selected.

I found this particularly frustrating since I normally use Ctrl-/ to select autocomplete fields, making the Tab key redundant as well as interfering.

Fortunately, Xcode supports a "Insert Tab without Extra Action" that is normally bound to Option-Tab which will only ever insert a Tab character (will never perform other UI actions). By exchanging the key bindings between "Insert Tab" and "Insert Tab without Extra Action", my complaint was solved.

On the topic of search and replace: a change to menus in Xcode 3.2 means that single file Search and Replace no longer appears in the menu (unless you're holding down the Control key, since its binding is now Ctrl-Command-F and "Ctrl" variants of menu items aren't shown unless "Ctrl" is held). This can make search and replace hard to find in Xcode 3.2. Another feature that has become harder to find are the search options — the little magnifying glass on the search field hides a popup menu that contains the essential options like "Ignore case".

A sideeffect of changing the tab bindings is that all text fields in the user interface — including the Search and Replace text fields — will change their behavior when the Tab key is pressed. Once the bindings are changed, Tab will no longer move the input focus from the search field to the replace field — it will instead insert the tab character into the selected field. This means that to jump from the Search field to the Replace field you will need to type Option-Tab. Of course, the ability to easily enter tab characters in the search field has its advantages too.

Conclusion

Text editing on the Mac is loaded with features that most people never use. While there's often no need to use many of the extra features, extra navigation options or extra kill-and-yank buffers can be useful additions.

The .GlobalPreferences.plist and global DefaultKeyBinding.dict files used to alter global key mappings provide a surprising amount of flexibility to applications that respect their content, like Xcode. You can use them to remap most key combinations to other commands if you choose. A corollary though: you should try to respect these dictionaries if writing your own custom text editor.

The design of an iPhone application

In this post, I'll discuss iPhone program design using the example of a small but non-trivial iPhone application to provide examples of how the design is implemented. The design includes: how to manage data coming from different sources, how to manage multiple views of that data and how to design your program so it remains simple and manageable as it grows. In short, I'll be discussing how Model-View-Controller (MVC) applies to an iPhone application but I'll also discuss how even simple programs are considerably more hierarchic through their controllers and branched through their models and views than the basic description "Model-View-Controller" might imply.

The sample program

The sample program for this post takes the Core Data SQL database of Australian Postcodes that I created last week from a CSV file and uses that in an iPhone application that allows you to:

  • Browse and search the database.
  • Display entries in a MKMapView.
  • Find the nearest entry to the user's GPS and display that on a map.

The following screenshots show the basic flow of the application.

IMG_0018.PNG IMG_0020.PNG IMG_0021.PNG

The "Show current location" step skips the middle screenshot in the workflow and goes straight to the map, displaying the closest "Postcode" in the database for the user's location, or just the user's location (with no pins in the map) if they are more than 0.1 longitude or latitude away from the nearest post office.

You can download the complete project AustralianPostcodes.zip (961kB).

Steps in designing a program

The basic steps in designing a small user program are:

  1. Decide what the program will display to the user.
  2. Work out how the user will navigate through the program to reach each view.
  3. Work out the data that is needed to populate the views that the user will navigate.
  4. Decide where that data will come from and how you will manage access to it.
  5. Decide how and when you will construct your views.
  6. Decide how you will provide your views with data.

The first two points are the program's requirements and the remainder is the high level design. For this post, I'm going to consider the first two points complete (as given in the previous section).

I'll discuss steps 3 and 4 as a single concept "Program data", then steps 5 and 6 as "Program construction".

Program data

While it might not be obvious, this program actually has 5 different sources of data:

  • Postcodes and their related data
  • The GPS location
  • The cache of fetched postcodes, filtered by search terms
  • The cache of postcodes nearest the selected location or GPS location
  • List of menu items on the top-level page

The last item doesn't need to be data (it could easily be done in code) but I wanted to talk about a range of different data sources and implementing menus like this from data can dramatically reduce the size and complexity of your code (see my earlier post Simplifying your code using NSDictionary).

Postcode data and the PostcodeController

Obviously in this program, the Core Data SQL database is the source of the postcode data. However, there is more to clarifying the data source than that.

The postcode data is used from multiple views in the application. For this reason, it will need to live in its own persistent location that views can access when needed. To handle its creation, persistence and access, we'll need some form of controller to manage the lifecycle of this data and access to it.

Core Data runs in an NSManagedObjectContext. In some respects, you may consider that the NSManagedObjectContext manages access to the data. While this is true at the low level of reading, writing, cacheing and fetching, the reality is that NSManagedObjectContext is not a manager in a broader sense for your program and your program specific logic.

Specifically, your program will need:

  • A way to construct the Core Data persistence stack (i.e. open a Core Data SQL store and create an NSManagedObjectContext for it).
  • A way to access the current context from anywhere in the program.
  • A place to put context processing code (if needed). This might include importing/exporting code, specialized fetching code, editing and validating code.

While this iPhone application doesn't need the last point, the first two are necessary.

In the default Xcode template for an iPhone application using Core Data, the NSManagedObjectContext is constructed in the AppDelegate object and from there is pushed onto the RootViewController so that it can be used there.

Simply put: I dislike this approach because it gives the AppDelegate multiple responsibilities unrelated to its primary purpose.

The AppDelegate exists to implement functionality during key points in the UIApplication's lifecycle (most importantly startup and shutdown). The AppDelegate doesn't use the NSManagedObjectContext for itself and the AppDelegate's primary responsibility is not document management — you should not be using your AppDelegate as your application's document manager.

Every class should have a single purpose
Every piece of functionality that a class exposes to the rest of the program (i.e. functionality that is part of the class' external interface) should be obviously part of that class' primary role in the program.

For this reason, I create a class (in this project it is named PostcodesController) whose responsibility it is to construct the document (our NSManagedObjectContext) and handle access to it or process the document if needed.

The PostcodesController will be a singleton, as most document managers are (see the NSDocumentController in Mac OS X). Since the application only has one set of Postcode data, there is no need to select the "current" document or manage a set of Core Data persistence stacks but this class is where that behavior would be added if needed in future.

The PostcodesController is then acccessed by the PostcodesViewController to get the current context. However, the data this context contains is not used directly; it is cached by the NSFetchedResultsController as a separate set of data and from there it is used to populate the table view.

Map data

Map data in the application consists of three parts:

  • The map "tiles" (the street map shown in the view)
  • The "current location" (which is either the result of a selection or the GPS location).
  • The array of pins displayed in the map

The actual map tiles are loaded by the MKMapView that Apple's MapKit provides. We don't really need to worry about that.

The selected location or GPS location is a little trickier. This either comes from the user's selection on a previous screen or from the CLLocationManager (which supplies us with location data from the GPS). If this was used by multiple views in the application, then we would need a special class to manage the current location for the whole application (like the PostcodeController manages the postcode database for the whole application) however, we only need location data on the map screen, so the MapViewController which manages this screen can handle this.

The array of pins displayed on the map is really just a selection of the data from the postcodes database, selected using criteria from the current location. Again, since this is only used on the map screen, so it can be controlled by the MapViewController as long as the difficulty of doing so remains low.

List of menu items

Sometimes, data is so simple to load and so customized to the location where it will be used that managing it is no concern at all. The array of dictionaries that provides the structure for the "Main Menu" in the program is a good example of this — the data can be read in a single instruction, its format is written to match the format that the "Main Menu" wants, it has no state to maintain and doesn't require any editing.

Program construction

This program started with a "Navigation-based application" template in Xcode and the project name "AustralianPostcodes". This means that the following steps are setup by the template:

  1. The UIApplication will load the MainWindow.xib on startup
  2. The MainWindow.xib will construct the AustralianPostcodesAppDelegate, a UIWindow and a UINavigationController which will load the RootViewController from RootViewController.xib and set it as the top level view in the navigation hierarchy.
  3. The AustralianPostcoddesAppDelegate will insert the UINavigationController's view into the UIWindow and display the window.

The primary controllers in the program are the PostcodesController (which controls the construction of the Core Data persistence stack), the RootViewController (which shows the main menu), the PostcodesViewController (which displays the table of objects fetched from Core Data) and the MapViewController (which displays a single Postcode object and the map view).

Constructing the RootViewController

The RootViewController needs to display its list of options in the table. When any row in the table is tapped, a new UIViewController subclass will need to be constructed and pushed onto the UINavigationController.

Due to the data-configured nature of the RootViewController, the subclass of UIViewController constructed and any parameters passed into its initializer are all specified in the data file.

This means that this class is incredibly simple. The three different rows perform three different tasks:

  1. Load a PostcodesViewController and sort by postcode
  2. Load a PostcodesViewController and sort by suburb
  3. Load a MapViewController and use the GPS for location data

But the differences come from the data. The work done by the RootViewController is the same in each case.

Constructing the PostcodesViewController

This class displays the postcode data but it has to perform four tasks as part of that job:

  1. Fetch the sorted (and possibly filtered) postcodes from the database
  2. Construct/configure the table view cells to display the fetched results in the table
  3. Respond to changes in the search string by refetching the data
  4. Respond to taps on a row by loading a MapViewController to display the map

In this case, much of the first step is handled by a standard NSFetchResultsController. This class is designed to work closely with the UITableView so it makes sense to keep this work close to the view. If we weren't using an approach so closely tied to the view, it would also be possible to put a fetch method in the PostcodesController and pass parameters to it so that it prepared the data in an appropriate format.

When a row is selected, the Postcode object (the subclass of NSManagedObject associated with the selected row) is passed into a newly constructed MapViewController.

Constructing the MapViewController

The MapViewController needs to perform the following tasks:

  1. If not given a selected Postcode object, then the Postcode in the database nearest the GPS location must be fetched.
  2. Postcodes around the current location must be fetched and displayed on the map
  3. The map view must be centered on the selected postcode
  4. Details about the selected postcode must be displayed in the UILabels at the top of the screen.

To enable the easy display of postcodes as points on the map, the Postcode class (the subclass of NSManagedObject used by the Postcode entity in the Core Data model) implements the MKAnnotation protocol. This means that the Postcode objects returned from a fetch can be immediately added to the MKMapView.

This class needs to perform its own fetching from the Core Data database. Since the PostcodesViewController and MapViewController both need to fetch (albeit in slightly different ways) and they both use a significant volume of code of to do this fetching, it is possible that a common interface to perform fetch actions for both would be a future improvement.

Always be ready to iteratively refactor
As you implement a program, you should always be on the lookout for easy ways to simplify your program by implementing minor redesigns. Looking for multiple places where your program repeats the same functionality is the most prominent example of this. A corollary to this is that you should never copy and paste blocks of code — a copy and pasted block of code should be a single method/function/macro that you simply invoke from multiple places.

Model-View-Controller

Interpreting this program according to model-view-controller would go like this:

  • Model — the Postcode objects in the NSManagedObjectContext are the model.
  • View — the UITableViewCell objects on the "Main Menu", "By suburb" and "By postcode" screens and the MKMapView on the map screen.
  • Controller — the AustralianPostcodesAppDelegate, RootViewController, PostcodesViewController, MapViewController, PostcodesController.
Why do we need so many controllers?

An interesting point to note is that every class in the project (with the exception of Postcode) is a controller class.

Why do we need so many controllers? The answer is that we have a normal number of controllers — the correct analysis is that we have is an absence of custom model and view objects.

A well written model object or view object needs no custom code for customization — the construction provided by the controller and the data (provided from configuration files on construction or from model objects when loaded) is all the customization that they require.

I've written posts about drawing customizing table views without subclassing any views — the customization comes from how the view is constructed and set up.

On the model side, NSManagedObject is configured by the .xcdatamodel file and can be used without further configuration if desired. Similarly, NSDictionary is used by the RootViewController to hold custom structure data without need for custom behavior.

Obviously you will probably need custom model and view objects at some point but the fewer you have, the easier things will be.

A simple diagram of the program
basicMVC.png

Generally though, I don't like this type of over-simplification. My problem is that while it does describe a single Model-View-Controller path through the program, this program has multiple sources of data and each source of data is managed at a different point and displayed in a different way.

A different way to think about application design

Despite the simplistic diagram shown above, the reality is that we have the following model-controller relationships in the program:

  • The Postcodes in the NSManagedObjectContext, controlled by PostcodesController
  • The Plist which describes the Main Menu, controlled by RootViewController
  • The cached fetch of Postcodes by suburb or postcodes, controlled by NSFetchedResultsController in conjunction with PostcodesViewController
  • The cached fetch of nearest Postcodes, controlled by MapViewController
  • The GPS location, controlled by CLLocationManager

So a full diagram of the data paths through the program would be considerably more complex than the diagram shown above.

An important point to note about the above diagram is that there are two layers of controllers: one layer that controls the model at the top and one layer that controls the views at the bottom. The result of this observation is that the view-and-controller pairs and the the model-and-controller pairs are really two instances of the same design pattern. This means that we can add the following view-controller relationships to the list of model-controller relationships shown above:

  • The UITableViewCells in the UITableViews, controlled by PostcodesViewController and RootViewController
  • The UILabels and the MKMapView, controlled by the MapViewController
Module-controller

What this does is to break the entire program down into pairings of:

  • Class that "does" something (store data, display data, fetch data)
  • and Controller which loads and configures it

Your program is then a hierarchy of controllers (UIApplication/AppDelegate, Document controller, UIViewController subclasses) which load and configure their own modules (.xib files and windows, Core Data, UIView classes) to perform their actual work — but these modules have no connections to the rest of the program, they rely entirely on the controller to set them up, provide their data and make them work.

As your program grows larger and more complex, your controllers may have sub-controllers — in fact, your module objects may become controllers to their own sub-module objects. Through this structure, you can have a giant program but each element is still simple within itself. Simplicity remains as the program grows because every individual element is self-contained.

Implications of this way of thinking
The best programs are highly modular and decoupled
A good application has tightly focussed modules which are totally decoupled from the rest of the program and controllers which are lightly coupled to other controllers in a hierarchical arrangement through the program but do nothing other than provide the contextual information for their controlled objects.

The idea is to make all aspects of your program clean, decoupled and resusable, in the same way that good view and model classes are.

I prefer this way of thinking about application design. In essence, treat every data object, every network connection and every view object in the program (any class which "does" something) in the same way: like its own independent module. These independent module classes should:

  • have their own controller which loads and configures them
  • not have a connection (pointer or reference) to any other part of the program (except other objects within the module)

If you feel like one of these module objects needs to access another part of the program, try to redesign so that the controller pushes the information to the modular class, instead of the module object fetching for itself.

The corollary to this is that your controller objects should, as much as possible, avoid "doing" anything except constructing and configuring these document-like objects. Controllers may have a lot of code compared to the module classes but that code should always be responding to a change from a module object and passing a message to another module or constructing another module. Controllers are all about setup, reactions and inter-module communication.

Most of the time, the module objects controlled by the controller will not be your own special subclasses — often they're just default NSDictionary objects or UIViews or NSManagedObjects — but it is important to keep the module object and controller relationship in mind to help guide how you write your program.

Keep the main advantage of Model-View-Controller thinking

Even if you do start thinking about every component in your program in module-controller terms, you shouldn't drop the key advantage of Model-View-Controller — to focus your application around the workflow of your "model".

Specifically: you should always know what the "model" of your program is. All aspects of your program should be directed towards showing, editing, displaying that data. Even if lots of other modules, tasks and activities are implemented as part of the overall process, they should not disrupt the model's workflow.

Conclusion

You can download the complete project AustralianPostcodes.zip (961kB).

I wrote a lot of code for this post but I've included none of it here. The code exists to make the abstract discussion about how to design a program seem less abstract — I hope that you can look at the code and understand the sometimes vague statements I've made in this post.

The first half of the post was about identifying the data your program has and then identifying how to build the controllers in your program to load and display this data. The purpose of going through these steps is to explain that these are the details you must have clear in your mind before you start writing code. Programming requires — above all else — that you be able to absolutely and unambiguously clarify your idea.

The second half of the post was about module-controller relationships. The purpose here is not to say Model-View-Controller is wrong but to point out that the purpose of Model-View-Controller is decoupled, reusable modules with all construction and context provided by hierarchy of lightly coupled controller objects running through the spine of your program — and that this is a pattern that can be applied repeatedly (and even recursively) throughout your program, not just to views or your main model.

There's a Garbage Collection ninja hiding in the project templates

Not all Xcode project templates are alike. Especially the Core Data Command Line Tool...

An unwanted feature jumped out and attacked me!

In yesterday's post on implementing a CSV parser, I briefly mentioned that the parser took around 0.28 seconds to parse the test data that I provided. I consider this a good time for the parser to take but my first run was not so quick.

The first timing test I ran gave a parsing time of 1.15 seconds. This was slightly shocking to me, since I had already run a "control" test — where I just used componentsSeparatedByString: to break into lines and then into fields — that took just 0.95 seconds.

Had I really spent all that time writing code that was 20% slower (albeit more functional) than a clumsy, brute force approach?

No. As it turns out, the Xcode Project Template I had used for the test project, the:

    Mac OS X→Application→Command Line Tool→Type:Core Data

project has Garbage Collection enabled by default.

The only real hint that this template is different is the objc_startCollectorThread() line in the default main() function — of course, I never saw this line because I always replace the source files in the templates with my own versions that follow my own formatting style.

Anyway, I turned Garbage Collection off and... boom 4 times faster.

I'd ask for more warning about these major — but subtle — changes to templates in the future but you know... ninjas, what can you do?