Using frameworks in iPhone applications

Now that the iPhone NDA is being lifted, we can share a few of the lessons we've learned while working with the SDK.

Since OmniFocus makes extensive use of the Omni Frameworks, one of the very first challenges we hit when starting with the SDK was how to structure our source and Xcode projects so that we could re-use some of our battle-tested common code between OmniFocus on the Mac and on the iPhone. Some people like splitting up their source into frameworks and some don't. That's all good, but on the iPhone 3rd party developers have no supported way to build their own frameworks.

Many limitations also come with a benefit, and in this case it is smaller application packages. A framework may include classes or categories that are used in several clients, but not all of them. On the iPhone, we want fast downloads from the App Store (possibly over the cell network) and we want fast startup times. Both of these require us to not bloat our executable with code not specifically used by our app.

Given that we can't use real frameworks bundles, we need to directly include our framework source in our iPhone project. I'll describe an approach that's worked well for us and might suit you too.

First, we want to be sure that we don't pick up any extra headers that we didn't intend to use. For example, if our OmniFoundation NSSet category imports another extension header, we want to be sure that we consider that change rather than just letting the dependency creep in (if nothing else, we need to build the corresponding .m file). One approach that helps with this is to use <…> instead of “…” imports, so we have:

#import

This ensures that Xcode finds our header from a “public” location rather than finding it relative to the file being compiled. Now, if we do have an internal header that shouldn't be published, we can leave it in an “…” import, but we have to take care to add those .m files to the target.

Second, we want to “publish” the headers for the public headers necessary to build the subset of the framework we'll be using. We accomplish this with a sequence of Build Phases in our OmniFocus target in Xcode:

OmniFocus for iPhone Target List.png


The first interesting phase, Create Header Directories, gives us a place in our build area for the headers:

FRAMEWORK_HEADER_BASE="$DERIVED_SOURCES_DIR/FrameworkHeaders"

mkdir -p "$FRAMEWORK_HEADER_BASE"

mkdir -p "$FRAMEWORK_HEADER_BASE"/OmniBase

mkdir -p "$FRAMEWORK_HEADER_BASE"/OmniFoundation

mkdir -p "$FRAMEWORK_HEADER_BASE"/XMLData

mkdir -p "$FRAMEWORK_HEADER_BASE"/OmniFocusModel

mkdir -p "$FRAMEWORK_HEADER_BASE"/OODataTypes

mkdir -p "$FRAMEWORK_HEADER_BASE"/OmniDataObjects


Then, for each “framework” we have a Copy Files build phase that is set to copy to the header directory in question. For example, Copy OmniBase Headers looks like:

Copy OmniBase Headers.png


Finally, we need to let Xcode find the headers when it builds our target. Opening the Target Info editor for our iPhone app, we can double-click the Header Search Path entry and add the top level FrameworkHeaders directory:

Header Search Path.png


Now we can add just the framework headers and source files we need to our iPhone project. Each “public” header can then be dragged into the appropriate Copy Files build phase for installation during builds.

This isn't the end of the road, but it is a good start. Other issues involve making sure that your NSError domain strings aren't dependent on your bundle identifier (since on the Mac they'll be in different bundles and on the iPhone not), resource location (again, due to the one bundle vs. many difference on the platforms), and generating strings files when your sources are spread around (there are a couple scripts floating around for doing this; once we clean ours up we'll hopefully publish it).