Storyboard Constants

Automate all the things

Storyboards can really speed up development but a problem I run into over and over again is working with identifiers that are set in the storyboard and referenced in code.

Problem

Let's take the example of making a segue:

You create a segue and then assign it an identifier in the storyboard... and that's the problem. The identifier is defined in the storyboard, which leaves me with plenty of opportunities to mess up referencing the segue in code.

My normal strategy is to create a constants file that would have a constant with the value of each of these identifiers, which starts to look like the following (forgive the naming):

StoryboardConstants.h

extern NSString * const PSBMasterToDetail;
extern NSString * const PSBMasterToSettings;

This works for me and I'm happy, but it seems like a lot of manual graft and can become tiresome to maintain especially in a young project with lots of development spikes. It's also still very possible to make typos and mess up these declarations or for them to become out of sync with the storyboard.

Automation

I got to thinking about this problem and two things stood out

  1. I don't like doing things manually and
  2. I don't like that it is so easy to misspell a constant when swapping between storyboard and code (yes I am aware copy and paste exists).

So I started tinkering and I've come up with a rough tool that I've cunningly named sbconstants.

My automation idea is that you add a build script that will extract constants from your storyboards and then dump them into a file.

Grab it with

sudo gem install sbconstants

My first effort has a UI like this (command line app)

% sbconstants -h
Usage: DESTINATION_FILE [options]
    -p, --prefix=<prefix>            Only match identifiers with <prefix>
    -s, --source-dir=<source>        Directory containing storyboards
    -q, --queries=<queries>          YAML file containing queries
    -d, --dry-run                    Output to STDOUT
    -v, --verbose                    Verbose output

To get this working on a project the sbconstants gem needs to be installed to the system Ruby and then we need to call the executable from a build script. An example call would look like this

sbconstants MyApp/Constants/PASStoryboardConstants.h

NB The argument is the destination file to dump the constants into - this needs to be added manually

Every time this project is built it will parse the storyboard files and pull out any constants and then dump them into PASStoryboardConstants.(h|m). This of course means that PASStoryboardConstants.(h|m) should not be edited by hand as it will just be clobbered any time we build.

The output of running this tool will look something like this:

PASStoryboardConstants.h

// Auto generated file - any changes will be lost

#pragma mark - tableViewCell.reuseIdentifier
extern NSString * const PSBAwesomeCell;  

#pragma mark - segue.identifier
extern NSString * const PSBMasterToDetail;  
extern NSString * const PSBMasterToSettings;  

PASStoryboardConstants.m

// Auto generated file - any changes will be lost

#import "PASStoryboardConstants.h"

#pragma mark - tableViewCell.reuseIdentifier
NSString * const PSBAwesomeCell = @"PSBAwesomeCell";

#pragma mark - segue.identifier
NSString * const PSBMasterToDetail = @"PSBMasterToDetail";
NSString * const PSBMasterToSettings = @"PSBMasterToSettings";

The constants are grouped by where they were found in the storyboard xml e.g. segue.identifier. This can really help give you some context about where/what/when and why a constant exists.

More options

Options are fun and there are a few to play with - most of these options are really only any good for debugging.

--prefix

-p, --prefix=<prefix>            Only match identifiers with <prefix>

Using the prefix option you can specify that you only want to grab identifiers that start with a certain prefix, which is always nice.

--source-dir

-s, --source-dir=<source>        Directory containing storyboards

If you don't want to run the tool from the root of your app for some reason you can specify the source directory to start searching for storyboard files. The search is recursive using a glob something like <source-dir>/**/*.storyboard

--dry-run

-d, --dry-run                    Output to STDOUT

If you just want to run the tool and not write the output to a file then this option will spit the result out to $stdout

--verbose

-v, --verbose                    Verbose output

Perhaps you want a little more context about where your identifiers are being grabbed from for debugging purposes. Never fear just use the --verbose switch and get output similar to:

sample output

#pragma mark - viewController.storyboardIdentifier
//
//    info: MainStoryboard[line:43](viewController.storyboardIdentifier)
// context: <viewController restorationIdentifier="asd" storyboardIdentifier="FirstViewController" id="EPD-sv-vrF" sceneMemberID="viewController">
//
NSString * const FirstViewController = @"FirstViewController";

--queries

-q, --queries=<queries>          YAML file containing queries

Chances are I've missed some identifiers to search for in the storyboard. You don't want to wait for the gem to be updated or have to fork it and fix it. Using this option you can provide a YAML file that contains a description of what identifers to search for. The current one looks something like this (NB this is a great starting point for creating your own yaml):

queries

---
segue: identifier
tableViewCell: reuseIdentifier
view: restorationIdentifier
? - navigationController
  - viewController
  - tableViewController
: - storyboardIdentifier
  - restorationIdentifier
  

This looks a little funky but it's essentially groups of keys and values (both the key and the value can be an array). This actually gets expanded to looks for

+----------------------+-----------------------+
|         node         |      attribute        |
+ ---------------------+-----------------------+
| segue                | identifier            |
| tableViewCell        | reuseIdentifier       |
| view                 | restorationIdentifier |
| navigationController | storyboardIdentifier  |
| viewController       | storyboardIdentifier  |
| tableViewController  | storyboardIdentifier  |
| viewController       | restorationIdentifier |
| navigationController | restorationIdentifier |
| tableViewController  | restorationIdentifier |
+----------------------+-----------------------+

Conclusion

I'm not sure if this tool will be of any use to anyone but for the projects I've been tinkering with it seems to work pretty well leaving me confident that my storyboard identifier's will not go out of sync with how they are referenced in code.

If anyone does use this and they like it or they have any issues please report it to me so I can get it fixed up and improve my own workflow.