Building Objective-C Frameworks

What is a Framework?

Conceptually, a framework is simply a way for you to modularize compiled code. Frameworks the ideal way for you to distribute re-usable code within and outside your organization, and they make handling library dependancies a whole lot easier than managing a binary archive and a global header.

At the technical level, a framework is a special kind of macOS bundle (and macOS bundles are literally just folders) designed for use by developers. A framework bundle usually contains development resources (usually statically-built libraries and interface headers), but can also contain other kinds of resources like images, storyboards, xibs, and property list files.

A framework behaves differently than a normal macOS bundle — a compiler can extract compiled binaries from a framework and link them during build time or even at run time, and the Finder treats bundles with a .framework extension differently, presenting them as normal folders for easy perusal by a developer.

Because of these unique features, framework bundles require specific directory structures and file placements to work correctly, particularly for their most common use, binary code distribution.

Why Should I Do This?

There are a lot of reasons you might want to create a framework, and a few reasons why you absolutely should. Frameworks are the best way to ship compiled libraries for use outside your organization, both for you and for your developers. They can also make the structure of a complex app a lot simpler to manage, because you can separate out different portions of the app into different targets, and handle those projects independently. Fundamentally, frameworks exist for three primary reasons:

  1. Modularity. Remove and replace independent parts of your codebase without fear of crazy compiler errors or linking problems. Work on pieces of your codebase one at a time
  2. Re-use. Bundle relevant operations together so they can be re-used across your app or apps.
  3. Encapsulation. Safely develop parts of your codebase to be entirely self-reliant and interference free.

You're probably well aware of these ideas by now, but because of the complexities that go with developing a framework, many people seem to avoid it.

However, if you're developing anything more than the most basic of iOS apps, separating things into frameworks is something really worth considering.

What About Swift?

You might be wondering why I explicitly excluded Swift in the title of this blog post. The answer lies in Swift's ABI stability which, according to Apple, just isn't ready for the primetime. 

ABI stability is crucial for frameworks to see their full potential. You can't bundle Apple’s iOS frameworks with your precompiled library (and it would quickly become impractical even if you code), and without the stability of the ABI, you can't guarantee that your frameworks will work the same way with OS libraries in production vs the ones you were developing your framework with.

This isn't a criticism of Swift, it's merely a symbol of its youth. ABI stability was supposed to be delivered in Swift 3, and we still don't have it even now at Swift 4. A stable ABI makes the language much harder to change, and Apple must feel like they have more changes they need to make to the language before they can lock it all down. ABI stability is a big focus of the next release of language, and I'd imagine we'll be able to abandon Objective-C sooner rather than later but for now, Objective-C should be your language of choice for developing a framework. Lack of ABI stability shows that Apple themselves isn't using much Swift in the bowls of the OS. 

Anatomy of a Framework

There are a lot of ways you can structure and package a framework bundle, and your choices should be made base on how you imagine the framework will be used in production. Before you go down this road, familiarize yourself with Apple's documentation on framework bundles — it's a good place to begin if you've never done anything like this before.

A framework typically contains 3 things:

  1. Compiled binary code. A well-designed framework should contain multiple binaries for different processor architectures.
  2. Interface Headers. This is Objective-C after all, and you'll need to provide headers for all the classes you distribute in your framework.
  3. Other Resources. Frameworks are sometimes used to distribute other resources as well. I don't really recommend this, because the way you do this can vary from platform to platform, and there are usually better ways to distribute resources that belong with your code, like a dedicated resources bundle.

Static & Dynamic Frameworks

Frameworks come in two variants, static an dynamic. The difference between the two is exactly what it sounds like: static frameworks are linked at build time, and dynamic frameworks are linked at compile time.

All of Apple's frameworks are dynamically linked, but your frameworks can be either. Until iOS 8, dynamic linking of frameworks was only available on macOS, but now you can use dynamic linking on iOS as well.

Static frameworks can increase your app's disk size and build time, because the entire binary is copied and linked during the build process. Dynamic frameworks only use the relevant bits of the framework during build time, and perform the linking during at runtime, potentially increasing your App's launch time but reducing its overall disk size.

