How To Use style-dictionary to manage colours in SwiftUI

In this blog post I am going to talk about how you can use Style Dictionary to manage colours across a SwiftUI and Web project.

Posted By Adam Bulmer In Swift,SwiftUI

Prerequisites

  1. Node 16 and NPM installed on your machine.
  2. Basic understanding of JavaScript or TypeScript.
  3. Understanding of YAML or JSON.

What is Style Dictionary

Amazon has created a tool called Style Dictionary that enables you to share design properties commonly referred to as design tokens. A design token is a value representing all the primitive values that form a design. These primitives can include font-sizes, radiuses, spacing sizes and colours.

These design tokens are stored in a platform agnostic file format such as YAML or JSON and act as an input to Style Dictionary.

Style Dictionary takes these tokens and processes them using pre-made or custom-made transformers to generate different output files specific to a platform.

Style Dictionary Workflow

Getting Started With Style Dictionary

Create a new folder named design-tokens and inside the folder run the following command to create a new package.json file. The default options are fine for now.

npm init

Install style-dictionary and initialise a basic template.

npm install style-dictionary --dev
./node_modules/.bin/style-dictionary init basic

We are left with a configuration file and the a few tokens files. Open ./tokens/colors/base.json and you should see some JSON defining our colour tokens.

{
  "color": {
    "base": {
      "gray": {
        "light": { "value": "#CCCCCC" },
        "medium": { "value": "#999999" },
        "dark": { "value": "#111111" }
      },
      "red": { "value": "#FF0000" },
      "green": { "value": "#00FF00" }
    }
  }
}

let's modify our package.json file and add "generate": "style-dictionary build" to our scripts object. We should end up with a package.json file looking like the following

{
  "name": "design-tokens",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "generate": "style-dictionary build"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "style-dictionary": "^3.7.2"
  }
}

Run npm run generate and take a look inside the ./build/ios-swift folder in the root directory of the project.

You should notice that some files have been generated. Opening StyleDictionary+Struct.swift you should see the following:

import UIKit

internal struct StyleDictionaryStruct {
	internal static let colorBaseGrayDark = UIColor(red: 0.067, green: 0.067, blue: 0.067, alpha: 1)
	internal static let colorBaseGrayLight = UIColor(red: 0.800, green: 0.800, blue: 0.800, alpha: 1)
	internal static let colorBaseGrayMedium = UIColor(red: 0.600, green: 0.600, blue: 0.600, alpha: 1)
	internal static let colorBaseGreen = UIColor(red: 0.000, green: 1.000, blue: 0.000, alpha: 1)
	internal static let colorBaseRed = UIColor(red: 1.000, green: 0.000, blue: 0.000, alpha: 1)
	internal static let colorFontBase = UIColor(red: 1.000, green: 0.000, blue: 0.000, alpha: 1)
	internal static let colorFontSecondary = UIColor(red: 0.000, green: 1.000, blue: 0.000, alpha: 1)
	internal static let colorFontTertiary = UIColor(red: 0.800, green: 0.800, blue: 0.800, alpha: 1)
	internal static let sizeFontBase = CGFloat(16.00) /* the base size of the font */
	internal static let sizeFontLarge = CGFloat(32.00) /* the large size of the font */
	internal static let sizeFontMedium = CGFloat(16.00) /* the medium size of the font */
	internal static let sizeFontSmall = CGFloat(12.00) /* the small size of the font */
}

If you look closely at the property names, you may notice a property named colorBaseGrayDark. This property name was generated using the paths within the JSON file I asked you to open previously.

{
	"color": {
		"base": {
			"gray": {
				"dark" : { "value": "#111111" } /* <------ Hi */
			},
		}
	}
}

You may have also noticed that style-dictionary has transformed the hex value stored in the JSON token file to the correct UIColor.

Before continuing it's worth pointing out that we've been working in a project separate to our XCode app project.

This is a deliberate decision, I like to keep my Design Tokens as a separate Swift package stored in GitHub. With that, let's add a Package.swift to the root of our directory so that we can import it later into our XCode project.

// swift-tools-version:4.0
import PackageDescription

let package = Package(
	name: "MyDesignTokens",
	products: [
		.library(
			name: "MyDesignTokens",
			targets:["MyDesignTokens"]
		),
	],
	targets: [
		.target(
			name: "MyDesignTokens",
			dependencies: []
		),
	]
)

If that meets your requirements, you can leave it here. However if you want to use the tokens within a XCAssets Colorset, let's keep going!

Implementing a Style Dictionary Transformer to Generate XCAssets Colorsets

If you've got here and you're wondering what a Colorset is, it's the file in XCode that allows you to define colours for both light and dark mode across devices. Colorsets are bundled together in a folder with the extension .xcassets.

Xcode Colorsets Example

So far we've used the pre-defined transformers and actions style-dictionary provides us.

At the time of writing, style-dictionary does not support XCAssets. We will have to use a couple of additional style-dictionary transformers and write a custom action for this functionality.

What is a style-dictionary transformer?

