Modern Modular Apps With Xcode 12 and Swift Package Manager
Last year, after Apple released the first beta of Xcode 11 with native support for Swift Package Manager (SPM) built in, I wrote a post on how we can leverage this to build modular apps with Swift Packages.
In it, I touted that it would make it possible to ditch Cocoapods as a dependency manager, but what we ran into quickly was a limitation with being unable to include resources in a Package, like colour assets, fonts, storyboards/xibs etc.
Thankfully, Xcode 12 includes the latest version of SPM which now allows Packages to include any resources, by including a new static property on Bundle
named .module
. This Bundle
instance references the current module, rather than just being able to reference the current target.
In this post, we’ll update our project from last year to include some assets which we can expose from the Package for use in any other module or target!
Step 1
To continue, first follow the steps in last year’s post, then come back here and we’ll carry on from there.
NOTE: You’ll need to use Xcode 12 instead of Xcode 11. Since you’ll be using Xcode 12 and a new version of SPM, you may encounter some slight differences in how to set up a module from last year’s post.
Notably, you’ll have to provide a package name as well as a product name when declaring dependencies in FantasticFeature. You can just use the same value for both.
Step 2
Welcome back! If you’ve followed along and got the project building from last year, you should have:
- A workspace named AmazingApp;
- A project also named AmazingApp;
- A Swift Package representing a feature module called FantasticFeature;
- Another Swift Package representing a shared module called Common
Make sure you’ve opened the workspace (not the project), and in the Common module we’ll add a new asset catalogue for our colours that we want to use across our app.
In the workspace, create a new file (command + N) in [Common] ➡ [Sources] ➡ [Common,] choose Asset Catalogue and click Next:
Since you can have multiple asset catalogues in any module, lets call this one Colors.xcassets in case we add others for images etc in the future.
Step 3
You should now have an empty asset catalogue called Colors.xcassets:
Let’s add some colours to it! Click the plus icon (not the one in the workspace, the one in the catalogue), and then click Color set:
Name your colour something like primarySurface
(which we’ll later expose in code), and then set whatever colour values you want for light and dark appearances:
Step 4
Now we can expose this in code, so in the Common module create a new Swift file called Colors.swift and create a public
extension
of UIColor
to expose the colours from the catalogue:
You might get the error above about using a UIColor
initialiser that is only available in iOS 11, since the Common module doesn’t yet declare a specific platform. You can fix this by opening up the Package.swift file in the Common module and explicitly setting the platform to iOS 11 or newer:
Looking back at Colors.swift, you can see we’re creating a static constant on UIColor
(line 5) named primarySurface
, which creates a UIColor
by name from the Colors.xcassets catalogue.
The most important part of that code is the .module
which is a static
constant on Bundle
representing our module.
In effect, that line of code is saying “create a UIColor
from a colour named "primarySurface"
, defined in an asset catalogue in this module
and store it in a static
constant also called primarySurface
“.
Step 5
Now we’ve exposed our colour in code, we can use it in code in this or other modules. As an example, let’s use it to change the background colour of the view controller that is shown when we run the app in the simulator.
In ViewController.swift, in the viewDidLoad
function, set the view
’s backgroundColor
to our newly exposed primarySurface
colour:
You’ll need to import Common
at the top of the file, and once you do that you can run the app and the view’s background colour will be whatever colour you chose in Colors.xcassets:
As an added bonus, because we defined both a light and dark variant of that colour, the system automatically knows how to select and change to the right one. You can test this by clicking the Environment Overrides button in Xcode and changing the Appearance:
Unfortunately, you can’t set the colour directly in a Storyboard or Nib unless the colour is defined in the same module as the Storyboard/Nib, but that’s the same when using regular Xcode projects as modules instead of Packages.
Final thoughts
I love how quick it is to create a new module using SPM, and although this example is fairly simple and just exposing a colour, that is only possible because a Package can now include and references resources just like legacy Xcode projects.
In a matter of moments, you can create a new module, declare its dependencies which Xcode will automatically resolve for you, and now include Storyboards, assets, fonts and anything else you want.
There are still some downsides to using SPM, like clearing derived data requiring your packages be re-resolved (which also means cloning any remote repos like Alamofire again). But personally, I would take these small annoyances over the pain of creating and managing Xcode projects as modules.
SPM is fast becoming the only dependency manager you’ll need for developing Swift apps and libraries, without the need to manually run any commands in Terminal or manage a bunch of Xcode projects in a workspace or worrying about dealing with Xcode project file conflicts when creating a pull request.