Converting an App to use Swift Package Manager

Chris Thomas
8 min readMar 23, 2022

--

When building an iOS App you’ve got a number of package managers to choose from. Swift Package Manager (SPM) is one of the newer kids on the block. It allows you to use Swift itself to define your dependencies and in many ways I find it much simpler to use than my experiences with Cocoapods or Xcode frameworks.

At John Lewis & Partners, we have an App we have been working on modularising that uses Cocoapods and internal frameworks. My colleague Colin has written a blog post in the past about the issues and solutions we’ve come across to have a 3rd party SDK imported into one of our internal frameworks. The idea is that using SPM will reduce this complexity drastically. It should also make the general management of frameworks easier day to day.

While developing though, I find the process of setting up a framework cumbersome. I’ve used SPM a lot for our Vapor microservices and decided it was time to have a go at moving the App over to use it for all our dependency needs, both internal and 3rd party.

Day 1 and 2 (Initial packages and Resources part 1)

Our existing app is broken up into 2 packages, each with a number of frameworks. Our Core package contains code that we share across a number of apps, it handles common UI components, networking, some tracking things. We also have a package that contains the things specifically related to the App only.

Day 1 was spent making a new ‘ios-core’ Swift Package to house all the core code. I decided to make the other frameworks (those specific to the single App I was working on) part of a local package. I will say this now, I am not wed to having a separate repo or local package for these libraries and targets. As a team we may decide one way is better or worse for our development needs. I wanted to experiment to start with though, and experience as much about SPM as possible.

Day 2 I spent making a small demo app to prove that the changes made in Xcode 12 to enable Swift Packages to consume resources still worked. To that effect I attempted to get our custom fonts working. This was actually easier than I feared.

There are 3 ways that Xcode will try and consume resources in a Swift Package¹:

  • automatically
  • defining their existence in the target of your package file using .process
  • defining their existence in the target of your package file using .copy

Font files ( .tffand .otf) are of the type that need to be added using .process. The Package.swift code ends up looking something like this:

.target(  name: “Components”,  dependencies: [“Icons”, “Images”, “JLUI”, “Layouts”, “Plexus”,       “Tracking”],  resources: [.process(“Resources”)]),

The process part tells Xcode to try and process the files it doesn’t automatically understand. In this instance all the files in the folder Sources/TargetName/Resources are processed. You should be able to use any folder structure you like, but the convention is to use a Resources folder. This would also be the place to put any storyboard or xib files you may want to use.

Having added the resources to the package, and defined how to consume them you need to make sure your code is pulling them in correctly.

I’d done some reading on SPM already, so I knew that when adding resources to a package a .moduleextension on Bundle was created, and this is what you should use when trying to access the resources. Registering fonts requires code that looks something like this:

private func registerFont(name: UIFont.Name) {let bundle = Bundle.moduleguard let url = bundle.url(forResource: filename, withExtension: ".otf"),
let fontDataProvider = CGDataProvider(url: url as CFURL),
let font = CGFont(fontDataProvider)
else {
return
}
CTFontManagerRegisterGraphicsFont(font, nil)
}

Here you can see I use Bundle.module to tell my code which bundle to use for the url to get the font files I’ve added.

Tip: We will come to this later but it can be really useful to write your own public extension to enable you to access each individual package bundle later on. Something like:

public extension Bundle {
static var LibraryName: Bundle {
Bundle.module
}
}

Luckily for me all the code to consume the font files existed already, so all I had to do was change how the bundle was called and it all worked.

Days 3–7 (Resources part 2)

Having managed to create the core library I wanted and seemingly having everything build correctly in Xcode, I moved on to look at converting the rest of the frameworks. As I mentioned I decided these could all be moved to be an internal package.

The process of moving all the code around was somewhat laborious, but not actually difficult. By about lunch time on day 3 I was fairly happy with the speed I was moving. The nice thing about SPM was that I’d managed to make importing a few of our third party libraries much easier; I could now just define them as dependencies in the target I created, rather than having to go through a number of hoops to get Cocoapods to manage them and then import them to the internal framework.

It was when I first tried to run the main App that I encountered the first issues.

“Could not find a storyboard named ‘Name’ in bundle NSBundle”

This was odd. I’d definitely included the storyboard file in the target folder, and according to the latest docs the latest Xcode version should be able to automatically recognise these types of files. I tried doing the same as above just to be sure and used .process but that didn’t help.

The storyboard I was trying to use was referenced from the Main.storyboard as a reference. Turns out that was part of the problem. After a few days of debugging, going a little bit bonkers, and reaching out to other developers I eventually found the right combination of words in google to turn up a post² on stack overflow that had some answers.

In Main.storyboard I had to change the reference to the storyboard I couldn’t find.

The bundle I needed to reference took a format that I had not seen before.

In the package that was exporting the storyboard I also had to make sure that any exports were defined like this:

