In Part 1 we identified and discussed the many challenges our teams face integrating third-party frameworks into our apps. We offered our Seaworthy Framework Checklist for Vendors as a baseline for evaluating framework quality. Then in Part 1.5 we took a detour to apply our checklist to Apteligent’s iOS framework which passed the test with flying colors. Now it’s time to continue that journey, and this time as framework authors—let’s build a seaworthy framework together!
Planning the Journey
Before we start writing code, we want to have a solid understanding of what our goals are for the framework. We ask ourselves important questions to hone in on what we want to build.
- What is the problem we are trying to solve?
- Is the framework going to be published to a large audience as a general solution, or are we interested in sharing code in our own projects as a way to achieve modularity?
- Is the framework going to be open or closed-source?
- Will we write it in Objective-C or Swift?
- How will we distribute our framework?
These questions will inform the type of framework we will build and the technical considerations we make.
We are presented with two templates when we open the Framework & Library section of Xcode’s new project dialog.
- Cocoa Touch Framework: This project builds a dynamic framework which will be loaded by one or more of your app targets and dynamically-linked at run time.
- Cocoa Touch Library: This project builds a binary which can be statically-linked to your app, which means that the binary is copied and linked at compile time.
For clarity, let’s go over a few definitions.
- Dynamic Framework: When we talk about a dynamic framework, we’re referring to the product of the Cocoa Touch Framework template which is intended to be dynamically-linked to our app.
- Static Framework: Note that the Xcode templates do not offer a “Static Framework” option. When we talk about static frameworks, we mean the result of taking a Cocoa Touch Library and packaging it as a framework.
- Universal Static Framework: The universal static framework expands a static framework by concatenating multiple binaries, each for a different architecture, into one binary so a single package can be distributed for use by more than one system architecture.
Now that we have those definitions nailed down let’s go over our options in more detail, consider some of the pros and cons of each, and issues we might encounter.
Dynamic Frameworks were introduced at the 2014 WWDC Building Modern Frameworks session and the ability to use them shipped with iOS 8. One advantage of dynamic frameworks is that we can add them as sub-projects and build them with our app. This diminishes many of the complexities making static frameworks—we can worry less about architectures and special packaging scripts.
Apple says, “It would be dangerous to rely upon binary frameworks that use Swift—especially from third parties” because Swift hasn’t achieved ABI stability yet. Because we shouldn’t ship binaries written in Swift, dynamic frameworks are the only option for sharing Swift APIs apart from simply distributing source files.
When we’re making architectural choices for framework use in our app, one tradeoff we can take part in balancing is compile time vs. run time performance. Apps that dynamically link frameworks rather than compiling and linking source files directly will generally take less time to build. Swift takes a long time to compile on our big projects, and reducing what we need to compile and link to can have a significant impact on our productivity across a team.
The idea of linking all our dependencies as dynamic frameworks and even dividing our app up into frameworks may sound great—a lot of people came away from WWDC 2014 with the impression that dynamic frameworks are destined to be the one true way to share code on our platform. However, consider that each embedded framework we add to our project increases startup time, and that can add up to a noticeable effect. In a WWDC 2016 session on Optimizing App Startup Time, Apple provides this advice about using dynamic frameworks: “So you absolutely can and should use some, but it’s good to try to target a limited number, we would, I would say off hand, a good target’s about a half a dozen.” We’re told to limit our dynamic framework use to about six. Six! That’s a really interesting piece of information, especially considering our two popular dependency managers both steer us towards dynamic frameworks—more on that later.
We have run into issues with building projects that have embedded dynamic frameworks on our continuous-integration (CI) server. It turns out that if the embedded framework project doesn’t contain the build configuration that the app is being built with, AdHoc for instance, we can get into a situation where we have a build failure with “MyModule not found” when we write an import statement such as import MyModule. You’ll either have to modify the framework project to include your build configuration, or you can add a new framework search path that tells the app where to find the embedded framework build artifacts. Since we prefer to not modify our third-party dependencies, we’ll adjust our framework search path. In Build Settings, you can add user-defined variable like this:
Then you can reference that variable in your framework search path by adding a new entry:
When there is a mismatch in build configurations, the embedded framework defaults to Release. What we are doing here is when we build the app with the AdHoc build configuration, we should look in Release build artifacts for embedded frameworks.
We have also had challenges with code-signing embedded frameworks on our CI server. Our best advice is to make sure that if your build scripts select and unlock a keychain, that all of the signing certificates that might be necessary to build the app are in that keychain. We do expect this issue to be alleviated with Xcode 8 which removes the requirement that embedded frameworks have code signing settings.
Universal Static Frameworks
If you are building something important, say something you’re basing a company on, we advise building a universal static framework, and writing it in Objective-C until Swift achieves ABI stability. In Part 1 we talked about minding your manners with regards to resource usage and how it’s important to be a good platform citizen. Shipping a dynamic framework and contributing to the host app’s dynamic framework load is at odds with that recommendation. Another reason we might choose to ship a static framework is if we would like to avoid sharing source code with framework users since static frameworks only require us to expose the public interface.
You might wonder why some well-known third parties have released SDKs written in Swift. Facebook recently announced a new Swift SDK. Consider that a company as engineering-heavy as Facebook has lots of resources, so putting out a beta version of their framework that is written in Swift can be thought of as “leaning into the future”, and they still offer an Objective-C SDK. We’ll probably see Facebook offer that Swift SDK as a static framework when that option becomes viable. Back in May 2015, DropBox released an iOS SDK for their v2 API that’s written in Swift. It garnered negative feedback from the developer community because they shipped it without Objective-C compatibility, and the community reacted by building their own unofficial SDKs. Now Dropbox has an Objective-C version of the SDK in the works due some time in August 2016. It’s probably acceptable to take Facebook’s approach which still leaves choice in the hands of their users, but the Dropbox situation left people wanting.
Let’s say for some of the reasons outlined above we’ve decided to build a universal static framework in Objective-C. What comes next? As we mentioned earlier, Xcode doesn’t have a “Universal Static Framework” project template. That means we’ll have to do more work ourselves. What we do have in Xcode is a template for building a static binary.
The basic steps for building a universal static framework are:
- Build a static binary for each platform you will support.
- Use lipo to concatenate each of your static binaries together into one fat binary.
- Package your fat binary, header files, and resources in a directory that ends in .framework.
The details for writing scripts that accomplish packaging a universal static framework are well-covered—check out iOS-Framework. While the setup and maintenance of a static framework are comparatively complicated, this is our recommended solution for any framework that’s going to be distributed to a wide audience in any capacity that could be considered “serious.” We hope to be writing Swift universal static frameworks next year with Swift 4, and maybe by then we’ll get some more support for building them from Xcode without requiring custom scripts.
The type of framework we build has an impact on our distribution options. The simplest form of distribution is simply to host your framework in a public place like GitHub and to provide really straightforward manual installation instructions. Beyond that we can explore using popular dependency managers, but do not want to impose our dependency management preferences on our users so we’ll make these channels optional. We recommend playing nice with the community, and you should strive to offer your framework through as many channels as you can support.
CocoaPods is the most popular and fanciest dependency manager for our platform, and it was inspired by how easy it is to share code in the Ruby ecosystem with RubyGems and Bundler. iOS development picked up at a time when Ruby on Rails had really hit its stride so many Ruby developers began writing iOS apps. Ruby projects can easily have 30 or 50 or more third-party gems as dependences, and the community is really driven by open-source and not reinventing the wheel.
While CocoaPods is a very successful project, it also brings a form of tension to our platform—sharing code that has to be compiled to specific architectures and with project file configurations is simply harder to do than sharing Ruby scripts. Also, on the whole, we find iOS engineers are somewhat less open-source driven than their Ruby counterparts, and being very dependent on third-party code is more risky when the community is divided on how much code sharing we should do.
The core CocoaPods experience is really nice which drives its popularity: we build up a Podfile and pod install and off we go. We’ll have to accept that CocoaPods seeks to be a comprehensive solution and that means we’ll give up our project file for a workspace file and also give up control of how our dependences are linked into our project. In addition, we’ll have to maintain a healthy Ruby environment on our development machines and CI servers. For those tradeoffs we get really easy dependency management and updates.
One catch of CocoaPods is that our project can either use CocoaPods to import static frameworks, or dynamic frameworks, but not both. If you write use_frameworks! in your Podfile, we’re in dynamic framework-only mode. Recall that we said Apple recommends just six dynamic frameworks, and CocoaPods seeks to bring a Bundler-like experience to iOS, and Ruby programmers are accustomed to using tens upon tens of third-party RubyGems. The canonical example has use_frameworks! set so we’re being guided towards using lots of dynamic frameworks.
Despite those challenges, Google has endorsed CocoaPods as the right way to do iOS dependencies and even mentioned it in their I/O 2015 keynote. We have encountered several third-party frameworks with CocoaPods-only installation and no fallback options.
The Carthage project seeks to provide a simpler dependency manager than CocoaPods. Where CocoaPods wants to be a solution for dependency management, packaging, distribution, and discovery, Carthage is focused on managing and building dependences.
Carthage only works with dynamic frameworks. That’s a show-stopper for our recommended path—we encourage vendors of much significance to provide static frameworks. Using Carthage is just as easy as CocoaPods, and Carthage doesn’t modify the way we build our project. We are responsible for discovering and adding frameworks to our projects ourselves.
Because Carthage builds frameworks directly outside the host app project, the Carthage team has needed to introduce a run script build phase that strips out architectures that are not required. Without putting in that script, you’ll likely be rejected if you try to submit to the App Store.
Swift Package Manager (SPM)
The Swift Package manager is an Apple-run effort to make the one true dependency manager for the platform. They hired Max Howell of Homebrew fame to work on the project. The original community proposal is a good read to get a feel for the goals of the project. Carthage and CocoaPods maintainers are of a “wait and see” mindset with regards to the SPM.
In its present state, the SPM only supports macOS. It’s really easy to install Xcode 8 and try it out following this guide. Rather than a Podfile, we express our dependencies in a Swift file called Package.swift. When our dependencies are specified, we run swift build and our dependences are downloaded and built for us. An interesting feature of this early version of SPM is we can specify whether we would like our dependencies built as static binaries or as dynamic libraries on a per-dependency basis.
The interesting part about the SPM isn’t what it is now, but what it might be soon. We expect that Xcode integration will accompany the first production SPM release and that Apple will push the platform to make packages very easy to share. When that happens, we believe the community will steer towards more code sharing and that we might approach what CocoaPods and Carthage are trying to achieve.
Now that we have gone through the factors that influence the choices we make for the type of framework we build and how we distribute it, let’s take closer look at what goes into building a framework. Rewind back to the beginning to the first question we should ask: What is the problem we are trying to solve? Ada Turner has already taken care of the first question for us—have a read through her Reimagining UITraitCollection article where she builds and introduces Briggs, an intuitive API for working with UITraitCollections programmatically which improves the overall experience of working with them. Our mission is to turn Briggs into a seaworthy iOS framework.
Up to now, Briggs exists as an API written in Swift and delivered in a sample project. Our goal for Briggs is to package it up so we can offer it to a wide audience for general use. As we discussed earlier, static frameworks containing Swift code are for all intents and purposes excluded as an option.
Because Briggs is written in Swift and will always be open-source, we’ll be shipping a dynamic framework. This means we’ll be able to offer Briggs via CocoaPods and Carthage, and we’ll include manual installation instructions. As we talked about previously, the choice of whether to link statically or dynamically should really be left with the framework consumer, but as a community we’re divided between opposing interests and some of those are at odds with the direction Apple is going. Even though we aren’t completely behind the idea of distributing dynamic frameworks as a de facto standard, considering our options we’ll go ahead with it and also encourage our users to simply copy-paste the source files in their project if they would rather spend compilation time rather than startup time.
After we create our new Cocoa Touch Framework project, the first step will be to transfer our framework source files from the demo project to the framework project. We’ll create a new top level directory called Source and toss in every .swift file that isn’t AppDelegate.swift or the the three view controllers. At this stage, we can build the framework project, and see Briggs.framework get generated in the Products group. We could even drag the project back into the sample project, and add the framework as an embedded binary!
With all of that set up, we can jump into BriggsViewController.swift and add import Briggs to the top. Unfortunately, compiler errors still abound! If we command click into Briggs to take a look at the generated framework header, we find it rather empty. In Swift, there are three access levels: private, which is only visible to the declaration and extensions in the same file, internal, which is the default and visible at all scopes within the module, and public, which is visible to other modules.
We’ll need to mark everything we want our framework to expose to consumers (our API) public for it to appear in the Briggs header and be available to use. Briggs doesn’t happen to have any internal or private components, so we’ll be marking all of our protocols, types, and functions as public. Now we should be able to build and run our sample project, and command-clicking into the Briggs header present us with a series of declarations that look like this:
At this point, you could consider our task technically finished—we have a standalone framework project that we are able to integrate into other projects, import, and access Briggs API. Anyone could clone our repository as a submodule and be well on their way to writing beautiful adaptive interfaces. However, building and shipping frameworks isn’t just about extracting source files and marking APIs public. As framework engineers, we are creating and maintaining a polished product that will be publicly available to our friends and colleagues in the developer community. We want to deliver a product that is well-documented, clearly-licensed, generally polished, and supports common integration methods.
Licensing can be simple enough if you let other, more learned people do all the heavy lifting for you. We’ll be using the MIT software license for Briggs to leave our users with the freedom to use it however they want. Next, let’s step up our documentation game by giving our source files a thorough makeover with Headerdoc. It looks like we made a decent effort with our first attempt, as most of our files have a short descriptive comment by their declaration. However, now that Briggs is going to be published as a framework, we’ll need to make sure that every declaration has a descriptive comment that fully utilizes Headerdoc format.
We want to make getting Briggs as easy as possible, so we’ll give consumers of our framework two popular options for integration: Carthage and CocoaPods. Carthage compliance is trivial: if you have built a dynamic framework and tag your releases on GitHub with Semver, you are Carthage compliant. Carthage consumers of our framework need simply to add a single line to their Cartfile, run carthage update, and follow standard framework integration procedures.
github "auswahlaxiom/Briggs" ~> 1.1.1
CocoaPods integration is much more involved for both the framework provider and consumer. In addition to tagging our release, we’ll need to create Briggs.podspec to specify some project metadata, check our compliance with pod spec lint and pod lib lint, and push to the trunk. Consumers will need to create a Podfile and run pod install. Note that since Briggs is a dynamic Cocoa Touch Framework, our consumers’ podfile must specify use_frameworks!, which will lock them into using CocoaPods for only dynamic frameworks—remember that Apple recommends you limit your dynamic framework usage to six. In spite of all this, CocoaPods remains a very popular, near community-standard system for framework integration, and we would be remiss not to support it.
platform :ios, '9.0'
target 'YOUR_TARGET_HERE' do
pod 'Briggs', '~> 1.1.1'
Finally, let’s fill README.md with some beautifully-formatted, helpful information. The README is one of the most important files in any project, and for as framework providers, it is absolutely critical to make sure ours is up-to-snuff. The README is our potential users’ first impression of our product, so not only do we need to be informative, we need to do it with style. As a general guide, a framework’s README should:
- Introduce the framework
- Show off a little taste of what makes it so cool
- State the problem it is solving and summarize how it will solve it
- Guide the user through supported integration methods
- Offer detailed documentation on what the framework does and how to use it
- Wrap up with credits and licensing information
Look at all those shiny shields!
Now, we can truly say Briggs is a polished framework and ready for action. We’ll fail on our own checklist items from Part 1 related to writing in Objective-C and delivering a universal static framework, but we’re making the best choices we can for packaging Swift APIs. We’ve created a wonderful framework that can be added with Carthage, CocoaPods, or integrated manually as a submodule. We’ve added Headerdoc to every declaration, a LICENSE supporting open-source software, and a beautiful, descriptive README with shiny badges. All that’s left to do is submit a pull request into master! … Or is it?
We may have successfully shipped version 1.1.1, but what does the future hold? iOS 10 and Swift 3 are looming on the horizon, and when they arrive, we will have plenty of source-breaking changes to deal with, and we’ll need to make sure all of our APIs still work properly when building against the new iOS SDK. And what if the new iOS version were to break something? Our test coverage is currently at 0%—a sorry state to be in! As framework providers, we want to give our users absolute confidence in our product’s integrity, so adding exhaustive functional and UI tests will definitely be on our agenda for future versions. We’ll also want to keep our eye on Swift Package Manager, and be ready to support it as an integration option as soon as it becomes available to iOS. Finally, when Swift achieves ABI stability, we’ll want to revisit our choices regarding how we packaged Briggs; perhaps a universal static framework will be a more desirable option. The responsibility of owning and maintaining a framework certainly does not end with shipping for first public version, but continues on as the platform, tools, and SDK continue to evolve.
The Journey Continues
Perhaps when the Swift Package Manager is ready for primetime with full Xcode integration we’ll simply specify what we need in a Package.swift file along with how we want to link it and Xcode will take care of the rest. In the meantime our community is fragmented around all of the options and tradeoffs we contend with. We’ll conclude that vendors whose businesses are built on delivering great SDKs should ship universal static frameworks written with Objective-C today. It’s also a good idea to be in the process of building Swift versions for when Swift achieves ABI stability—hopefully that happens with Swift 4 in 2017. Smaller Swift projects can be offered as dynamic frameworks until next year, and users should either link them dynamically or link directly to source files depending on how they want to balance their compile-time vs. run-time tradeoff.
So Long and Thanks for All the Fish
And with that we wrap up our series on iOS frameworks—thanks for joining us! For your reference, here’s the full collection of articles that culminated in this finish.
- iOS Frameworks Part 1: A Treacherous Voyage Through Murky Waters
- iOS Frameworks Part 1.5: Apteligent
- iOS Frameworks Part 2: Build & Ship
- Adaptive Interfaces Part 1: How UITraitCollection Changed Everything
- Adaptive Interfaces Part 2: Reimagining UITraitCollection with Briggs
And here’s our new dynamic framework: Briggs on GitHub!
We’re excited to see where Apple steers the platform and we hope that the story around sharing code gets much simpler and fun. Give Briggs a try on your next project and let us know what you think.