An Introduction to Creating and Distributing Embedded Frameworks in iOS
- As JasonR1 points out in the comments, the bug/feature referred to in the “Working with Older Xcode Versions” section has made a comeback in Xcode 7.1, so please review the information below as it’s once again relevant.
- As featherless points out in the comments, distributing frameworks written in Swift via a pre-built .framework file (as opposed to via source code from which users can build the framework themselves) can be problematic. In order for your app to build properly, it and the frameworks it uses must have been built with the same version of the Swift compiler. Since Swift is changing so quickly, maintaining compatibility of the framework with whichever version of Xcode a consumer of your framework is using quickly becomes a hassle. For this reason, you may want to use Objective-C or distribute the source code instead.
With iOS 8, Apple introduced a number of app extensions such as the Share and Today extensions, as well as WatchKit extensions for interacting with WatchKit apps. To facilitate building these extensions, Apple introduced a new (to iOS) type of framework called an embedded framework. The typical use cases Apple has shown for using these frameworks revolve around projects where the embedded framework’s code is included in the same project as the code that uses the framework (e.g., an app with a Share extension packaged up as an embedded framework residing in the same project). What hasn’t been quite as clear is how one can distribute these frameworks when the framework’s parent project and the framework consumer’s parent project are not the same project.
At Hootsuite, as we create new apps beyond our flagship iOS app (such as Suggestions by Hootsuite), we’ve become interested in sharing code between our apps as well as between our individual apps’ main target and their various extensions. For example, many projects (including the main Hootsuite iOS app) define their own branded fonts and colors in a category on UIFont and UIColor. Replicating this across several apps, each of which has several of its own extensions, quickly leads to duplication of this code many times over. The same can be said for common networking code, custom UI elements, etc. These new embedded frameworks appear to be a great way to share such common code across projects.
In this blog post, we’re going to walk through creating and distributing a simple embedded framework consisting of a single class extension on UIColor. The embedded framework we create won’t be that exciting; it’s the ability to distribute and consume the framework in multiple apps that is. We should note that we are taking the DIY approach here and that there are tools out there such as Carthage and CocoaPods that can handle most of the heavy lifting of this process for you. Even if you do decide to use one of these tools, it can be helpful to go through the process yourself to better understand what’s going on. We should also note that we’ll be using Xcode 6.3.2 (with the command line tools installed) and there may be slight variations if you are using a different version of Xcode. To get started, let’s review some important framework-related terminology.
The iOS 8+ “embedded frameworks” are both embedded and dynamic. To distribute them properly, we must make them into fat frameworks containing all the necessary slices. What does this even mean!? Let’s find out.
Static vs. Dynamic Frameworks
Static frameworks are linked at compile time. Dynamic frameworks are linked at runtime, and can be modified without relinking. If you’ve ever used a non-Apple framework prior to iOS 8, you were using a static framework which was compiled into the source code of your app. See Apple’s “Dynamic Library Programming Topics” document for further discussion.
Embedded vs. System Frameworks
Embedded frameworks are placed within an app’s sandbox and are only available to that app. System frameworks are stored at the system-level and are available to all apps. Apple reserves the ability to create system frameworks for itself; there is currently no way for third-party developers to create system frameworks on iOS.
Thin vs. Fat Frameworks; Slices
Thin frameworks contain compiled code for one architecture. Fat frameworks contain compiled code for multiple architectures. Compiled code for an architecture within a framework is typically referred to as a “slice.” For example, if a framework had compiled code for just the two architectures used by the Simulator (i386 and x86_64), we would say it contained two slices. If we distributed this framework, it would only work when its consumer was built for the Simulator and would fail when the consumer was built for device. In order to ensure our frameworks can be consumed properly, we must also include the device architectures (currently arm64, armv7 and armv7s) for a total of five slices.
Well, that wasn’t so bad. Next, let’s create our embedded framework.
Creating the Framework
This is the easy part. Here we’re going to create probably the world’s simplest embedded framework.
- In Xcode, go to File > New > Project and select iOS > Framework & Library > Cocoa Touch Framework:
- Name your project “BrandingFramework”, enter your organization identifier, select Swift as your language and save it to disk.
- Add a Swift file to the project (File > New > File > Swift File) and name it “Branding.swift.” The following example will be in Swift, but you could do something similar in Objective-C.
- Add to this file your extension on UIColor:
- In the build settings for the BrandingFramework target, make sure that “Build Active Architectures Only” is set to “No” for both debug and release. As well, “Valid Architectures” should include “armv7s”, “armv7” and “arm64.” Depending on your version of Xcode, the “Standard Architectures” option will include all of these or just a subset of them. If you’re reading this in the future (relative to the summer of 2015), then there may be other architectures listed to support newer devices:
- Build the project against the Simulator. At this point, your framework will be created using only the slices for the Simulator (alternatively, if you built it for device it would only have the slices for the device). The default scheme for building is set to create a debug version, so this framework will be the debug version.
- Locate the .framework file that was created. It will be in your project’s Derived Data folder in one of the subfolders of Build/Products, but the exact folder will depend on your build settings. For example, when I built the debug version to the Simulator my .framework file was located at “
/Users/Brett/Library/Developer/Xcode/DerivedData/MyFramework-hezzcnchdodkgegxmncdzjjhkkde/Build/Products/Debug-iphonesimulator/BrandingFramework.framework.” If built for device, the folder will have “iphoneos” in its name instead of “iphonesimulator”, and if built for release it’ll have “Release” instead of “Debug.” You can locate your Derived Data folder easily in Xcode by selecting Window > Projects in the menu, selecting your project,and clicking the arrow icon next to the Derived Data path displayed in the Projects window.
- Switch to the folder containing your framework in the Terminal. We can verify the architectures of the framework using the following command:
- This should output something like this:
This confirms that the framework has the Simulator’s (and only the Simulator’s) required architectures. If you repeat steps 5–7 and build to device, you will see a different set of architectures, which will be the device architectures. If you were to add either .framework file to another project right now, that project would only be able to build to either the Simulator or the device depending on which .framework file you used; it would not be able to build to both. In the next section, we will learn how to stitch together a .framework file containing both the Simulator and device architectures.
Creating the Fat Framework
As we’ve seen so far, in order to be able to consume our framework, we need it to contain the correct architectures that match with the build settings of the consumer. When the framework and the consumer of the framework are part of the same project, this isn’t a problem; Xcode will build the framework and the consumer using the same settings. A problem only arises when the framework and its consumer are in different projects. To solve this problem, we can use the same command line tool, lipo, that we used in the previous section. The basic idea is to use lipo to merge two frameworks together into one framework containing all the needed architectures. Of course, we don’t want to have to manually build for Simulator and device separately and then run lipo every time we want to build the framework, so we’ll use a build script to handle this for us. To automate the merging of the device and simulator slices into one framework, we’ll use a simple build script I wrote. It’s rather basic and geared toward demonstration purposes, but feel free to expand upon it and modify it as necessary.
- Create a new aggregate target in Xcode, by going to File > New > Target and selecting “Aggregate” from iOS > Other:
- Name the target “Build framework.”
- In the Build Phases section of the newly added aggregate target add a new run script phase.
- Copy and paste the following script into the editor area of your new run script. See the comments in the run script to understand what it’s doing.
- Build the project using the target we just created. The BrandingFramework.framework file will appear on your Desktop. In practice you probably don’t want to build the framework to the Desktop, but this makes things easy for our purposes. Next up, we’ll add this newly created framework to our consumer project.
Consuming the Framework
Now that we have our fat framework ready, we can import and use it in a separate project.
- Create a new Single View Application project (File > New > Project then iOS > Application > Single View Application) in Xcode and name the project “Consumer.”
- Drag the BrandingFramework.framework file from the Desktop into the Consumer target’s “Embedded Binaries” section. In the resulting popup window ensure that “Copy items if needed” is checked. Xcode will automatically add the framework to the “Linked Frameworks and Libraries” section as well.
- Replace the contents of ViewController.Swift with the following:
- Run the app. The app should build and launch with a magenta background, which is coming from the framework. Hooray!
Working with Older Xcode Versions
If you’re using Xcode 6.3.2 or newer, you can skip over this section. Versions of Xcode 6 prior to 6.3.2 suffered from a bug which caused validation to fail when an embedded framework contained the simulator architectures. Although this is no longer a problem, we’ll cover an approach to solving this for posterity’s sake. We again solve this by using lipo and some build scripts, but things get a little messy here. We’ll use two run scripts, the first will create a backup copy of the fat framework and then replace the framework with a version that has had its simulator architectures removed. The second script will restore the backup of the fat framework after it has been used to the build the Consumer project.
- In the Consumer project, add a run script to the Consumer project and copy and paste the following script into the run script’s editor area. This build script will strip out the simulator slices from the framework.
- Create a second run script and copy and paste the following script into the run script’s editor area.
- Arrange the run scripts such that the first one is listed just below “Target Dependencies” and the second run script is listed just below “Embed Frameworks.:
- Create an archive build of the Consumer Project (Product > Archive in the menu) and submit it for validation (Window > Organizer in the menu; select the project in the Organizer, press “Validate…” button and follow on screen instructions. The app should pass validation as the simulator architectures were removed from the framework during the build process.
Today we covered how to create embedded frameworks, make them into fat frameworks and consume them from separate projects. We also demonstrated how to work around a related Xcode 6 bug in versions prior to Xcode 6.3.2. In practice, you would want to properly version the framework and have some sort of distribution mechanism in place (e.g., posting the framework on a web page or using git submodules instead of building and manually copying the framework file from the Desktop as we did). Although the DIY approach can be fun and a great way to learn, if it seems like a little too much work for you, Carthage and CocoaPods are both excellent tools that can do much of the heavy lifting. By using embedded frameworks when working on multiple closely related apps we can avoid much of the code duplication we might otherwise require.
About the Author
Brett is an iOS developer. He spent 30 seconds trying to come up with an about the author blurb and then got distracted by something else. Follow him on Twitter @bstover.