Transformers are functions that modify a token so it can be understood by a specific platform.

Transformers allow you to modify a few aspects of a token.

  1. The name of the token.
  2. The value of the token.
  3. The attributes or metadata of a token.

This unlocks the ability to transform a hex value to the format a Colorset requires.

{
  "colors": [
    {
      "color": {
        "color-space": "srgb",
        "components": {
          "alpha": "1.000",
          "blue": "0.067",
          "green": "0.067",
          "red": "0.067"
        }
      },
      "idiom": "universal"
    }
  ],
  "info": {
    "author": "xcode",
    "version": 1
  }
}

One last thing to note on transformers.

Transforms are isolated per platform; each platform begins with the same design token and makes the modifications it needs without affecting other platforms and the order you use transforms matters because transforms are performed sequentially.

What is a style-dictionary action?

Actions provide a way to run custom build code such as generating binary assets like images.

This is useful when creating XCAssets containing Colorsets because they are made up of a few files which will need to be output when running style-dictionary

Displays XCAssets Folder

Generating XCAssets using style-dictionary.

Now we know what a transformer and action are, let's implement a solution so that we can generate the XCAssets folder.

Implementing a transformer

A Colorset within XCAssets expects a colour definition in the following format.

{
  "alpha": "1.000",
  "blue": "0.067",
  "green": "0.067",
  "red": "0.067"
}

The colours stored within our token file can be any valid colour format, for this blog post we are using HEX (#111111). We therefore need to transform the hex value into the correct format.

style-dictionary provides us a couple of built-in transformers which we can make use of.

  1. attribute/cti adds a field called category to each token based on the location in the tokens file. This will be useful later on when we need to filter colour tokens.
  2. attribute/color adds a rgb field which is compatible with the format Colorset expects.

Let's update our config.json file to use these transformers.

{
	source: ["tokens/**/*.json"],
	platforms: {
		"ios-colorsets": {
			buildPath: "build/ios-colorsets/",
			transforms: ["attribute/cti", "name/cti/pascal",  "attribute/color"],
		},
		/* ...Other Platforms here... */
	},
};

Implementing the action

We now have our colour in a valid Colorset format. Let's implement an action to output the Colorset files.

Create a new folder called src and create a file within it called colorset-action.js. Copy the following code, there is a lot going on but we'll break it down in a second.

const fs = require("fs");
const path = require("path");

const CONTENTS = {
  info: {
    author: "xcode",
    version: 1,
  },
};

const createDir = (path) => {
  try {
    fs.mkdirSync(path, { recursive: true });
  } catch (err) {
    if (err.code !== "EEXIST") throw err;
  }
};

module.exports = {
  do: (dictionary, { buildPath }) => {
    const assetPath = path.join(buildPath, "DesignTokens.xcassets");

    createDir(assetPath);
    fs.writeFileSync(`${assetPath}/Contents.json`, JSON.stringify(CONTENTS));

    dictionary.allProperties
      .filter((token) => {
        return token.attributes.category === `color`;
      })
      .forEach(({ name, attributes: { rgb } }) => {
        const colorsetPath = `${assetPath}/${name}.colorset`;
        createDir(colorsetPath);

        fs.writeFileSync(
          `${colorsetPath}/Contents.json`,
          JSON.stringify({
            colors: [
              {
                idiom: "universal",
                color: {
                  "color-space": `srgb`,
                  components: {
                    red: `${rgb.r}`,
                    green: `${rgb.g}`,
                    blue: `${rgb.b}`,
                    alpha: `${rgb.a}`,
                  },
                },
              },
            ],
            ...CONTENTS,
          })
        );
      });
  },
  undo: function (dictionary, platform) {},
};

This action is doing a few things but the main tasks it completes are;

  1. Create a XCAssets folder and Contents.json file in the root of the XCAssets folder.
  2. Loop over all the tokens and only perform the action on a color token. This category field was added thanks to attribute/cti.
  3. For each token create a folder with the format TokenName.colorset.
  4. For each token create a Contents.json file with the transformed color value made possible by attribute/color.

Let's update our config.json file one final time. For this to work, you will need to rename config.json to config.js.

module.exports = {
  // <---- Hi
  source: ["tokens/**/*.json"],
  action: {
    colorsets: require("./src/colorset-action"), // <---- Hi
  },
  platforms: {
    "ios-colorsets": {
      buildPath: "build/ios-colorsets/",
      transforms: ["attribute/cti", "name/cti/pascal", "attribute/color"],
      actions: [`colorsets`], // <---- Hi
    },
    /* ...Other Platforms here... */
  },
};

Rerun npm run generate and inspect the build folder. Congratulations you've now generated a XCAssets Colorset.

Summary

We have successfully used style-dictionary to generate Colorsets stored within XCAssets. These should be made available as a Swift Package if you push them to GitHub and include them as a dependency within your XCode Project.

style-dictionary can be used for more than just colours, you can use it to generate spacing values and also generate images.

Subscribe To The Newsletter

If you want to create your first native app or have just begun your native app development journey, be sure to sign up to the newsletter. No Spam