Previously, while using internal frameworks, a developer would have selected Inherit Module from Target , now we can’t do that and have to change everything. To do this I used the following commands in terminal:

grep -rl "customModuleProvider=\"target\"" * | xargs sed -i "" 's/customModuleProvider=\"target\"//g'

to remove the backups after making the alterations.

Finally having made these changes there was one final step, the crux of the problem as described by the user on stack overflow as follows:

If you aren’t importing and using a package’s target anywhere in your code, Xcode tries to optimize by not loading that package’s resource bundle (it is probably an oversight on Apple’s part that storyboard references alone aren’t enough to trigger this). So a workaround is needed to trick Xcode into making an SPM package’s bundle available if you are only using its resources in a storyboard.

Thanks goes out to user mm2001 because without this post I never would have figured out how to solve it. It boils down to tricking Xcode into loading the resources into the bundle by adding the following code to your AppDelegate initialiser:

override init() {
super.init()

let bundleNames = ["PackageName_LibraryName"]
bundleNames.forEach { (bundleName) in
guard
let bundleURL = Bundle.main.url(forResource: bundleName, withExtension: "bundle"),
let bundle = Bundle(url: bundleURL) else {
preconditionFailure()
}
bundle.load()
}
}

Day 8+ (Resources part 3)

Day 8 started much like days 4–7. Unable to find a table view cell in the bundle I was looking for. I immediately started to think ‘ahhhh I faced this issue with the storyboards, I bet its something similar.’ I then spent another 3 days trying to figure out what it was.

It wasn’t the same problem.

This was the next lesson. Bundles in SPM are REALLY important and using the right references to the right bundle for the right xib file needs to be done in a particular way.

In this instance a shared protocol was defined in Package1. The xib was defined in Package2. The function to instantiate the xib was called from Package2, then tried to find the xib in Package1. This won’t work. A quick change to this common protocol to accept a Bundle type into the function fixed all that up.

We went from

public protocol Instantiatable: UIView {
static func instantiate() -> Self
}
public extension Instantiatable {
static func instantiate() -> Self {
let nib = UINib(nibName: String(describing: Self.self), bundle: .module)
return nib.instantiate(withOwner: nil, options: nil).first as! Self
}
}

to

public protocol Instantiatable: UIView {
static func instantiate(module: Bundle) -> Self
}
public extension Instantiatable {
static func instantiate(module: Bundle) -> Self {
let nib = UINib(nibName: String(describing: Self.self), bundle: module)
return nib.instantiate(withOwner: nil, options: nil).first as! Self
}
}

This next one seems obvious in retrospect so I won’t go into it too much and you can all have a good laugh that not only did I miss it when building the demo project, I completely forgot I’d learnt the lesson when it came to trying to make the real project work. You must add the libraries from your packages into the App under the General/Frameworks, Libraries and Embedded Content section.

The final issue I’ve faced so far is how cells are registered. Our current code would register cells and views as follows:

tableView.registerCell(CategoryCell.self)tableView.registerHeaderFooter(CategorySectionHeaderView.self)

But this just doesn’t play nicely with SPM. Now I have to do:

tableView.register(UINib(nibName: "CategoryCell", bundle: .module), forCellReuseIdentifier: "CategoryCell")tableView.register(UINib(nibName: "CategorySectionHeaderView", bundle: .module), forHeaderFooterViewReuseIdentifier: "CategorySectionHeaderView")

In this instance the code is registering the cell and view to the table view in the package that defines both. But this is where my hint from earlier came in. If you expose a public static variable that defines the name for the bundle of the package, you can replace the bundle: .module with something a bit more specific. This will allow you to have Package1 define the xib, but Package2 register the cell or view.

And Finally…

Before setting out on this journey I was convinced that SPM was going to be the way we wanted to go. I have been excited to start using it, to do away with the internal framework system we’ve been using up to now. My use of it in our Vapor APIs has been pretty much effortless, so I was really looking forward to a simple migration. Looking back it is nice to know I can sometimes still be naive!

SPM as a package manager for your iOS app does have a lot of benefits, but it has a lot of pitfalls too that aren’t totally obvious or, in my opinion, particularly well documented. Hopefully some of them are now pulled together in this one post!

Does that mean I think we will move to use SPM? At the moment yes. It feels like it makes the management of packages a lot easier than our current methods. It codifies the definitions in a way that I find much easier to understand.

My biggest concern is how many ‘work arounds’ I had to do. I also worry how much will continue to change in future versions of Xcode.

This will be one for the team to decide if the pros of moving out way the cons.

[1] For more info about using process or copy see the Apple docs here

[2] https://stackoverflow.com/questions/58000140/swift-package-manager-storyboard-bundle

#engineeringinjlp #spm #swiftpackagemanager

--

--

Chris Thomas

I’m currently a lead iOS Developer working for John Lewis & Partners.