Monday June 27, 2011

✦ Library Management With Xcode Workspaces

So, you’re an iOS developer. You practice the single responsibility principle religiously. You have a keen eye for extracting common code into reusable pieces. You want to build a library for reuse in other projects. What’s the best way to do this with Xcode?

That was one of my quests at the WWDC labs. Technically, there’s no official way that you must do this. The engineers I talked to said they made Xcode flexible to support a variety of situations. But in the interest of consistency and the high value I place on my opinion, I’ve adopted one: Sibling projects with static libraries in an Xcode workspace managed by git submodules.

Now, I’ll talk about git submodules in the future, soon. This post is already going to be pretty long. Let’s first focus on setting up Xcode to handle this. For our example, I will create a project that depends on my fork of the TouchJSON library.

Steps From Scratch

Create a new directory somewhere named JSONTestWorkspace.

mkdir JSONTestWorkspace

In Xcode, create a new, empty workspace file with the same name and save it inside there.

Now, clone1 the TouchJSON repo into the workspace directory.

cd JSONTestWorkspace
git clone git://github.com/navellabs/TouchJSON.git

the awesomeness packed into git submodules.

Your workspace directory structure should now look like this:

JSONTestWorkspace/
    |- TouchJSON/
    \- JSONTestWorkspace.xcworkspace

Now, right click on the empty Xcode sidebar and choose “Add Files to JSONTestWorkspace”.

Add files to JSONTestWorkspace

Browse to TouchJSON/Support and choose the Xcode project named TouchJSON-iOS.xcodeproj. The project should now be in the sidebar of the workspace.

In the “File” menu, choose “New” and then “New Project…”. Name it JSONTestApp and make sure to check the “Include Unit Tests” option.

New Project

When prompted, save it in the main workspace directory where TouchJSON and the workspace file reside. Make sure it doesn’t create a new git repo, and make sure its “Group” setting is the workspace your already created.

New Project Settings

Your project tree in the sidebar should now look like this with the JSONTestApp and TouchJSON projects as siblings in the workspace.

Sidebar looks like this

When you create or import projects into a workspace, Xcode automatically adds schemes for each project. For our purposes, we don’t really want any schemes other than for our main app project, so choose “Manage Schemes…” at the bottom of the “Project” menu and delete the TouchJSON scheme that you see as in the sheet below.

Remove the extra scheme

To make sure everything is working as expected, pick JSONTestApp and one of the iPhone or iPad simulators in the scheme picker and choose “Test” from the “Product menu to attempt a build. You should see the following error:

First test failure. Good sign!

That means that the test suite works. We’ll use this test file to make sure we wire up the TouchJSON library correctly in the workspace. In the JSONTestAppTests/JSONTestAppTests.m file, replace all the contents with the following:

#import "JSONTestAppTests.h"
#import "CJSONSerializer.h"

@implementation JSONTestAppTests

- (void)testDictionaryToJSON
{
    NSDictionary *dict = nil;
    NSData *jsonData = nil;
    NSString *jsonString = nil, *expectedJSON = nil;

    dict = [NSDictionary dictionaryWithObject:@"object" forKey:@"key"];
    jsonData = [[CJSONSerializer serializer] serializeDictionary:dict
                                                           error:nil];
    jsonString = [[NSString alloc] initWithData:jsonData
                                       encoding:NSUTF8StringEncoding];

    expectedJSON = @"{\"key\":\"object\"}";
    
    STAssertEqualObjects(expectedJSON, jsonString,
                         @"The JSON strings didn't match!");
}

@end

Back in the file browser of the Xcode sidebar, choose the JSONTestApp project. Then choose the “JSONTestApp” project in the inner sidebar as shown below. Choose choose the “Build Settings” tab and search for “User Header” to filter down the list of settings. Double click on the empty value to add the recursive path ../TouchJSON/Source.

Add header path

Now for each target in that inner sidebar (both JSONTestApp and JSONTestAppTests) add the TouchJSON libary to be linked. Make sure the “Build Phases” tab is selected and add the libTouchJSON.a as shown below. Click the “+” to add it. (Again, make sure you do it to both targets!)

Link with binary

Finally! Choose “Test” from the “Product” menu (or just press Cmd-u). A simulator with a black screen will pop up while the tests are run and no error will be reported. If you view the run logs, you should see something like the following:

Test Suite 'JSONTestAppTests' started at 2011-06-28 03:05:28 +0000
Test Case '-[JSONTestAppTests testDictionaryToJSON]' started.
Test Case '-[JSONTestAppTests testDictionaryToJSON]' passed (0.000 seconds).
Test Suite 'JSONTestAppTests' finished at 2011-06-28 03:05:28 +0000.
Executed 1 test, with 0 failures (0 unexpected) in 0.000 (0.000) seconds

To make sure you have everything wired up, let’s alter the test to purposely make it fail. If you change the expectedJSON string variable to something like @"walrus" and rerun the tests, you’ll see the error “The JSON strings didn’t match!”

This walkthrough gives you some hands on experience adding sibling projects to a workspace, making sure the headers are found in the right place, and the targets are linked to the libraries. You can extract code into your own independent libraries like the way TouchJSON is set up and then link them in to your projects. In a bit, I’ll write up a post on using this technique with git submodules to help manage library versions and share the code with other app projects.

  1. I’m using clone for now for simplicity for those that don’t know