29
Jan
2013
 

Down with Magic Strings!

by Patrick Hughes

Developing iOS apps in Xcode is pretty great. With Objective-C and llvm we get type checking and autocompletion of all our classes and method names which is a nice improvement over my favorite dynamic languages. Unfortunately there are still some places where the compiler can’t help us. There are various resources we load from files like images, nibs & xibs and other resources which we need to specify by name, like a view controller we want to load from a storyboard.

### The Problem

Unfortunately these names are not collected by Xcode, so we must type them in as strings. These names-as-strings are generally referred to as “Magic Strings” because they make things happen without the compiler knowing anything about them. This can be problematic for a handful of reasons. Firstly, there’s no autocompletion. I don’t know about you, but I can’t type @"detail_table_view_cell_rounded_top_and_bottom_blue" with enough confidence that I don’t quadruple check it. Next, when things aren’t working properly I end up having to de-nest my [imageView setImage:[UIImageView imageNamed:@"background_image"]] method call to ensure that the image is actually being loaded. Thirdly, when the design changes and I have to replace 45 image files the compiler won’t be able to warn us if any of the names have changed. That can be an irritating source of subtle regressions.

An ideal solution to this problem would:

1. Allow compile time checking of resource requests to ensure the resource exists.
2. Enforce runtime checking to ensure the resource loaded properly.
3. Create tokens that Xcode can use for autocompletion.
4. Tell me how wonderful I am.

Compile time checking would require a compiler verified token of some sort, like an NSString constant, preferably auto-generated. That also would allow for autocompletion. Unfortunately, a simple string constant wouldn’t solve the third requirement as runtime checking would require some sort of assertion after the resource is loaded.

### A Solution
The solution I’ve created so far only deals with images, but could easily be expanded to nibs and xibs. Creating tokens for Storyboard IDs would be a bit more work.

I’ve written a small python script that solves these problems in a way that I’m satisfied with. Each time it runs it scans a folder for images. It then compares the image names to collect the various platform specific and scaled versions and groups them together. It then #defines a block for each group that loads the image using imageNamed:, throws an assertion if the image doesn’t load and then returns the image.

For instance, if I have the following image files in my images folder:

red-button.png
red-button@2x.png
red-button~ipad.png
red-button@2x-ipad.png

The script will recognize that they are all part of the same group and will only create one define statement, which will look like this:

#define imgRedButton (UIImage*)^{ UIImage *image = [UIImage imageNamed:@"red-button"]; ZAssert(image, @”Image red-button not found”); return image; }()

Now, whenever I need to load an image, I can simply type:

[imageView setImage:imgRedButton]

(With full auto completion, of course.) and we’re golden!

I’ve only used this script on one small project, so I’m sure there are a million ways it can be improved.

### Using The Script

