Pioneering Software

Innovation, quality, care.

Developing a Project as an OS X Framework and iOS Static Library at the Same Time

| Comments

You want to develop a framework for OS X and iOS at the same time!

This is not a strange thing since OS X and iOS are substantially similar underneath: a Unix box running Cocoa. With Lion, Apple seem determined to make them even more similar. And why not?

You want both framework and library to share identical sources so that you do not need to maintain two mostly-similar sets. If there are any differences, you want to limit that only to actual differences, not just differences in the way they need compiling and linking.

There is a simple way to do it.

To explain how, take a simple example.

The scenario: You want to develop a framework called MyKit. You want this compiling to a Mac OS X framework for use on OS X and as a static library for use on iOS. The final build products will be:

  • MyKit.framework with the standard framework layout
  • libMyKit.a, a static library with headers:
    • MyKit.h, the monolithic public header
    • MyKit/MyKitPrivateHeader.h, one (or ultimately more) private headers

Developer tools: Xcode 4.2, build 4D139.

How to Use This Article

I have pushed a Git repository for a sample MyKit project at GitHub. I recommend examining the commit history in concert with the steps outlined below. You can see what has changed at each step.

The short version looks like this, where MyKit stands for your framework-library’s product name:

  1. Locate the Product Name build setting for target MyKit (the framework target) and change it from $(TARGET_NAME) to MyKit.

  2. Rename the MyKit framework target from MyKit to MyKitFramework. This just renames the target, not the product since we have disconnected the two; product name no longer depends on target name.

  3. Rename MyKitTests target to MyKitFrameworkTests. This time however, do not rename the Product Name for this target. Let the product name reflect the target name.

  4. Delete MyKit.h and MyKit.m, the stubs supplied by the Cocoa Framework template—really delete, not just delete references.

  5. Add a Cocoa-based Objective-C category file. Category on NSObject. Save as NSObject+MyKit. (This is just for testing. You might also just add existing sources to the framework or create new ones, as many as you like.)

  6. Add implementations along with a little unit testing.

  7. Change the NSObject+MyKit.h header’s target membership for the MyKitFramework target from Project to Private.

  8. Create a monolithic header. Make it Public. It serves only to import the private headers.

  9. Choose the Cocoa Touch Static Library template for the new target. Include Unit Tests. Call the product name “MyKitLibrary.”

  10. Delete the stub object sources: MyKitLibrary.h and MyKitLibrary.m. Not required.

  11. Also delete the test sources, MyKitLibraryTests.h and m. Also not needed.

  12. Change the product name from $(TARGET_NAME) to MyKit for the MyKitLibrary target. Also add NSObject+MyKit.h and m to this target. Make the header a Private member.

  13. Change ___VARIABLE_bundleIdentifierPrefix:bundleIdentifier___ in the bundle identifier for the test bundle to your company name as a reversed DNS string.

  14. Add MyKitTests.m to the MyKitLibraryTests target.

  15. Add -all_load to the Other Linker Flags for the MyKitLibraryTests target. Now the tests run successfully.

  16. Add MyKit.h to the public headers for MyKitLibrary target.

  17. Change your library target’s public and private header folder from /usr/local/include, the default settings, to include and include/$(PROJECT_NAME) respectively.

For the long version of the story, read on.

Create an OS X Cocoa Framework

This is the starting point. Call the product MyKit, include unit tests, and use version control. Xcode stubs out a basic template with two targets: one for the framework and a bundle for the unit tests.

Product Does Not Equal Target

This is a key ingredient. We want multiple targets by different names to create products with the same name, albeit with differing outcomes.

Start by disconnecting the dependency between the target and product names. By default, the product name is the target name. Make the product name equal to MyKit so that you can rename the framework to MyKitFramework.

Step 1: Locate the Product Name build setting for target MyKit (the framework target) and change it from $(TARGET_NAME) to MyKit.

Step 2: Rename the MyKit framework target from MyKit to MyKitFramework. This just renames the target, not the product since we have disconnected the two; product name no longer depends on target name.

You can now rebuild and retest the framework (Command+U shortcut). The project performs both successfully; although the default unit test successfully fails! Check out the framework under Products. You will notice that its name remains MyKit. Control-click and select Show in Finder to see the evidence directly. Framework product name remains unchanged.

Do the same for the unit test bundle.

Step 3: Rename MyKitTests target to MyKitFrameworkTests. This time however, do not rename the Product Name for this target. Let the product name reflect the target name.

Now you have two targets:

  • MyKitFramework
  • MyKitFrameworkTests

But you have sources:

  • MyKit
  • MyKitTests

Public and Private Headers

The Cocoa Framework template only gives a stub for a class called MyKit. It derives from NSObject and does nothing. Make the MyKit.h header a private header. This will become the monolithic header for importing all private headers.

