SwiftGen with Image & Color Asset Catalogs

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


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.

If you’re using Xcodegen, you can simply add a preGenCommand to the options: in your project.yml file to run SwiftGen.

  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/
  inputs: Localization.strings
    - templateName: structured-swift5
      output: Strings.swift
        enumName: Strings
  - inputs: Colors.xcassets
      - templateName: swift5
        output: Colors.swift
          enumName: Colors
  - inputs: Assets.xcassets
      - templatePath: swift5
        output: Assets-Constants.swift
          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

// 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 %}
  {% 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
      - templatePath: Config/custom-assets-template.stencil
        output: Assets-Constants.swift
          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.

About the Author

Object Partners profile.
Leave a Reply

Your email address will not be published. Required fields are marked *

Related Blog Posts
Infrastructure as Code – The Wrong Way
You are probably familiar with the term “infrastructure as code”. It’s a great concept, and it’s gaining steam in the industry. Unfortunately, just as we had a lot to learn about how to write clean […]
Snowflake CI/CD using Jenkins and Schemachange
CI/CD and Management of Data Warehouses can be a serious challenge. In this blog you will learn how to setup CI/CD for Snowflake using Schemachange, Github, and Jenkins. For access to the code check out […]
How to get your pull requests approved more quickly
TL;DR The fewer reviews necessary, the quicker your PR gets approved. Code reviews serve an essential function on any software codebase. Done right, they help ensure correctness, reliability, and maintainability of code. On many teams, […]
Kafka & Kubernetes: Scaling Consumers
Kafka and Kubernetes (K8s) are a great match. Kafka has knobs to optimize throughput and Kubernetes scales to multiply that throughput. On the consumer side, there are a few ways to improve scalability. Resource & […]