How to use CocoaPods with your internal iOS frameworks

Alexander Freas
6 min readMay 14, 2019

In my previous post about how to build features as frameworks, I discussed how you can improve your team’s productivity by using iOS frameworks directly in your project’s workspace. The example I walked through was quite simple and gave a starting point for this approach, but many of us need to build something more complex, which usually includes third party dependencies. It can be a bit tricky to integrate CocoaPods into your project when it is structured like this, so this is what I would like to explain in this tutorial.

CocoaPods or Carthage?

When deciding on a dependency manager for your iOS project, most people choose between two options: CocoaPods or Carthage. CocoaPods has been the standard bearer for many years now, and is a very mature codebase with tons of packages to choose from. The way it integrates code into your project is by creating a framework target for each of the included Pods, building the frameworks, and copying them into your main app module using a special script.

Carthage works a bit differently. It is not dependent on integration into your project, instead it downloads and builds the code for each dependency into their own frameworks that can then be imported into your project. This means dependencies must be integrated into your project manually, but it gives your project independence from Carthage itself.

What you choose as a package manager is up to you, and you can even use both if you need to. Carthage has the advantage of not rewriting any of your project file, while CocoaPods only takes a few automated steps to get you started. In this article, I will go over how to integrate CocoaPods into your internal frameworks.

Getting started with CocoaPods

Let’s begin with the small example project I wrote about in the previous article. I have made all the following changes on a branch in the repo called use-cocoapods, but you can start with what’s already on the master branch. If you get stuck, check out the code to see the final result.

Make sure you have CocoaPods installed, either system-wide or using Bundler with CocoaPods defined in your Gemfile.

If you’ve used CocoaPods before, you probably know that the usual way to get started using it is by running pod init in the directory that contains the .xcproject file. Running this command creates a workspace that CocoaPods can use to manage the dependencies. Because we created our own workspace already that contains our internal dependencies, we will need to do something a bit different.

With a text editor, create a new file called Podfile in the root directory of the git repo. Add the following lines to it:

use_frameworks!workspace 'SimpleCounter.xcworkspace'

This tells CocoaPods that you want to use an existing workspace to contain your Pod dependencies. Run pod install in the same directory to confirm that it’s working. This will not install anything but will integrate the Pods project into that workspace.

Integrate a CocoaPod

Let’s integrate the UICircularProcessingRing Pod to our app and framework. This is a simple ring shaped progress bar that we can use to show counting progress toward a specified goal.

UICircularProgressRing Example

Add the following to your Podfile:

target 'SimpleCounterFeature' do
project './SimpleCounterFeature/SimpleCounterFeature.xcodeproj'
pod 'UICircularProgressRing', '6.1.0'
end

This will apply the UICircularProgressRing CocoaPod to the SimpleCounterFeature framework target. This all looks familiar, except that you specify the exact .xcodeproj file that needs the dependency. We need to do this because in the root directory with our .xcworkspace file, it’s not clear to CocoaPods where the target is that needs the dependency. Usually, the .xcproject file for the main app module and the CocoaPods-generated .xcworkspace file live in the same directory, so CocoaPods will find it by default.

Then, run pod install. This will download and apply the UICircularProgressRing dependency to the SimpleCounterFeature framework target.

Now, let’s test to see if the integration was successful. Open the CounterViewController.swift file and add

import UICircularProgressRing

to the top of the file. Select the SimpleCounterFeature target in the targets list and build. The build should succeed.

Now try building and running the SimpleCounterApp target and see what happens. The build will succeed, but the app crashes on startup! We get the following error message:

dyld: Library not loaded: @rpath/UICircularProgressRing.framework/UICircularProgressRingReferenced from: /Users/akfreas/Library/Developer/Xcode/DerivedData/SimpleCounter-awbumravmznuqofjwyrvfwfaoohc/Build/Products/Debug-iphonesimulator/SimpleCounterFeature.framework/SimpleCounterFeatureReason: image not found

Why is that? We added the dependency to our SimpleCounterFeature and it built just fine, and so did the app! Why does it say library not loaded?

Gotta link em’ all

What’s missing is the actual compiled UICircularProgressRing binary that the SimpleCounterFeature requests when it is loaded. This is because iOS frameworks are dynamically linked, meaning they only reference their dependencies rather than packaging them with the compiled binary image. Doing this generally reduces app size, as dependencies that are shared between binaries in the executable image (in this case, our app module) are not duplicated and instead loaded only once by the dynamic linker. In this case the linker is an Apple utility called dyld, which threw the fatal error above.

In other words, the app has no idea where to find the UICircularProgressRing binary because it was not packaged with either the SimpleCounterFeature framework or the main app module. When dyld loads the SimpleCounterFramework, that binary is requested but cannot be found, causing the crash.

What we need to do is add the UICircularProgressRing framework to the linked binaries of the main app module. This is done by adding these lines to your Podfile:

target 'SimpleCounterApp' do
project './SimpleCounterApp/SimpleCounterApp.xcodeproj'
pod 'UICircularProgressRing', '6.1.0'
end

Now, run pod install, clean the app target, and try building and running the app again. You may have to delete the app from your simulator or device if you don’t see the counter interface on startup.

Dependencies sorted, let’s build something!

Now that we have the UI binary available to both targets, we can use it. I integrated the progress view into the framework, which you can check out in the code on github. There’s nothing really unique to building with frameworks here, so I’ll leave that up to you to look at. What we are left with is an interface that looks like this:

Success!

Common Issues

There are a few problems you can run into when integrating more dependencies into your app module and framework.

App is still saying library not loaded

If you are getting the same error we had above, where dyld is complaining that it cannot find your internal framework, make sure you have the framework listed in both Linked Frameworks and Libraries and in Embedded Binaries in the General section of your build settings for your main app module. It should look like this:

Issues when integrating into another architecture (watchOS, MacOS)

If you are building your framework for another architecture, such as WatchOS, you will need to create another build target in your framework project that builds your code for that architecture.

By setting the Base SDK to watchOS and the Valid Architectures to armv7k, you can build your framework for watchOS and integrate it into your main watchOS app module in the same way you did for the iOS app. There are some more difficulties that can cause snags, but I won’t go into more detail here.

Thanks for reading!

If you have any questions, have suggestions, or encountered any problems while doing this in your project, feel free to leave a comment below!

--

--

Alexander Freas

Entrepreneur and founder, Truffle.bot. I’m working to make your life on Slack a bit easier every day. Writing about Slack tech and non-tech topics.