labs

Getting Started on Your First iOS App – Part II

This blog was co-authored by Yulia Tolskaya. The full source can be found here.

In our last post, we covered the steps to bootstrap a Cedar-tested iPhone app. In this post, we’ll add some basic features (like buttons!) to the app.

The feature we want to start today is the ability to create and save a named scoresheet. We’re thinking a button on the main page with a list of saved scoresheets. Tapping the button would take us to a page where we can type in a name. Since we have Cedar, we’re going to TDD the whole thing. Hopefully this post and the next post will help teach the basics of navigation and models in relation to unit tests in iOS/Cedar. So, let’s get started!

Navigation

When thinking about the flow of this app, we really wanted to be able to do the swipe-left-to-navigate-back action. This flow is achieved by the UINavigationController which acts like a stack of views that you can push and pop by tapping into views and swiping right out of them. The UINavigationController hangs off the AppDelegate and is assigned a top view. You can further push views onto it and pop views off to navigate with history. Pretty spiffy stuff. Before implementing let’s write some tests.

#import "AppDelegate.h"
#import "RootViewController.h"

using namespace Cedar::Matchers;
using namespace Cedar::Doubles;

SPEC_BEGIN(AppDelegateSpec)

describe(@"AppDelegate", ^{
    __block AppDelegate *subject;

    beforeEach(^{
        subject = [[AppDelegate alloc] init]; //stickshift

    });

    describe(@"after the app finishes launching", ^{
        beforeEach(^{
            [subject application:nil didFinishLaunchingWithOptions:nil];
        });

        it(@"should display the rootViewCotroller in a navigation controller", ^{
            subject.window.rootViewController should be_instance_of([UINavigationController class]);
            UINavigationController* rootViewController = (id)subject.window.rootViewController;

            rootViewController.topViewController should be_instance_of([RootViewController class]);
        });
    });
});

SPEC_END

In the above test, we touched up the previous assertion to now check if AppDelegate is defaulting to our UINavigationController and that the previously created RootViewController is the first view for navigation. For those of you coming from Rails or other dynamically typed languages, behold the wonders of casting and pointers. There are probably other blogs out there to help grok the idea.

Running the specs expectedly fails. Switching over to AppDelegate, we now create a nav controller with the root view sitting snuggly inside. This should make the specs pass, but still makes for a rather unimpressive app.

#import "AppDelegate.h"
#import "RootViewController.h"

@implementation AppDelegate

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    self.window = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];

    RootViewController* homeViewController = [[RootViewController alloc] init];
    self.window.rootViewController = [[UINavigationController alloc] initWithRootViewController:homeViewController];

    self.window.backgroundColor = [UIColor whiteColor];
    [self.window makeKeyAndVisible];
    return YES;
}

@end

So we have our navigation controller, and we have our front landing page. Time to add a button (!). As per Can, we’re going to be taking the horribly easy route for creating a button by using the XCode-built-in Interface Builder. In the Project Navigator (left tab, folder icon), open up your RootViewController.xib. If you don’t have the Assistant editor open, go ahead and open it (look for the tuxedo in the top right corner). Open up your RootViewController.m in the right pane so that you have the wireframe on the left and your code on the right.

In the far right window there should be six tabs; click on the Attributes inspector (looks like a slider handle). There should be a drop down called Top Bar. Select Translucent Navigation Bar from the drop down to simulate this view being in a Navigation Controller’s domain. You should see a horizontal line drawn across your wireframe. Now drag a button from the bottom right onto the screen. Rename your button by double clicking it. At this point, your setup should look like this:

08_XibSetup

Now get ready for some magic! Right click and drag the button in your interface builder into your RootViewController.m file under your existing function. You should see a blue line draw across the two windows and when you lift the mouse button a menu should open that looks something like this:

09_ActionWizard

Name it something descriptive of the event we want to wire up. In our case, we’re wanting a Touch Up Inside event (when your finger leaves the button) to create a new view. Click connect and you should get a magically wired function, like so:

