Supporting both Swift Package Manager and CocoaPods in your library

Robert Manson
Clutter Developers
Published in
4 min readAug 20, 2021

Gradually Transitioning to SPM

CocoaPods has been a part of the iOS ecosystem for 10 years now and has greatly improved the workflows of many iOS developers. However, like many companies, Clutter is in the process of transitioning our dependencies from CocoaPods to Swift Package Manager. Having first-class support for SPM within XCode is a huge draw for us, however, due to the the size and complexity of our apps, the transition is a large undertaking. In order to maintain productivity, we need to support both dependency managers while we gradually transition over.

Previously, we used the CocoaPods resources syntax in our frameworks, which resulted in resources being copied into the framework alongside the binary. This had the advantage of our resources (Xibs, Storyboards, .xcasset) usually being in the same bundle (the framework) as the code that would typically use it.

For example, in the past, the following would have been a reliable way to find the XIB for a particular class:

Bundle(for: MyLibraryTableViewCell.self)

Migrating towards SPM means that we are going from dynamically linked to statically linked libraries. With a statically linked library, our code is now in the same bundle as the main executable (the app bundle) so the approach above, of simply referencing where our class was defined, does not work.

To avoid having code or behavior depend on what dependency manager we are using we wanted to configure our Pod and SPM package to lay out assets in a similar file structure. This would allow loading those assets (either programatically or through IB/Storyboards) to be consistent.

Since SPM must use a dedicated resource bundle (since there is no Framework bundle), we decided to standardize on using a resource bundle for CocoaPods as well.

Exploring the Resource Bundle

The following illustrates the differences in how an app is laid out when compiled with static dependencies versus CocoaPods frameworks. Notice that although our resource bundle is named the same and has the same contents, it is in a different location.

App Layout with CocoaPods

Our example app, built with CocoaPods and Frameworks. Our binary and resource bundle lives alongside the other frameworks in our example application.

App Layout with SPM

Our example app, built with Swift Package Manager. Notice the larger binary size of the executable. Our resource bundle is selected.

Resource Bundle Comparison

Taking a look inside each of the resource bundles: the files in each resource bundle are identical, although, interestingly, some of the files processed by SPM are smaller.

Configuring our Pod Specification

Configuring CocoaPods to place all assets including Xibs in a bundle makes it much easier to then load them from either a Cocoapod or SPM package.

Pod::Spec.new do |spec|

spec.name = "DressCode"
spec.version = "0.15.1"
spec.summary = "Clutter's iOS UI / UX Library"

# cut for brevity

spec.source_files = "Sources/DressCode/**/*.swift"
spec.exclude_files = "DressCode/DressCodeExample/*"

# Note: make sure to use the `resource_bundle` and not the `resources` directive
spec.resource_bundles = {
'DressCode_DressCode' => [ # Match the name SPM Generates
'Sources/DressCode/**/*.xib',
'Sources/DressCode/Resources/Colors.xcassets',
'Sources/DressCode/Resources/Assets.xcassets'
]
}
end

Configuring our SPM Package.swift

For SPM we need to tell the package manager to process our xcasset files:

let package = Package(
name: "DressCode",

// Cut for brevity

targets: [
.target(
name: "DressCode",
dependencies: ["AlamofireImage"],
exclude: [
"Info.plist"
],
resources: [
.process("Resources")
]
),
// Cut for brevity
]
)

Finding the Bundle

Resources not referenced from Storyboards or Xibs need to specify the bundle. The following is an example helper class that will find the correct bundle regardless of whether or package is built with CocoaPods or SPM.

The first candidate is used for SPM, representing the main .app bundle. The second candidate is used for CocoaPods, representing the DressCode framework directory.

public final class DressCodeResources {
public static let resourceBundle: Bundle = {
let candidates = [
// Bundle should be present here when the package is linked into an App.
Bundle.main.resourceURL,

// Bundle should be present here when the package is linked into a framework.
Bundle(for: DressCodeResources.self).resourceURL,
]

let bundleName = "DressCode_DressCode"

for candidate in candidates {
let bundlePath = candidate?.appendingPathComponent(bundleName + ".bundle")
if let bundle = bundlePath.flatMap(Bundle.init(url:)) {
return bundle
}
}

// Return whatever bundle this code is in as a last resort.
return Bundle(for: DressCodeResources.self)
}()
}

So, for example, registering a table view cell would use this helper like so:

tableView.register(DressCodeResources.resourceBundle, forCellReuseIdentifier identifier: "MyReuseIdentifier")

Xibs and Storyboards

Referencing resources in our bundle is straightforward. We just need to remember to specify the module where the asset lives and the bundle is magically loaded for us regardless of how we package our DressCode library.

Referencing a class within our library. Note: Make sure “Inherit Module from Target” is not selected.

Looking Ahead

Swift Package Manager offers closer integration within XCode both for external dependencies and for creating local modules to improve readability, and perhaps most importantly, compile times.

Creating local modules will also require us to be more aware of where we load our assets so that patterns identified here should have utility after we’ve left CocoaPods behind.

--

--