Sketch is the preferred tool at WillowTree for creating many of our application designs. It provides designers with powerful tools for creating many of the world class designs WillowTree is known for. Sketch operates off of Artboards, Layers and Symbols. For the purposes of this article, we care about what Artboards and Layers are. Artboards are the primary containers where designs for a screen exist. Artboards typically have layers contained within them. Layers are the actual design elements. These are things like text labels, buttons, images, etc. Sketch also provides a way for developers to create and integrate custom plugins into the application. These can augment a designer’s workflow and save time and effort when creating designs.

The Plugin

The idea for the plugin we will be exploring came from WillowTree designer Ben Bloodworth. The plugin takes one Artboard as the “master” board and pairs it to multiple boards that will copy its contents and update when it changes. sketch plugin blog img 1 This is useful for creating Product maps where you want one-to-one copies of designs organized in a specific way. Another use is having a board be a base design, such as an empty modal, and being able to use it as a base for all modals in the application. That way, when the base modal is updated, all modals in all designs are also updated without having to manually change them.

Anatomy of a Sketch Plugin

The Structure

Sketch plugins have a very specific folder structure similar to that of MacOS applications. The top level folder must end with the extension .sketchplugin. Note that this is in fact a folder, not a file, but if sketch is installed, you will need to right click it and select “Show Package Contents” to actually get inside it. sketch plugin blog img 2 Inside the .sketchplugin folder, there needs to be a Contents folder which in turn must have a Sketch folder. The Sketch folder is where the bulk of the plugin will live. There can also be a Resources folder inside the Contents folder that can contain any static assets your plugin may need to reference, like images. Finally, there must be a manifest.json file and at least one script file, that will be executed, inside the Sketch folder. Sketch plugin blog img 3

The Manifest File

As mentioned previously, Sketch plugins require a manifest.json file. This file helps Sketch know the name of your plugin, the commands it can perform, and the items to put into the plugin menu. The manifest for the plugin I created is as follows:


{
    "name": "Artboard Pairer",
    "identifier": "com.willowtreeapps.artboard-pairer",
    "version": "0.3",
    "description": "Pairs an Artboard to an existing one, allowing for in-sync dupelicates",
    "authorEmail": "evan.compton@willowtreeapps.com",
    "author": "Evan Compton",
    "commands": [
        {
            "name": "Update Pairings...",
            "identifier": "updatePairs",
            "shortcut": "cmd+shift+u",
            "script": "main.js",
            "handler": "updatePairs"
        },
        {
            "name": "Create Pairing...",
            "identifier": "createPair",
            "shortcut": "",
            "script": "main.js",
            "handler": "createPair"
        },
        {
            "name": "Remove Pairing...",
            "identifier": "removePair",
            "shortcut": "",
            "script": "main.js",
            "handler": "removePair"
        }
    ],
    "menu": {
        "items": [
            "updatePairs",
            "createPair",
            "removePair"
        ]
    }
}

The fields above the commands property just describe the plugin. The name, version, and author are fairly straightforward, and provide metadata about the plugin. The commands property is where all the interesting stuff happens. A command is a specific action the plugin can perform.

The name is what will appear in the Sketch Plugin menu.

The identifier is a unique ID for that command.

The shortcut is a keyboard combination the user can use to execute the command.

The script is the name of the script file where the command’s code live.

The handler is the name of the function that corresponds to the command in the specified file.

Lastly the menu property specifies the commands to put into plugin menu using the identifiers.

Here’s what this plugin looks like in Sketch: sketch plugin blog img 4

Writing the Plugin

CocoaScript

Sketch plugins are written in a language called CocoaScript. This language is an interesting combination of Objective-C and JavaScript and requires knowledge of both languages and Cocoa classes in general. The reason this language is used is because it allows for the calling and representation of Cocoa objects in a scripting-like environment. Be warned, there is very little documentation on this language, and Sketch objects in general, so learning by example is your best bet. The following sections are what I was able to figure out and learn in the hopes to make future development for others easier.

Building A Command

As described in the Manifest section, each command needs a handler function. The basic signature of this function looks something like this function createPair(context). The big thing here is the context object. This is the main tool of getting the objects you will want to work with. To help illustrate this, let’s walk through building the command to pair two Artboards together. We will be using a JSON file to store the pairing with the Artboard names as the keys for the pairing.

First, we need to get all of the Artboards the user wants to pair to a single master Artboard. The best way to do this is to have the user select all the ones that he or she wants in Sketch. Luckily, we can access a user’s selection in our script:

var doc = context.document;
var selection = context.selection;

Here we grab the current document the user is editing, also known as the currently open Sketch file, and then grab all of the currently selected items, which is represented as an array. However, if nothing is selected we can’t do anything, so let’s do a quick check:

 If (selection.length == 0) {
        doc.showMessage("At Least One Artboard Not Selected");
        return;
   }

Here we see if the length of the selection array is 0, and then call a function on the Sketch Document that sends a little pop-up message telling the user they need to select an Artboard for our plugin to work. Finally, we do a return which prevents our script from executing further.