#import "RootViewController.h"

@interface RootViewController ()

@end

@implementation RootViewController

- (void)viewDidLoad
{
    [super viewDidLoad];
    // Do any additional setup after loading the view from its nib.
}

- (IBAction)touchUpCreateNewScoresheet:(id)sender {
}

@end

In the same right-click-and-drag motion, drag the button to your RootViewController header file to create a variable, which we’ve named *createNewScoresheetButton. With the variable in the header file, we’ve essentially declared our createNewScoresheetButton as a public member to the RootViewController. As an important side note (that hosed us): if you decide to delete the automagically-generated code you may have a problem running your code. To see what we mean, delete the code you just made, right click and drag a new method and name it slightly differently (this is what we did to find this out). Running the specs now, you should see something like this:

EXCEPTION RootViewController after Create New Scoresheet is tapped should display the create new scoresheet controller
-[RootViewController tapThisThing:]: unrecognized selector sent to instance 0xc013e60
(null)

This is due to the binding in the XIB file still existing, but the correlated code in your source file being missing. You’ve now simulated clicking the button and the callbacks get fired, but that old callback is now deleted. To fix this, open up your XIB file, click on the button and access the Connections inspector (right pane, far right button that looks like an arrow). You should see two bindings to your event:

10_MultipleBindings

Click the little x beside the binding you’ve deleted and the world should be right again. You can thank us later for saving you half a day (we accept cookies or pie as thanks).

Time to write some specs to define the behavior a little. We want to write a spec for RootViewController driving out the tap-to-create-a-scoresheet behavior. From the past example, we created an empty RootViewController with no spec file; create a RootViewControllerSpec file now (go ahead and look at the last article, we had to). In our spec, we’ll be creating the RootViewController as the root view to a UINavigationController, like we do in AppDelegate. To simulate tapping on our button, we’ll have to access the button from the RootViewController and send the tap to it. Lastly we’ll assert that the topViewController of the navigation controller has swapped to our newly created CreateScoresheetViewController.

Spec:

#import "RootViewController.h"
#import "CreateScoresheetViewController.h"

using namespace Cedar::Matchers;
using namespace Cedar::Doubles;

SPEC_BEGIN(RootViewControllerSpec)

describe(@"RootViewController", ^{
    __block UINavigationController *navController;
    __block RootViewController *subject;

    beforeEach(^{
        subject = [[RootViewController alloc] init];
        navController = [[UINavigationController alloc] initWithRootViewController:subject];
    });

    describe(@"after Create New Scoresheet is tapped", ^{
        beforeEach(^{
            [subject.createNewScoresheetButton sendActionsForControlEvents:UIControlEventTouchUpInside];
        });
        it(@"should display the create new scoresheet controller", ^{
            navController.topViewController should be_instance_of([CreateScoresheetViewController class]);
        });
    });
});

SPEC_END

To get things to compile, we need to create the CreateScoresheetViewController class. This can be empty for now. Running the specs we get a failure:

FAILURE RootViewController after Create New Scoresheet is tapped should display the create new scoresheet controller
/path/to/workspace/BlogScoreKeeper/UISpecs/RootViewControllerSpec.mm:24 Expected <<RootViewController: 0x8d28430> (RootViewController)> to be an instance of class <CreateScoresheetViewController>

Hmm? Apparently iOS does some nifty optimization things automagically such as not allocating any views until the views are accessed. Neat, but annoying in test cases. What we can do is assert in the beforeEach that our RootViewController’s view is not null. This should access the view and new it up:

beforeEach(^{
    subject = [[RootViewController alloc] init];
    subject.view should_not be_nil;
    navController = [[UINavigationController alloc] initWithRootViewController:subject];
});

Now we have passing test cases, hooray!

In our next part, we’ll talk about passing and rendering data between views so that we can save the names of scoresheets.


Footnotes

#1 – I am completely guessing from my ten-year-old C++ hazy college days