For something our framework will do, let us make it extend a standard object class with a category. This is the most awkward kind of library-oriented thing for Objective-C. Mac OS X handles it gracefully, but iOS has a little issue with loading category-only libraries. We will need to add an addition linker option -all_load later on.

So, add a new category on NSObject called MyKit which adds a new method to NSObject called -stringFromClass, an instance method which answers the class name as a string for this instance. Not a very useful method; it amounts to NSStringFromClass([self class]). Also, something very similar already exists, i.e. -className appears as a scripting extension, category NSObject(NSScriptClassDescription). Still, it will serve as a piece of very simple functionality, for the purpose of testing out linking against a category-only library.

Step 4: Delete MyKit.h and MyKit.m, the stubs supplied by the Cocoa Framework template—really delete, not just delete references.

Step 5: Add a Cocoa-based Objective-C category file. Command+N is the shortcut. Category on NSObject. Save as NSObject+MyKit. Xcode provides the h and m extensions appropriately for the header and source. Include in the MyKitFramework target, the default selection.

Step 6: Add the implementation along with a little unit testing.

1
2
3
4
- (NSString *)stringFromClass
{
    return NSStringFromClass([self class]);
}

Unit tests:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)testStringFromClass
{
    STAssertEqualObjects([[[NSObject alloc] init] stringFromClass], @"NSObject", nil);

    // NSString deploys a class clustering architecture. The actual class is an
    // implementation-specific sub-class or compatible class, depending on what
    // kind of string, and presumably what version of Cocoa and on what platform
    // since the exact underlying class might change. Be prepared for test
    // breakage.
    STAssertEqualObjects([@"" stringFromClass], @"__NSCFConstantString", nil);

    // This is freaky. You would not expect this to work. But it does; classes
    // are also objects. Invoking an instance method on a class: it compiles and
    // runs! You would expect the compiler to baulk, but no.
    STAssertEqualObjects([NSObject stringFromClass], @"NSObject", nil);
}

Notice that the first assertion does not autorelease the NSObject instance. The project uses Automatic Reference Counting (ARC).

Step 7: Change the NSObject+MyKit.h header’s target membership for the MyKitFramework target from Project to Private. This will mean that applications using the framework cannot directly import the header. Instead they will import the monolithic header MyKit.h which will include all the headers for the framework.

Note that the unit test module still compiles, even though it imports “NSObject+MyKit.h” because Xcode resolves this header against the workspace.

Step 8: Create monolithic header. Make it Public. It serves only to import the private headers.

Now the framework is complete. Next the iOS library.

Add Static Library Target

Add the Cocoa Touch Static Library target for iOS under iOS Framework & Library in Xcode’s template chooser, giving it the product name as the target name MyKitLibrary but then subsequently renaming the product. This differing initial target name prevents the library template from stomping over the existing framework sources.

Step 9: Choose the Cocoa Touch Static Library template for the new target. Include Unit Tests. Call the product name “MyKitLibrary.” This will become the product name, initially, as well as the target name.

Step 10: Delete the stub object sources: MyKitLibrary.h and MyKitLibrary.m. Not required.

Step 11: Also delete the test sources, MyKitLibraryTests.h and m. Also not needed.

Step 12: Change the product name from $(TARGET_NAME) to MyKit for the MyKitLibrary target. Also add NSObject+MyKit.h and m to this target. Make the header a Private member.

There is a bug in Xcode 4.2’s static library template. It writes ___VARIABLE_bundleIdentifierPrefix:bundleIdentifier___ in the bundle identifier for the test bundle.

Step 13: Change this to your company name as a reversed DNS string.

However, you cannot yet run the tests, because there are none defined for the MyKitLibrary target.

Step 14: Add MyKitTests.m to the MyKitLibraryTests target.

The test fails at run-time because linking against the library does not automatically import categories.

Step 15: Add -all_load to the Other Linker Flags for the MyKitLibraryTests target. Now the tests run successfully.

Step 16: Add MyKit.h to the public headers for MyKitLibrary target.

Header Search Within the Workspace

One more thing to do.

You do not need to add special header search paths for your client application—not even for iOS applications. Thanks to Xcode 4, the build process automatically picks up the headers from their respective build locations.

Step 17: To make this searching work though, you need to change your library target’s public and private header folder from /usr/local/include, the default settings, to include and include/$(PROJECT_NAME) respectively. If you want to be fancy and make them interdependent, make the public header folder path equal to include and the private equal to $(PUBLIC_HEADERS_FOLDER_PATH)/$(PROJECT_NAME).

Why include? This is because if you build an iOS application with your library as a dependency and a nested project, Xcode builds everything within a single DerivedData folder. When building the application project and all its dependencies, Xcode automatically adds a header search path at include within this folder. Installing the library headers at this location lets enclosing projects find them easily.

Renaming the Schemes

You might want to consider renaming the MyKit scheme to MyKitFramework. You then have two schemes: MyKitFramework for the framework and MyKitLibrary for the library. Logical!

Comments