10 Tips For Distributing Your Framework via CocoaPods

Craig Rouse
8 min readNov 15, 2018

--

In my relatively short career of iOS development, I’ve developed something of a love-hate relationship with CocoaPods; when it works, it’s great, but getting it working how you need or expect it to is often challenging. On my most recent project, however, I learnt a healthy respect for it, and I hope these tips will help you make the most of what is really an excellent, community-developed dependency manager, notwithstanding some minor drawbacks.

The following tips are small nuggets of wisdom I’ve acquired mostly through trial and error. The CocoaPods documentation is concise and useful, but sometimes it doesn’t go into quite enough detail for beginners, and in some cases, useful features and options are very well-hidden.

It goes without saying that all opinions are my own, and although I believe everything I’ve written is accurate, I make no guarantees of this. In such cases, I refer you to the standard developer disclaimer:

(1) Testing your Podspec locally before publishing

When I first started out with CocoaPods, testing my Podspec changes before releasing seemed like a dark art. There are lots of instructions available on how to do this, but none were particularly straightforward to the uninitiated. Most instructions talk about setting up your own local specs repo, and pushing your Podspecs into that, then changing the source parameter in your Podfile to point to your local repo.

I found this workflow to be slow and cumbersome, but at the time, I didn’t know a better way, until I discovered a very simple trick: make changes to your Podfile locally, then in your test target, when specifying the dependency, simply add , :path => ‘/path/to/your/spec.podspec’ to the end of the line. This will tell CocoaPods to look for your Podspec in the location you specified, instead of a remote specs repo.

# Example Podfile
# platform :ios, '9.0'
target 'MyApp' do
# Comment the next line if you don't want to use dynamic frameworks
use_frameworks!
# Pods for MyApp
# :path should represent the path relative to the directory containing your Podfile which contains the Podspec
pod "MyPod/Core", :path => '../../mypodspec.podspec'
end

(2) Testing pre-release code on a specific branch

Usually, a Podspec will point to a specific release tag on GitHub, which will tell CocoaPods to look for that specific release, download the code, and build it. However, what if you’re not ready to release your library yet, and don’t have a release tag created on the remote repo? Don’t panic: it’s a well-kept secret, but CocoaPods can also pull code directly from a specific branch, which bypasses the need to have a tagged release on GitHub. You do get a warning when building, stating that you haven’t specified a release tag, but apart from that, it works just fine, and CocoaPods pulls down the latest commit from the specified branch. This does get a brief mention in the documentation, but there are no examples given that show the exact syntax. In your Podspec, where you would normally specify

