Using frameworks in iPhone applications
by Tim Wood on October 1, 2008
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:
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:
The first interesting phase, Create Header Directories, gives us a place in our build area for the headers:
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:
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:
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).