So now we know the user has selected something, but we don’t know what they have selected. It could be an Artboard, a Layer, a Symbol, who knows! Again, we need to do some more checking:


var copyNames = []
var numBoards = 0
for(var i = 0; i < selection.length; i++) {
    var current = selection[i];
    if([current className] == "MSArtboardGroup") {
        copyNames.push([current name] + '');
        numBoards +=1
     }
  }
  if(numBoards == 0) {
    doc.showMessage("At Least One Artboard Must Be Selected");
    return;
  }

There’s deceptively a lot going on here so let’s break it down a bit more. We don’t care if the user has accidentally selected something that’s not an Artboard, as long as there is at least one Artboard the command will work. To check this we loop through the selection array to check if the object is an MSArtboardGroup, which is the Object representation of an Artboard. Here we see our first example of the Obj-C syntax in our JS file if([current className] == "MSArtboardGroup"). The object is actually a Sketch Cocoa Object and we access it’s properties like we would an Obj-C object. Here we get it’s className property to see if it is in fact an MSArtboardGroup. If so, we need to store it’s name in our copyNames array and increment our counter by one. We can grab the name using the name property on the Sketch object, but that actually gives us an NSString object and not a native JS String object. Already we’re running into some of the fun quirks of this language. We can take advantage of JS’s type coercion, which implicitly converts an object of say type int to another type like a String, to make it into a native JS String by just concatenating it with an empty String copyNames.push([current name] + '');.

Now we have all the selected Artboards, but we need a way for the user to designate which Artboard is the Master. Since we’re already using selection as the means to get the Artboards to pair to, we need some other way to get user input. This is a perfect use case for a dialog modal. First, let’s create a function that grabs the name of every Artboard in the Sketch Document to present as options.


var getAllArtboardNames = function(context) {
    let pages = context.document.pages();
    var names = []
    // Filter layers using NSPredicate
    for(var i = 0; i < pages.length; i++) {
        var currentPage = pages[i]
        var scope =  [currentPage children],
        predicate = NSPredicate.predicateWithFormat("(className == %@)",           "MSArtboardGroup"),
        layers = [scope filteredArrayUsingPredicate:predicate];
        var loop = [layers objectEnumerator], layer;
    
        while (layer = [loop nextObject]) {
	var nameOfBoard = [layer name]
            if(nameOfBoard.indexOf("-synced") < 0) {
                names.push(nameOfBoard);
            }
        }
    }
    return names;
}

We pass in the context and grab all the Page objects in the current Sketch Document. In each Page, we need to check all objects that exist in that Page to find the Artboards. To do that, we call the children function on the Page object which returns us an NSArray of all the Sketch Layer Objects it has. We then use NSPredicate to create a filter function for us that checks the className property of each object to see if it is an MSArtboardGroup. Lastly, we loop through the list of filtered objects using an enumerator and save the names to our array for return. Notice we don’t coerce these values because we want to work with them later as NSStrings.

We’ve got the Artboard names, but now we need to build the actual dropdown dialog. If you’ve ever built a dialog programmatically in Obj-C, this code will look very familiar:

var dropdown;

var getDropdownValue = function() {
    if(dropdown) {
        return dropdown.titleOfSelectedItem();
    } else {
        return null;
    }
}

var createDropDownWindow = function(context,title,boardNames) {

    var alert = COSAlertWindow.new();

    alert.setIcon(NSImage.alloc().initByReferencingFile(context.plugin.urlForResourceNamed("copy.png").path()));
    alert.setMessageText(title)

    // Creating dialog buttons
    alert.addButtonWithTitle("Ok");
    alert.addButtonWithTitle("Cancel");

    // Creating the view
    var viewWidth = 300;
    var viewHeight = 100;

    var view = NSView.alloc().initWithFrame(NSMakeRect(0, 0, viewWidth, viewHeight));
    alert.addAccessoryView(view);

    // Create and configure your inputs here
    // Create label
    var label = NSTextField.alloc().initWithFrame(NSMakeRect(0,viewHeight - 33,(viewWidth - 100),35));
    [label setBezeled:false];
    [label setDrawsBackground:false];
    [label setEditable:false];
    [label setSelectable:false];
    // Add label
    label.setStringValue("Select Artboard:");
    view.addSubview(label);
    
    // Creating the input
    dropdown = NSPopUpButton.alloc().initWithFrame(NSMakeRect(0, viewHeight - 50, (viewWidth / 2), 22));
    var names = boardNames;
    for(var i = 0; i < names.length; i++) {
        var name = names[i];
        [dropdown addItemWithTitle:name];
    }
    // Filling the PopUpButton with options    
    dropdown.selectItemAtIndex(0);
    // Adding the PopUpButton to the dialog
    view.addSubview(dropdown);

    // Show the dialog
    return [alert]
}

