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
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.
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!
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
.
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.
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.
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.
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
Now we know what a transformer and action are, let's implement a solution so that we can generate the XCAssets folder.
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.
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.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... */
},
};
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;
Contents.json
file in the root of the XCAssets folder.attribute/cti
.TokenName.colorset
.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.
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.
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