Generating multi-brand multi-platform icons with Sketch and a Node.js script — Part #2

Second part: the build script and the generation of the assets.

Cristiano Rastelli
Dec 6, 2018 · 17 min read
Image for post
Image for post

Image for post
Image for post

The build script

// ... modules imports hereconst SKETCH_FILES = {
badoo: ['icons_common'],
blendr: ['icons_common', 'icons_blendr'],
fiesta: ['icons_common', 'icons_fiesta'],
hotornot: ['icons_common', 'icons_hotornot'],
};
const SKETCH_FOLDER_PATH = path.resolve(__dirname, '../src/');
const SKETCH_TEMP_PATH = path.resolve(SKETCH_FOLDER_PATH, 'tmp');
const DESTINATION_PATH = path.resolve(__dirname, '../dist');
console.log('Build started...');if (sketchtool.check()) {
console.log(`Processing Sketch file via ${sketchtool.version()}`);
build();
} else {
console.info('You need Sketch installed to run this script');
process.exit(1);
}
// ----------------------------------------function build() {
// be sure to start with a blank slate
del.sync([SKETCH_TEMP_PATH, DESTINATION_PATH]);
// process all the brands declared in the list of Sketch files
Object.keys(SKETCH_FILES).forEach(async (brand) => {
// get the design tokens for the brand
const brandTokens = getDesignTokens(brand);

// prepare the Sketch files (unzipped) and get a list of them
const sketchUnzipFolders = await prepareSketchFiles({
brand,
sketchFileNames: SKETCH_FILES[brand],
sketchFolder: SKETCH_FOLDER_PATH,
sketchTempFolder: SKETCH_TEMP_PATH
});
// get the Sketch metadata
const sketchMetadata = getSketchMetadata(sketchUnzipFolders);
const sketchDataSharedStyles = sketchMetadata.sharedStyles;
const sketchDataAssets = sketchMetadata.assetsMetadata;
generateAssetsPDF({
platform: 'ios',
brand,
brandTokens,
sketchDataSharedStyles,
sketchDataAssets
});
generateAssetsSVGDynamicMobileWeb({
platform: 'mw',
brand,
brandTokens,
sketchDataSharedStyles,
sketchDataAssets
});
generateAssetsVectorDrawableDynamicAndroid({
platform: 'android',
brand,
brandTokens,
sketchDataSharedStyles,
sketchDataAssets
});
});
}

Preparing the Sketch files

Image for post
Image for post
A comparison of the layer names, inside the Sketch files, before and after the update.

Reading the Sketch metadata

