You might remember back in 2015 when iOS 9 was introduced, and we were finally given a way to manage all of our assets in one place with Asset Catalogs. A few years later, support for colors was added. However, to reference these assets, UIKit still requires us to reference their names as strings like so:
UIImage(named: "blog-asset")
Images, fonts, and colors all have to be referenced in this way. This has a lot of downsides given that we have to look up the asset names in order to reference them, it’s prone to typos, and there’s no auto-complete. Worst of all, if someone deletes the asset, the code still compiles.
Fortunately, there’s a great tool for this called SwiftGen which solves all of these problems. The image reference above simply becomes
Assets.blogAsset.image
We can also set this up to work with localized strings, fonts, colors, and more. However, you’ll notice when setting up SwiftGen that it’s not all that clear how to make it work with multiple different Asset Catalogs which is common to have in projects to store colors separately from images or even separate images by feature. In this post, we’ll go into the details of how we can set this up. SwiftGen provides a lot of flexibility for installing it into your project. We’ll touch on just one option, but I encourage you to look at the readme file on their GitHub page to see what installation is best for you.
Installing SwiftGen
For my project, I decided to go with the Homebrew installation method described in the SwiftGen readme file. Each installation method will have its pros and cons depending on how your project is setup. In my case, we’re also using Xcodegen which makes it easy to generate an Xcode project from a simple to use configuration file. Since we’ve installed SwiftGen via Homebrew, we can run it directly from a run script phase in our Xcode project’s “Build Phases” tab.
![](http://objectpartners.com/wp-content/uploads/2020/07/Screen-Shot-2020-07-01-at-12.13.36-PM.png)
If you’re using Xcodegen, you can simply add a preGenCommand
to the options:
in your project.yml
file to run SwiftGen.
options:
preGenCommand: swiftgen
Now our project is set up to run SwiftGen whenever the app builds. However, it won’t do much because we haven’t set up a configuration file that SwiftGen can use to find our assets and generate type-safe Swift code.
To get started with a configuration file, you can run the following command to generate a sample file:
swiftgen config init
Once you have this swiftgen.yml
file, you’ll want to update it to conform to your needs. Here’s what my file looks like. I’ll step through what all of the pieces mean.
input_dir: ProjectName/Sources/Resources
output_dir: ProjectName/Sources/Generated/
strings:
inputs: Localization.strings
outputs:
- templateName: structured-swift5
output: Strings.swift
params:
enumName: Strings
xcassets:
- inputs: Colors.xcassets
outputs:
- templateName: swift5
output: Colors.swift
params:
enumName: Colors
- inputs: Assets.xcassets
outputs:
- templatePath: swift5
output: Assets-Constants.swift
params:
enumName: Assets
At the top there’s an input_dir
and output_dir
. These let us specify the directories we’d like SwiftGen to look for our assets (input_dir
) and the directory the generated Swift code should be placed (output_dir
). We’ll skip the details of our localized string setup here since it’s very similar to our assets setup we’ll describe below. For our assets property (xcassets:
), we provide a file name for our input catalogs that are found in our input_dir
. We have a Colors.xcassets catalog and an images catalog named Assets.xcassets. For our outputs
, we define a few parameters (there are many more parameters to customize to your project in the documentation). The templateName
tells SwiftGen how to generate the asset names into Swift code. Here we’re using the globally available swift5
. We also output to a file named Colors.swift
and Assets-Constants.swift
which goes into our output_dir
. Finally, we use params:enumName:
to provide a name to reference our colors and images with. In this case, we’ll reference our colors as Colors.someColorName.color
and images as Assets.someImageName.image
. If our enumName:
for our images had been Images
instead, we’d access the images as Images.someImageName.image
instead.
Now that we have our config file in place to support two different asset catalogs and a localized string file, we should be able to generate some swift code. However, if you were to run this now using swift5
for the templatePath:
on both xcassets:
outputs and you’re on a version of Swiftgen older than 6.2.1, you’d receive build errors with multiple duplicate definitions. This is because SwiftGen will generate some of the same boilerplate code to support referencing the assets for both catalogs. Since we only need to generate this boilerplate once, we’ll need to define a custom template for one of our asset catalogs that strip out this redundant boilerplate code and only provides new enum references for our assets (if you’re on the latest version of Swiftgen, you can ignore the custom template steps). It sounds complicated, but it’s not too bad. We simply create a new template file we’ll call custom-assets-template.stencil
and copy paste the following into it:
// swiftlint:disable all
// Generated using SwiftGen — https://github.com/SwiftGen/SwiftGen
{% if catalogs %}
{% set enumName %}{{param.enumName|default:"Asset"}}{% endset %}
{% set colorType %}{{param.colorTypeName|default:"ColorAsset"}}{% endset %}
{% set dataType %}{{param.dataTypeName|default:"DataAsset"}}{% endset %}
{% set imageType %}{{param.imageTypeName|default:"ImageAsset"}}{% endset %}
{% set colorAlias %}{{param.colorAliasName|default:"AssetColorTypeAlias"}}{% endset %}
{% set imageAlias %}{{param.imageAliasName|default:"AssetImageTypeAlias"}}{% endset %}
{% set forceNamespaces %}{{param.forceProvidesNamespaces|default:"false"}}{% endset %}
{% set accessModifier %}{% if param.publicAccess %}public{% else %}internal{% endif %}{% endset %}
#if os(OSX)
import AppKit.NSImage
#elseif os(iOS) || os(tvOS) || os(watchOS)
import UIKit.UIImage
#endif
// swiftlint:disable superfluous_disable_command
// swiftlint:disable file_length
// MARK: - Asset Catalogs
{% macro enumBlock assets %}
{% call casesBlock assets %}
{% if param.allValues %}
// swiftlint:disable trailing_comma
{{accessModifier}} static let allColors: [{{colorType}}] = [
{% filter indent:2 %}{% call allValuesBlock assets "color" "" %}{% endfilter %}
]
{{accessModifier}} static let allDataAssets: [{{dataType}}] = [
{% filter indent:2 %}{% call allValuesBlock assets "data" "" %}{% endfilter %}
]
{{accessModifier}} static let allImages: [{{imageType}}] = [
{% filter indent:2 %}{% call allValuesBlock assets "image" "" %}{% endfilter %}
]
// swiftlint:enable trailing_comma
{% endif %}
{% endmacro %}
{% macro casesBlock assets %}
{% for asset in assets %}
{% if asset.type == "color" %}
{{accessModifier}} static let {{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{colorType}}(name: "{{asset.value}}")
{% elif asset.type == "data" %}
{{accessModifier}} static let {{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{dataType}}(name: "{{asset.value}}")
{% elif asset.type == "image" %}
{{accessModifier}} static let {{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}} = {{imageType}}(name: "{{asset.value}}")
{% elif asset.items and ( forceNamespaces == "true" or asset.isNamespaced == "true" ) %}
{{accessModifier}} enum {{asset.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
{% filter indent:2 %}{% call casesBlock asset.items %}{% endfilter %}
}
{% elif asset.items %}
{% call casesBlock asset.items %}
{% endif %}
{% endfor %}
{% endmacro %}
{% macro allValuesBlock assets filter prefix %}
{% for asset in assets %}
{% if asset.type == filter %}
{{prefix}}{{asset.name|swiftIdentifier:"pretty"|lowerFirstWord|escapeReservedKeywords}},
{% elif asset.items and ( forceNamespaces == "true" or asset.isNamespaced == "true" ) %}
{% set prefix2 %}{{prefix}}{{asset.name|swiftIdentifier:"pretty"|escapeReservedKeywords}}.{% endset %}
{% call allValuesBlock asset.items filter prefix2 %}
{% elif asset.items %}
{% call allValuesBlock asset.items filter prefix %}
{% endif %}
{% endfor %}
{% endmacro %}
// swiftlint:disable identifier_name line_length nesting type_body_length type_name
{{accessModifier}} enum {{enumName}} {
{% if catalogs.count > 1 %}
{% for catalog in catalogs %}
{{accessModifier}} enum {{catalog.name|swiftIdentifier:"pretty"|escapeReservedKeywords}} {
{% filter indent:2 %}{% call enumBlock catalog.assets %}{% endfilter %}
}
{% endfor %}
{% else %}
{% call enumBlock catalogs.first.assets %}
{% endif %}
}
// swiftlint:enable identifier_name line_length nesting type_body_length type_name
private final class BundleToken {}
{% else %}
// No assets found
{% endif %}
Save this in your project directory. I put mine in a /Config
directory at the root of my project. Next, update the templateName:
reference in the swiftgen.yml
file to point to this new template file like so:
...
- inputs: Assets.xcassets
outputs:
- templatePath: Config/custom-assets-template.stencil
output: Assets-Constants.swift
params:
enumName: Assets
...
Easy! Now, when SwiftGen runs at compile time, the build errors should go away and we should be able to reference our strings, colors, and images without string references! The best part is, the colors even work with dark mode and all of the features that come with using asset catalogs without the aforementioned downsides.
SwiftGen is super powerful and highly customizable. I definitely encourage digging into their extensive documentation and playing around with it yourself to make it work perfect for your project setup.