Dynamic frameworks also have another advantage: because the linking happens every launch a framework can be updated without the need for re-building the app. 

Dynamic frameworks are certainly the more modern way of doing things, but there are good reasons to use static frameworks, especially for code you plan on shipping for use outside of your organization. 

Processor Architectures and Slicing

Binaries in a framework usually contain multiple versions of the same code designed for different processor architectures. These pieces are called slices, because they are merged together into a single binary. To create a proper framework, you'll need to make sure you're including slices for every version of the ARM instruction set used by iOS devices you plan on supporting, as well as slice for the special x86 version of iOS used by the iPhone simulator.

Building a Dynamic Framework

Building a dynamic framework with Xcode is a lot less complex than you might think. In this example, we'll be creating a framework called ExampleKit, which contains a data structure called EKObject and a processing object called EKSession. You can find both classes here, but don't do anything with them just yet.

Start by creating a new Xcode project. Choose the "Cocoa Touch Framework" option, and name your project "ExampleKit". As usually, creating your project with a git repository is probably a good idea.

The Cocoa Touch Framework project template, found in the iOS section, in Xcode 9.x

The Cocoa Touch Framework project template, found in the iOS section, in Xcode 9.x

Setting Up Your Project

Before we proceed, let's take a look at what Apple's template already provides you with.

  1. You've already got an umbrella header, with clear instructions on how to import public headers.
  2. All the necessary build phases are already in place for you
  3. Code signing has been set to "don't code sign"

These are good things, as you'll see when we build a static framework. Apple has done the bulk of the complex work for you. The only real setup you need is to choose a deployment target — the minimum version of iOS that you plan on supporting.


NOTE: iOS 11 officially drops support for 32-bit devices, meaning all currently available iOS devices use the arm64 instruction set as of this writing. If you plan on supporting older versions of iOS, you might need to include slices for armv7 and armv7s


That's pretty much all the set up you need!

Writing Your Code

If you've build an iOS app target before, you're probably familiar with the concept of "target membership". You know that resources must be a part of the app's "copy resources" build phase, and that implementation files must be a part of the "compile sources" build phase, and that header files typically aren't mentioned in an app target's build phases.

Add the both the classes to your project, and make sure they've been copied into your source directory as well. Once you've done so, make sure that both the .m files are a member of your framework's target, "ExampleKit".

When you're building a framework, headers too are a part of the build phase, and must also be members of target. You might have already noticed that the .h files have target membership available to them in the file inspector. Add them to the as members of the target as well.

The file identity and type inspector of a header file, "EKObject.h", in Xcode 9.x

The file identity and type inspector of a header file, "EKObject.h", in Xcode 9.x

Access Control

Notice that when you added the header files to the the target, you were also give an access control selector. Make sure that both header files are set to "Public", rather than "Project" or "Private".

