Supporting both Swift Package Manager and CocoaPods in your library
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
App Layout with SPM
Resource Bundle Comparison
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.
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.