s.source = { :git => “https://github.com/your-company/your-repo.git", :tag => “#{s.version}” }

… you can instead use

s.source = { :git => “https://github.com/your-company/your-repo.git", :branch => "your-branch"}

Your Podspec will now pull from your specified branch. Remember to change this back before publishing the Podspec, unless you really want users of your pod to pull from a specific branch!

(3) Working with 3rd party dependencies from other pods

Sometimes 3rd party dependencies are unavoidable. Sometimes, that dependency may in fact be one that you have written yourself, which happens to be published in a separate pod. Fortunately, CocoaPods makes the process really easy, and you can even specify separate dependencies for each platform you support. See the syntax here. Dependencies will be automatically downloaded and built when users install your pod.

# NOTE: Dependencies do NOT require an equals sign between the 2 operators
# Dependencies can be an existing pod available on CocoaPods, or an existing subspec within the same file
s.ios.dependency "MyDependency" # will pull "MyDependency" via CocoaPods

(4) Subspecs and optional components

Subspecs are, in my opinion, one of CocoaPods’ best features. Sometimes, you want to give your framework users a choice over which components of your library they install. With Subspecs, you can easily split up your code into separate modules, and users need only install the bits they need. Alternatively, if you want to give them a choice to install the entire framework as a single bundle, you can specify a default Subspec which references the entire project.

Pod::Spec.new do |s|
s.name = "ExampleSpec"
s.version = "1.0.0"
# ...
s.default_subspec = "Complete"
s.subspec "Complete" do |complete|
complete.source_files = "myproject/**/*"
# optionally, exclude files for specific platforms
complete.osx.exclude_files = "myproject/ios/*"
end
s.subspec "Core" do |core|
core.source_files = "myproject/core/*"
end
s.subspec "MyOptionalModule" do |optional|
optional.source_files = "myproject/optional/*"
# MyOptionalModule depends on "Core" ^^
optional.dependency "Core"
end
# optionally, specify dependencies for the entire Pod:
s.ios.dependency "MyThirdPartyPod"
end

(5) Conditional compilation

Sometimes we need to write code that is only active for CocoaPods builds, or inversely, code that is ignored when the framework is built through CocoaPods. An example of this is when our code is separated into modules (separate Xcode targets), and we have dependencies between one or more of the modules; if a module references code in one of its sibling modules, it will need to import that module (e.g. import MyModule in Swift), but because CocoaPods generates a single product (framework) for all Subspecs, this would not be required for CocoaPods, and would result in a compile error.

Fortunately, CocoaPods provides a nifty “active compilation condition”, which allows us to write code that will only be compiled if CocoaPods is in use. As of November 2018, I can’t find any references to this in CocoaPods’ documentation, but maybe I’ve missed something. Come to think of it, I don’t even remember how I came to find out this information in the first place. To make use of this, you can wrap your desired code using the following syntax:

// only import MyModule if NOT CocoaPods (e.g. for Carthage)
#if !COCOAPODS
import MyModule
#endif
// only set myString if COCOAPODS compilation condition is active
var myString = ""
#if COCOAPODS
myString = "CocoaPods"
#endif
print(myString) // prints CocoaPods if framework was built by CocoaPods

I have found this feature extremely useful when supporting CocoaPods and Carthage in the same project, due to the different import styles required.

(6) My pod is installed, but the code isn’t running. Help!

Sometimes, I’ve noticed that after installing a new pod and running the project, even though the pod is successfully installed, and I can see the code in my project, the new code is not executed when the app runs. Usually, this is due to Xcode not detecting changes in your workspace properly, so you end up with a cached build. Cleaning the build folder usually fixes this instantly (Product > Clean Build Folder).

Recently, I also saw an issue where I couldn’t set breakpoints in code included in my Pod. After playing with some Xcode project settings (Debug Information Format, Symbol stripping), cleaning the build folder, and recompiling, this issue went away. I subsequently wasn’t able to reproduce the problem in a fresh project, but I wanted to mention it in case you find yourself in a similar situation. Xcode is a strange beast sometimes, and on occasion, toggling a project setting on and off can do the world of good!

(7) Generating static libraries from Swift code

Always wondered what that use_frameworks! line is doing in your Podfile? That was telling CocoaPods to build a Dynamic Framework, instead of a static library. Before Xcode 9, this was the only option for Swift-based frameworks, so CocoaPods required the use_frameworks! directive if you were building a Swift framework. As of CocoaPods 1.5.0, however, Swift frameworks can also be compiled to static libraries. For apps with a large number of dynamic frameworks, you might see some improvements in startup time by switching to static libraries for some of your dependencies. Simply commenting-out the use_frameworks! line in your Podfile will cause CocoaPods to compile your pod into a static library, instead of a dynamic framework. Since it’s so easy to switch, you can easily try out both, and see which works best for your app.

(8) Trunk

When you’re ready to publish your Pod, you need to register with CocoaPods Trunk. Trunk is the service that lets you authenticate with CocoaPods before you can push your Podspec to the master specs repo. Rather than using the usual username and password authentication, you create a session on your machine using your email address. When you register a new session, Trunk sends you an email to confirm your identity, and the session then commences. You can only push a new Podspec for a package you own; if you need to become a collaborator on a package, the owner must add you using the pod trunk add-owner command. Once your pod is published, you will get a listing on the cocoapods.org index, and your package will be available for general installation.

(9) Public Headers

By default, CocoaPods will make all your source files available as public headers in your framework. This is good for people wishing to debug your framework later on, but sometimes, you may wish to restrict this only to certain files, or indeed remove all public headers. To add only specific files, you can use the public_header_files and private_header_files directives to specify custom file patterns to include in your public headers. If you want all files to be included as public headers, just omit these lines from your Podspec entirely.

(10) Other dependency managers

As a framework vendor, you want it to be as easy as possible for your end users to install your framework, whatever their toolset. Sadly, in the Apple world, there is no official package manager that supports all platforms (iOS, tvOS, macOS, watchOS, Linux). The Swift project is working on Swift Package Manager, but currently this only supports macOS and server-side Swift (running on Linux), which makes it unsuitable for iOS/watchOS/tvOS frameworks. CocoaPods is one solution to this problem, and on the whole, it does a fine job, but it’s still a 3rd party product, and it would be nice if there were an officially-maintained cross-platform tool.

Another commonly-used (and increasingly-popular) dependency manager is Carthage. Carthage is different to CocoaPods, in that it is totally decentralised; it does not rely on a central index of packages. Instead, it works by looking at an Xcode project hosted on GitHub, and building all the “shared” schemes it finds within that project. The schemes will all be built into separate .framework files. As a framework vendor, supporting multiple package managers is an extra burden, but you need to do it if you want to support the widest-possible user base. Each dependency manager has its own drawbacks; for Carthage, I would say its biggest drawback is lack of flexibility. For instance, it’s not possible in your Cartfile to specify specific schemes to be built, which means that you have to build the entire project, even if you only need a module that’s contained in 1 scheme. CocoaPods, on the other hand, is a lot more flexible, but some developers don’t like the way that it introduces a new workspace into your project. On balance, having used both, I tend to prefer the flexibility of CocoaPods over the relative simplicity of Carthage, but you may have a different opinion.

I hope you’ve found this article informative and useful; if nothing else, it will serve as a personal checklist next time I run into issues during Pod development! This is my first technical blog post, and I’d really appreciate any feedback.

--

--

Craig Rouse

Software Engineer for Tealium, the market-leading Data Orchestration platform. All opinions are my own.