Download [the image.py script](https://gist.github.com/4462966) and place it in your project directory.

usage: images.py [-h] [-s SOURCE] [-d DESTINATION] [--prefix PREFIX]
[--format FORMAT] [--warn-retina RETINA] [--warn-ipad IPAD]
[--warn-iphone IPHONE] [--warn-duplicates DUPLICATES]

The script is invoked via the command line, all values have sane-ish defaults:

Option Default
-s Directory of images. Current working directory.
-d Output file. Current working directory.
–prefix Token Prefix. ‘img’ Prepended to all image names.
–format Output format. A block which loads the image (via imageNamed:) and ensures that it loaded (via ZAssert).

In addition the script can add warnings to the output file to more proactively alert you of missing images:

Option Default Effect
–warn-iphone False Warn if there are only ~ipad versions of images.
–warn-ipad False Warn if there are no ~ipad versions of images.
–warn-retina True Warn if there are no @2x version of images (dependent on –warn-iphone and –warn-ipad).
–warn-duplicates True Warn if there are slight naming inconsistencies that are incompatible with the script.

### Xcode integration (The Eye of Sauron)

Manually running the script every once in a while is dumb, and stupid and stuff. Don’t do it that way. Instead, run it every time you build. That way you can be sure that it has its watchful eye on your images.

My preferred method is to create a separate target to run the script, set that target as a dependent target, and then import the output file in prefix.pch. That way all my classes have access to the images by default.

To add a new target in Xcode 4.5:

1. Ensure that you have downloaded [the image.py script](https://gist.github.com/4462966) and placed it in your project directory.
1. Choose File > New > Target… An action sheet should appear.
1. In the left panel under OS X select Other.
1. In the right panel select “External Build System”.
1. Click the “Next” button in the bottom right corner.
1. Enter the target options:
a. Give your product a name. I use “Image Script”.
a. Change the “Build Tool” to "/usr/bin/python".
1. Select your project in the Project Navigator.
1. Select your newly created target.
1. In the Arguments text box enter the appropriate command line arguments.
a. $(SRCROOT) is the root directory of your project.
a. If your project path has spaces in it wrap the argument in quotation marks.
a. For example: "$(SRCROOT)/My Project/image.py" -s "$(SRCROOT)/My Project/Resources/Images" -d "$(SRCROOT)/My Project/images.h"
1. Select your build target.
1. Select the “Build Phases” tab on the right.
1. Expand the “Target Dependencies” section.
1. Click “+” and select your new target in the action sheet.
1. Build your project. This will create the images.h file for the first time.
1. Add images.h to your project.
1. Add #import "images.h" to your prefix.pch file.

At this point your project should now recreate the images.h header file with each build, ensuring that your images.h file is always an accurate representation of the image files available in your project. There are still issues that can occur, but now Xcode is working for you. You’ll learn about missing images right away, and will get a very helpful assertion failure if you’ve somehow forgotten to add an image to the build target. (Not that that’s ever an issue with Xcode.) All with less work on your part. Good for you! You get a cookie!

### Update.
Florian Bürger [tweeted](https://twitter.com/efelbi/status/296560328685260801) about his update to the script, which only overwrites the images.h file if there have been changes. I’ve incorporated his changes into my version. Awesome guy, that Florian.

Comments

Iain says:

This is a neat hack, but what I’d really like to see is a change in the language to allow lightweight, easily-declared single purpose singletons, something very close to Ruby’s symbols.

Sharing symbols between Xcode and IB would be a bit of a trick, but I think it could be done.

dmishe says:

Nice, I wonder if there are any performance implications of calling a block each time image is loaded?

Another way that avoids #define could be to create a category on UIImage, or a custom Theme class (like in WWDC example) that loads the image. This way you get compiler checks inside the methods as well?

I the last weeks I’m using an Xcode plugin called KSImageNamed . It helps when you write the image name proposing the suggestion for the name. Obviously it doesn’t allow the “compile time checking of resource requests to ensure the resource exists”

ijansch says:

Very nice approach! The only thing I don’t like is that all names end up in the global completion namespace. If a project has a lot of images, this can be annoying.

In Android development, something similar happens. The precompiler creates an R.drawable object with members for each image. So there the completion is R.drawable.image_something, which doesn’t pollute the global completion namespace.

I think your script might be able to genarate such a class too, instead of a list of defines. It would have a little more runtime overhead, but that would be marginal.

Ivo, unfortunately Objective-C doesn’t have namespaces, so it is impossible to place images in a separate namespace. What I have done is to prepend all image names with “img” to fake a namespace, which is as good as it gets in Objective-C.

nevyn says:

Spotify did this in its desktop client before, for images and localization strings. This meant that every time you modified the resource folder, or the strings file, a header would be regenerated: a header that was included everywhere. Compilation would take minutes. Horrible.

Objective-C does have namespaces, just not namespaces for organizing classes. You could generate a class for each folder, and have accessors nesting access to them. My colleague wader has written this for iPad development: https://github.com/wader/rgen

nevyn says:

Err, for iOS development, of course. Oh, and I didn’t mean to imply that your solution is horrible, only that you can reach madness down that path if you’re not careful.

netbe says:

Funny, we got a similar idea, and came up with a script too here: https://github.com/sinnerschrader-mobile/iOS-resource-helper
It also detects if you forget a retina image…

suhinini says:

Patrick, thanks for the inspiration :) I did my own build-time script that handles magic strings in Localizable.strings – http://suhinini.me/2013/02/09/genloc-stop-using-magic-strings-for-localizing-iososx-projects/