{
"_class": "document",
"do_objectID": "45D2DA82-B3F4-49D1-A886-9530678D71DC",
"colorSpace": 1,
...
"layerStyles": {
"_class": "sharedStyleContainer",
"objects": [
{
"_class": "sharedStyle",
"do_objectID": "9BC39AAD-CDE6-4698-8EA5-689C3C942DB4",
"name": "features/feature-like",
"value": {
"_class": "style",
"fills": [
{
"_class": "fill",
"isEnabled": true,
"color": {
"_class": "color",
"alpha": 1,
"blue": 0.10588235408067703,
"green": 0.4000000059604645,
"red": 1
},
"fillType": 0,
"noiseIndex": 0,
"noiseIntensity": 0,
"patternFillType": 1,
"patternTileScale": 1
}
],
"blur": {...},
"startMarkerType": 0,
"endMarkerType": 0,
"miterLimit": 10,
"windingRule": 1
}
},
...
const parsedSharedStyles = {};
parsedDocument.layerStyles.objects.forEach((object) => {
parsedSharedStyles[object.do_objectID] = {
name: object.name,
isFill: _.get(object, 'value.fills[0].color') !== undefined,
isBorder: _.get(object, 'value.borders[0].color') !== undefined,
};
});
{
"commit": "623a23f2c4848acdbb1a38c2689e571eb73eb823",
"pagesAndArtboards": {
"EE6BE8D9-9FAD-4976-B0D8-AB33D2B5DBB7": {
"name": "Icons",
"artboards": {
"3275987C-CE1B-4369-B789-06366EDA4C98": {
"name": "badge-feature-like"
},
"C6992142-8439-45E7-A346-FC35FA01440F": {
"name": "badge-feature-crush"
},
...
"7F58A1C4-D624-40E3-A8C6-6AF15FD0C32D": {
"name": "tabbar-livestream"
}
...
}
},
"ACF82F4E-4B92-4BE1-A31C-DDEB2E54D761": {
"name": "XP_this__is_an_experiment",
"artboards": {
"31A812E8-D960-499F-A10F-C2006DDAEB65": {
"name": "this__is_an_experiment/tabbar-livestream[variant1]"
},
"20F03053-ED77-486B-9770-32E6BA73A0B8": {
"name": "this__is_an_experiment/tabbar-livestream[variant2]"
},
"801E65A4-3CC6-411B-B097-B1DBD33EC6CC": {
"name": "this__is_an_experiment/tabbar-livestream[control]"
}
}
},
{
"navigation-bar-edit": {
"do_objectID": "86321895-37CE-4B3B-9AA6-6838BEDB0977",
...sketch_artboard_properties,
"name": "navigation-bar-edit",
"assetname": "navigation-bar-edit",
"source": "icons_common",
"width": 48,
"height": 48
"layers": [
{
"do_objectID": "A15FA03C-DEA6-4732-9F85-CA0412A57DF4",
"name": "Path",
...sketch_layer_properties,
"sharedStyleID": "6A3C0FEE-C8A3-4629-AC48-4FC6005796F5",
"style": {
...
"fills": [
{
"_class": "fill",
"isEnabled": true,
"color": {
"_class": "color",
"alpha": 1,
"blue": 0.8784313725490196,
"green": 0.8784313725490196,
"red": 0.8784313725490196
},
}
],
"miterLimit": 10,
"startMarkerType": 0,
"windingRule": 1
},
},
],
...
},
"experiment-name/navigation-bar-edit[variant]": {
"do_objectID": "00C0A829-D8ED-4E62-8346-E7EFBC04A7C7",
...sketch_artboard_properties,
"name": "experiment-name/navigation-bar-edit[variant]",
"assetname": "navigation-bar-edit",
"source": "icons_common",
"width": 48,
"height": 48
...
const SKETCH_FILES = {
badoo: ['icons_common'],
blendr: ['icons_common', 'icons_blendr'],
fiesta: ['icons_common', 'icons_fiesta'],
hotornot: ['icons_common', 'icons_hotornot'],
};
Image for post
Image for post
The logical schema of how the “overriding” of the same icon works, between a common/shared set of icons and icons specifically designed for white-labels (also considering the case of AB testing)

Generating the final files in different formats for different platforms

Image for post
Image for post
sketchtool.run(`export slices ${cloneSketchFile} --formats=svg --scales=1 --output=${destinationFolder} --overwriting`);

PDF files for iOS

React/JSX files for web

<?xml version="1.0" encoding="UTF-8"?>
<svg width="128px" height="128px" viewBox="0 0 128 128" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: sketchtool 52.2 (67145) - http://www.bohemiancoding.com/sketch -->
<title>badge-feature-like</title>
<desc>Created with sketchtool.</desc>
<g id="Icons" fill="none" fill-rule="evenodd">
<g id="badge-feature-like">
<circle id="circle" fill="#E71032" cx="64" cy="64" r="64">
<path id="Shape" fill="#FFFFFF" d="M80.4061668,..."></path>
</g>
</g>
</svg>
/* This file is generated automatically - DO NOT EDIT */
/* eslint-disable max-lines,max-len,camelcase */
const React = require('react');
module.exports = function badge_feature_like({ tokens }) {
return (
<svg data-origin="pipeline" viewBox="0 0 128 128">
<g fill="none" fillRule="evenodd">
<circle fill={tokens.TOKEN_COLOR_FEATURE_LIKED_YOU} cx={64} cy={64} r={64} />
<path fill="#FFF" d="M80.4061668,..." />
</g>
</svg>
);
};

VectorDrawable files for Android

Image for post
Image for post
<!-- This file is generated automatically - DO NOT EDIT -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="128dp"
android:height="128dp"
android:viewportWidth="128"
android:viewportHeight="128">
<path
android:fillColor="?color_feature_liked_you"
android:pathData="M64 1a63 63 0 1 0 0 126A63 63 0 1 0 64 1z"
/>
<path
android:fillColor="#FFFFFF"
android:pathData="M80.406 ..."
/>
</vector>
Image for post
Image for post
An example of a VectorDrawable icon imported into Android Studio

JSON dictionary as assets library

{
"platform": "mw",
"brand": "blendr",
"assets": {
"badge-feature-like": {
"assetname": "badge-feature-like",
"path": "assets/badge-feature-like.jsx",
"width": 64,
"height": 64,
"source": "icons_common"
},
"navigation-bar-edit": {
"assetname": "navigation-bar-edit",
"path": "assets/navigation-bar-edit.jsx",
"width": 48,
"height": 48,
"source": "icons_common"
},
"tabbar-livestream": {
"assetname": "tabbar-livestream",
"path": "assets/tabbar-livestream.jsx",
"width": 128,
"height": 128,
"source": "icons_blendr",
"abtest": {
"this__is_an_experiment": {
"control": "assets/this__is_an_experiment/tabbar-livestream__control.jsx",
"variant1": "assets/this__is_an_experiment/tabbar-livestream__variant1.jsx",
"variant2": "assets/this__is_an_experiment/tabbar-livestream__variant2.jsx"
},
"a_second-experiment": {
"control": "assets/a_second-experiment/tabbar-livestream__control.jsx",
"variantA": "assets/a_second-experiment/tabbar-livestream__variantA.jsx"
}
}
},
...
}
}

The final result

Image for post
Image for post
Structure of ”src” and ”dist” folders after completion of the build process.

Conclusions (and lessons learned along the way)

Image for post
Image for post
Sketch as a design tool can be imagined as a possible “target” of the codebase.




Credits

Bumble Tech

This is the Bumble tech team blog focused on technology and…

Cristiano Rastelli

Written by

Passionate web designer & developer. In love with Design Systems, CSS & Front-End Architecture. Father & husband. Attendee, speaker & organiser of tech meetups.

Bumble Tech

We’re the tech team behind social networking apps Bumble and Badoo. Our products help millions of people build meaningful connections around the world.

Cristiano Rastelli

Written by

Passionate web designer & developer. In love with Design Systems, CSS & Front-End Architecture. Father & husband. Attendee, speaker & organiser of tech meetups.

Bumble Tech

We’re the tech team behind social networking apps Bumble and Badoo. Our products help millions of people build meaningful connections around the world.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch

Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore

Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store