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

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.

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.

About the Author

Brian Bethke profile.

Brian Bethke

Sr. Consultant

Brian has experience in Swift, Objective-C, Java, Kotlin, PHP, integration with hardware peripherals over BLE, and restful API’s. He has spent over 8 years developing native iOS applications in a variety of fields. When he’s not working, Brian enjoys scuba diving, playing piano, playing video games, and reading.

Leave a Reply

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

Related Blog Posts
Understanding Mutual TLS Options in the Public Cloud
When delivering an API over the public internet via a cloud provider, some organizations and frameworks require mutual TLS verification as a part of the interaction for that API. Mutual TLS can be used to […]
Performance Test Liquibase Update
When doing a liquibase update to a database if you’re having performance issues, it can be hard to find out which updates are causing problems. If you need to measure the time to apply each […]
TICK Stack Monitoring for the Non-Technical
TICK – Telegraf, Influx, Chronograf, and Kapacitor – is a method of monitoring your systems and applications. In this article, I discuss in non-technical terms what the difference is between TICK and Prometheus Grafana A […]
Design Systems, Part 1 • Introduction
Business leaders need a practical guide to plan and execute Design System Initiatives. The aim of this series is to be that guide. This installment introduces terms and definitions as a primer on Design Systems.