These titles are a bit misleading — all headers will actually be visible to your clients and explorable in the finder, but explicitly making headers public is necessary to import them in the umbrella header. Generally, you'll want to put headers that your clients should use in "Public", and others that are specific to the implementation of your library in "Project" and "Private". They won't be invisible, but they won't come up in code completion, won't be imported in your umbrella header, and it's generally understood that they aren't to be used outside the framework. (This is one of the things that Swift allows that Objective-C just doesn't — true file and project privacy)

Umbrella Header

Speaking of the umbrella header, we need to go back and make our new class interfaces visible. This way, they can all imported together with a single statement. Go to the ExampleKit.h file, and add import them like so:

#import <ExampleKit/EKObject.h>
#import <ExampleKit/EKSession.h>

Universal Support

We're using an Xcode template explicitly designed for creating dynamic frameworks, so the bulk of the configuration is already done for you. Compile your code once to make sure everything is working before proceeding with this next step.

Remember the business of slicing we were talking about earlier? Xcode will build your framework only for the currently selected platform, and its applicable slices. That means, if you're building for iphonesimulator, you'll get a framework with an x86 slice but no ARM slice. If you build for iphoneos, you'll get a framework with arm slice(s), but no simulator support. 

To do this correctly, we'll need to write a script that compiles for *both* platforms and *all* potential slices, merges the binaries produced from each, and produces a completed framework using the structure from either of the first two, single platform builds.

Make sure you understand what this script does rather than just blindly copying it and pasting it. You may want to modify it as needed with future release of Xcode as well as your project configuration's specific needs, and it maybe worth adding the script as a dedicated file rather than typing it into Xcode directly, so you can more easily keep track of it with version control.

You might not want to perform this step every time you build framework during the development process — it can make builds take longer. There are a few ways to separate out the platform binding script:

  1. You could manually enable and disable it in your target's build phases
  2. You could write your script to only invoke when building with the RELASE configuration
  3. You could create a new aggregate target, link your framework target as dependency, and add the script to that target's build phases, rather than your primary one.

The first option is the easiest. Go to your target's build phases, as the last phase. Then, just build your target for "Generic iOS Device" and you're set, but you'll have to disable and re-enable the script as necessary.

The second option is also pretty simple. Just wrap your script in a if statement and check if the build configuration is set for release:

if [ $CONFIGURATION == Release ]; then
# script here
fi

The third option is my method of choice. Create a new aggregate target, and call it "Framework". Then, add your dynamic framework target is a dependency, and add your script to the new "Framework" target instead. Then, build the "Framework" target for "Generic iOS Device" when you want to build a universal binary, and build just the framework target when you want to build a binary containing slices only for the currently selected platform.

Using Your Dynamic Framework

Using your dynamic framework is a bit different than using Apple's dynamically linked frameworks, which come directly from the OS. Because you're providing the binary as part of your app, you need to add it as an embedded binary, so the run time knows to load & link that binary from within the app itself at launch like so:

embedded-binaries.png

Download the fully completed ExampleKit dynamic framework, as well as a completed app that correctly links against it.

Building a Static Framework

Building a static framework is bit trickier than building a dynamic framework, because Xcode doesn't actually have the correct template for you do work with. Because of this, we'll need to start with Xcode's static library template, and add some scripting to correctly package that static library as a framework.

Setting Up Your Project

Create a new Xcode Project and this time choose the "Cocoa Touch Static Library" template. Again, name this project "ExampleKit" and initialize a git repository along with it.

The Cocoa Touch Static Framework project template, found in the iOS section, in Xcode 9.x

The Cocoa Touch Static Framework project template, found in the iOS section, in Xcode 9.x

If you build the dynamic framework before getting this far, you'll notice some differences. EmampleKit.h doesn't look like it's designed to be an umbrella header. It provides an interface for a class called ExampleKit, and .m file for you to implement class has been added as well.

Start by fixing that: delete the interface declaration in ExampleKit.h, and delete ExampleKit.m all together. 

Writing Your Code

Just like in the prior example, our static framework will contain two classes: EKObject and EKSession. Both are available here. Add all four files to your project, and make sure that EKObject.m and EKSession.m are members of your static library target, ExampleKit.

Access Control

Earlier, we were able to assign access control to each header simply by adding it to our target and choosing it's access control level in the file and identity inspector. Unfortunately, since this template was not designed to be a framework, such functionality isn't baked in. We'll need to add a headers phase ourself.

Go to the "Build Phases" section of your target's settings, click the (+) button in the top left hand corner, and add a new "header phase".

add-header-phase.png

Once you've done this, you'll be able to add each header to the target as a member and assign a level of access control. Make sure that EKSession.h and EKObject.h are both "public", but keep ExampleKit.h out of the target's membership.

Umbrella header

Just like the dynamic framework, we'll want to expose our headers in the umbrella header so they can all be easily imported at once. Go to ExampleKit.h add import them like so:

#import <ExampleKit/EKObject.h>
#import <ExampleKit/EKSession.h>

Build your "ExampleKit" target and make sure that everything compiles without errors.

Packaging

Because we're using a template designed for a static library, we'll need to make some adjustments to our target make it produce a static framework. Our template doesn't create the necessary directory structure and symbolic links, so we'll need to change a few things our target's build settings, as well as create a script to do some of the heavy lifting for us.

Update Build Settings To Support Static Frameworks

  1. Change Public Headers Search Path setting to include/$(PROJECT_NAME)
  2. Change Dead Code Stripping setting to NO
  3. Change Strip Style to Non-Global Symbols

Build your static target again, and right click on your "libExampleKit.a" product and reveal it in the finder. You'll see that your static library has been reduced to a single archive an some headers. An improvement, but still no single framework.

Module Support

The dynamic framework template automatically creates a module map for you, and places it into the appropriate directory at build time. Clang Modules are a better way to import files than #include (or #import, which is just #include without duplication), and are essential if you want your framework to be usable with Swift projects. Create a new blank file, and title it "module.modulemap". It doesn't need any target membership.

framework module ExampleKit {
    umbrella header "ExampleKit.h"
    
    export *
    module * { export * }
}

For more information on Clang modules and how they work, see this document on the LLVM website. 

Creating The Bundle Structure

Add the following script as your static library target's final build phase:

Now, build your static library target again for "Generic iOS Device". Reveal your libExampleKit.a product in the finder, and take a look! You'll now notice that, in addition to the usual build products, you'll also find "ExampleKit.framework". Double click on it, and you should find a bundle that has the right structure for an objective-c framework.

Universal Support

Just like the Dynamic Framework, you'll need to use a script to build the target for both platforms and merge their binaries into a single multi-slice binary. The script for this is a bit different than the dynamic framework, but not by much. Essentially, since we're merging files that were created as the product of a script rather than directly from Xcode, we'll need to point to them using an environment variable called "STATIC_LIB". Just like the dynamic framework, this script needs to be run every time you want to create a fat binary, and there are a few different way to separate it from your normal build process to give you extra flexibility during the development process.

Once you've decided on how you want to include this script, build your target again for "Generic iOS Device", and you'll find that your static framework is ready for use in the /Release directory of your project folder.

Using Your Static Framework

Using your static library in Xcode is identical to using any of Apple's dynamically linked-platform frameworks. You don't need to list ExampleKit.framework as an embedded binary, simply add it to your app target's "Linked Libraries and Frameworks" target, and ensure that your framework bundle is located within your app project folder. You can download a completed version of the static ExampleKit framework, as well as a correctly linked app.

Best Practices

Creating a universal framework is only half the battle. If you plan on distributing your framework for use by use than more than few people with specific use cases in mind, there are a few things you should do to make sure your framework will work in a completely foreign codebase, no-matter what the situation. 

Creating a good framework can't really be summarized into a set of rules — its really an entire mindset change. You need to assume that your user is an engineer, and that they might try to do all kinds of stuff with your framework. They might access private implementations, subclass things they shouldn't, and store your custom data structures in collections in ways you haven't thought of. They might write poor code, keep things in memory for too long, or incorrectly use your programming patterns. Your job is to try and come up with as many of these scenarios as possible and plan for them in advance. 

There are, however, a few things you can do to solve a bunch of common problems.

Basic Requirements Documentation

Tell your developers as much about how your framework should be used as possible. Make a list of required OS dependencies and provide them to your engineer. If your framework makes use of Objective-C categories, document this usage and instruct developers to make use of the -all_load and -ObjC linker flags. Clearly indicate which classes are designed for subclassing and which ones aren't.

Swift Interoperability

Swift introduces the concept of optionals, a syntax system which defines the nullability of a variable and affects it’s control flow because of it. Options affect control flow and swift, and it's important that you annotate your interfaces with nullability using the appropriate directives in Objective-C. Information on how you might do that is available here.

Safe Subclassing

Objective-C is an incredibly dynamic language, and it's reflexive properties are one of the many reasons that language loyalists love it so much. However, this can sometimes mean that subclassing an object who's implementation you aren't intimately familiar with can cause some unexpected issues. You can make objects safer to subclass with a few simple stylist changes to the way you declare methods: don't use super generic method names, and add underscores to methods definitions which aren't included in the classes header.

Modern Objective-C Features

Swift might be the future of the platform, but that doesn't mean that Apple has completely abandoned our favorite small-talk inspired object-oriented C family language. In the past few years  Apple has added tons of new features to Objective-C like generics and covariants, object literals, class-level properties, and designated initializers to name a few. Make sure you're talking advantage of these new features whenever they make sense for you to use them.

Good Collections & Data Structures

If you're class includes custom data structures of collection objects, take step to ensure they work in as many scenarios as possible. These kinds of objects can have the widest variety of uses, so there are a few things you can do to make sure they do their best:

  1. Implement NSSecureCoding and NSCopying protocols. Explicitly declare the classes conformation to these protocols in their header. 
  2. Override the -isEqual: and -hash methods, so your data structures might work with with other collections whose implementations rely on accurate responses from these messages.
  3. For simple data structures, consider subclassing NSValue.

Class Separation & Access Options

When working on a class who's existence in memory is long, provide your developers with as many ways as possible to interact with that object as you can. You never know how a developer might use your object, and you can't assume they'll always make the right choice. As such, make sure you offer delegation, block, and NSNotification options whenever possible. You want your custom object to work in any scenario that might be thrown at it.

Update the UI Only on the Main Thread

Apple has long discouraged this practice, and began sending run-time warning logs in iOS 10 whenever the UI was updated anywhere other than the main thread. With iOS 11, this practice actually trigged a run-time exception. Make sure that any methods that are designed to update the UI are always called on the main thread, and send messages to your custom delegate objects on the main thread as well when possible, as you have no idea how a developer might have implemented your custom delegate protocol and what they might want executed when the message is sent. 

Apple Unified Logging

Don't use NSLog. Try to log as little possible on the release version of your framework, and take advantage of the Apple Unified Logging APIs introduced in iOS 10 if possible. This system provides much better performance of logging at run-time, and allows for much more control and flexibility of how log messages are are stored and for how long. If you haven't already, take a look at Apple's documentation on Unified Logging.

Bitcode Support for App Store Distribution

If your framework is going to be used in an app-target to be distributed on the App Store, then you're going to want to support Bitcode. The standard iOS app templates all have bitcode enabled by default (meaning an app linked to your framework will cause a build-time error), and bitcode can help save space of the target's total product (especially if you're using a static framework). Adding bitcode support is pretty simple. Go to build settings of your relevant target, and add the following:

Comparing Static and Dynamic Frameworks

I've created two identical apps, which link against static an dynamic versions of the same ExampleKit framework. Let's compare their launch times:

ExampleApp (Dynamic)

Total pre-main time: 162.94 milliseconds (100.0%)
         dylib loading time:  53.74 milliseconds (32.9%)
        rebase/binding time:  30.17 milliseconds (18.5%)
            ObjC setup time:  52.79 milliseconds (32.4%)
           initializer time:  26.09 milliseconds (16.0%)
           slowest intializers :
               libSystem.dylib :   2.03 milliseconds (1.2%)
    libMainThreadChecker.dylib :  14.02 milliseconds (8.6%)

ExampleApp (Static)

Total pre-main time: 110.49 milliseconds (100.0%)
         dylib loading time:  33.73 milliseconds (30.5%)
        rebase/binding time:  24.80 milliseconds (22.4%)
            ObjC setup time:  30.91 milliseconds (27.9%)
           initializer time:  20.94 milliseconds (18.9%)
           slowest intializers :
               libSystem.dylib :   1.88 milliseconds (1.7%)
   libBacktraceRecording.dylib :   3.19 milliseconds (2.8%)
    libMainThreadChecker.dylib :   9.63 milliseconds (8.7%)

ExampleKit is a really simple framework with only two classes, and yet there is a significant difference in launching times. I ran these tests several times, and I could never get the dynamically linked app to fire in under 150ms, while statically linked one was reliably under 150ms. Avoid embedding tons enormous of binaries into an app, as you can't statically link Apple's frameworks.

Conclusion

Breaking up your code into frameworks a good idea for large projects, and an absolute must for distributing compiled code outside your organization. While setting up your framework project may seem daunting, you can see that with a bit of project configuration, it's actually not all that tough! Using frameworks, you can create usable iOS APIs that will help simplify your own app, share code amongst a team of developers, and ship compiled features to anyone without revealing your source code.