First off, we define a global variable and function for obtaining its value. We do this because there isn’t a good way to get the value from the modal once it’s being shown. Next, we actually define the creation function by passing in our context, the title we want to display, and the list of Artboard names to populate the dropdown. The most interesting lines are up top:

var alert = COSAlertWindow.new(); creates a CocoaScript representation of a Cocoa Alert Window, and building it is almost exactly like Obj-C. alert.setIcon(NSImage.alloc().initByReferencingFile(context.plugin.urlForResourceNamed("copy.png").path())); shows how you can reference an image asset from your Resources folder.

In this case, to set the icon on the Alert window. The only other thing to note in this code is the slapdash alternating between JS style calls and Obj-C style calls throughout the function. This is the biggest issue with CocoaScript, where the difference seems arbitrary and only by seeing examples can you truly determine which style to use.

We’ve got our Artboard Names and we have a way to present them. Let’s now show the modal and snag the reponse to get our pairing:

var boardNames = getAllArtboardNames(context)
var window = createDropDownWindow(context,"Create Pairing",boardNames);
var alert = window[0]; 

 var response = alert.runModal();
  if (response != "1000"){
    return;
  }
    var masterName = getDropdownValue();

We use our new functions to get our names and alert modal. Calling runModal(); pauses execution until the user responds. The response is a code that relates to the button pressed, 1000 equals the first button, 1001 the second, and so on. We check to see if they hit the first button, our Okay button, and if not we quit the script. Since we know a selection has been made, we can grab the value of our saved variable.

We can now associate our Master board to our selections, but to be useful we need to persist these pairings. An easy way to do this is to store them in a JSON document. Let’s create a few functions that handle reading and writing a file:

var getFilePath = function (context) {
    var path = context.scriptPath;
    var parts = path.split('/');
    var build = '/';
    // We do -4 as that gets us the path to the top level plugin directory for sketch
    // saving inside the plugin would mean the files are lost upon upgrade of the plugin
    for(var i = 0; i< parts.length-4; i++) {
      if(parts[i] !== "") {
          build += parts[i] + '/';
      }
    }
    return build+context.document.cloudName()+"-pairings.json";
}

var errorHandler = function(error) {
    log(error);
}

var writeJSONToFile = function (context, jsonObj) {
    var file = NSString.stringWithString(JSON.stringify(jsonObj, null, "\t"));
    var filePath = getFilePath(context);
    [file writeToFile: filePath atomically: true encoding: NSUTF8StringEncoding error: errorHandler];
}
var readJSONfromFile = function(context) {
    var filePath = getFilePath(context)
    var fileContents = NSString.stringWithContentsOfFile(filePath);
    return JSON.parse(fileContents);
}

First, we need to figure out where to keep this file. For this plugin, we’re going to store it in the general Sketch Plugin folder. We can’t store it inside the plugin itself, as the files will get overwritten upon upgrading. We then create a JSON file per document by using the actual name of the file. This does, however, mean if a file is renamed the pairings are lost. The reading and writing is easy thanks to NSString being able to read and write to files and built in JSON stringification and parsing. With the last of our utility functions written, we can finish up our command:

var masterExists = false;
    if(json) {
        for (var i = 0; i < json.pairs.length; i++) {
            var current = json.pairs[i];
            if(current.master == masterName) {
                for(var j = 0; j < copyNames.length; j++) {
                    if(current.copies.indexOf(copyNames[j]) >= 0) {
                        continue;
                    }
                    json.pairs[i].copies.push(copyNames[j]);
                }
                masterExists = true   
            }
        }
    } else {
        json = {"pairs":[]}
    }
    if(!masterExists) {
        // Need to coerce vlaues to JS String objects
        json.pairs.push({
            "master" : masterName + '',
            "copies": copyNames
        })
    }
    writeJSONToFile(context, json)
    doc.showMessage("Pairing Created");

The additional logic in the code checks to see if a file already exists or not. If it does and the selected master board already has pairings, we then just append them instead of creating a new entry. When all is successful, it writes the file to disk and tells the user that the command finished successfully. The full command and other commands can be seen in this Github Repo.

Other Fun Tidbits

Debugging

There isn’t too much in terms of Debugging to help when making the plugin. Your best friends will be the System Console and Sketch’s Run Script option. You can use the log() function to write to the System Console to see output. This is where you will also see exceptions. Just be sure to search for Sketch in the search bar to filter out other applications. The Run Script tool allows you to run Sketch plugin scripts as if they were inside your handler function. This is useful when trying to inspect properties or trying out sections of your code, especially ones that manipulate Sketch objects.

Importing Helper Functions

All the utility functions shown in this article actually live in a separate util.js file in the plugin implementation. These functions can be accessed via an Obj-C style import at the top of the main.js file @import 'util.js'.

Conclusion

Writing a plugin for Sketch is not an easy process. It requires a good knowledge of both Obj-C and JavaScript and there is very little documentation on it. Sketch provides some references and resources in their developer site, but it is not all encompassing. However, if you do take the time to get passed the learning curve you can create some pretty awesome plugins that will make your fellow designers both more productive and happier.