Software developers can benefit a lot from automatic code generation tools. They take tedious, repetitive processes out of the workflow, and help eliminate potential mistakes caused by sloppy development (or in the case of iOS, poor spelling in stringly-typed code).
To help simplify automatic code generation, I decided to make my own Xcode plugin. My initial goal was to automatically run Cat2Cat (an Asset Catalog to UIImage/NSImage Category utility that Vokal open-sourced) whenever anything in an asset catalog folder changed. That way, I wouldn’t have to rely on running the script to kick off the utility myself to ensure that the generated code was always correct.
However, as I continued to think about it, that goal seemed a bit limited. We already use several command line utilities on a regular basis to auto-generate boilerplate code. A couple public ones I've used are Wolf Rentzsch's MOGenerator and the identifierconstants utility within Square's objc-codegenutils.
I realized that each of these code-generation utilities could be tied to either a specific file or folder:
- MOGenerator should run anytime anything in the .xcdatamodeld is changed.
- Cat2Cat should run anytime anything in an asset catalog is changed.
- identifierconstants should run when any storyboard is changed.
With these in mind, I expanded my goal to make the plugin be able to watch various files and folders, then run a given script any time changes in that file or a folder are made. The result of this was XcodeAutoBasher.
At Vokal, we have quarterly Hack Days when we get the opportunity to work on whatever we want, so long as it is in some way beneficial to the company. For our first Hack Days last year, I decided to take on building the plugin as a way to speed up our code generation workflow.
Apple's documentation for creating Xcode plugins is a bit thin on the ground, unfortunately. So I did what I always do when Apple's documentation isn't great: I Googled it. Starting from NSHipster's article on Xcode plugins, I found an excellent template for creating Xcode plugins, which allowed me to get started early and focus learning around Mac OS X process and UI handling instead of how to actually create a working plugin.
First, I learned that dealing with the folder-watching API (which uses kernel queues) in OS X is pretty painful. After a few aborted attempts to get it working based on Apple's sample code, I was able to track down the highly useful VDKQueue class, which wrapped this functionality very nicely in Objective-C.
Next, I learned a bit about Mac UI development - I'd only built a very, very simple example app for Cat2Cat for the Mac, so this was my first run at NSTableView and its delegate, which is a bit different from its UIKit equivalent. In the end, my experience working with AutoLayout on iOS proved highly beneficial, as it allowed me to think resizably from the start. I didn’t initially take advantage of Cocoa bindings, but not starting from that position allowed me to actually get a working initial version together within our Hack Days’ time frame.
Finally, I learned about the Xcode plugin debugging system. One of the most frustrating parts of writing an Xcode plugin is trying to debug the plugin. Since you have to build a plugin as a binary then restart Xcode to try your changes, the only way to get any info about the process as it executes is to add a whole lot of logging and watch the console as you click through it. It was maddening, but it did the job.
Beyond The Hack
The plugin got the job done after my work during Hack Days, but still had quite a bit of room for improvement and refinement. My colleague Isaac Greenspan, who has significantly more OS X development experience than I do, had a bit of time available after Hack Days, and used his considerable skill set to improve what I’d started.
The plugin is named XcodeAutoBasher, since a) The idea of bashing Xcode with a hammer was an amusing mental image and b) originally, all scripts that it runs had to be written in Bash. Isaac, being a man of many languages, made his first order of business updating the runner to use shebangs, allowing us to execute scripts in any language.
Next, he brought his knowledge of desktop development to revamp the UI and allow multiple file/folder watches to be separated by project, rather than be handled individually. He swapped the NSTableView I’d been using for an NSOutlineView and an NSTreeController to use Cocoa Bindings to its best advantage. He also changed the trigger for firing up watchers from when Xcode opened to when an individual project opened. This way, we could set up a significant number of watches across multiple projects but not constantly be watching files and folders that weren’t in use.
Finally, Isaac thought it would be a good idea to share settings so that multiple members of the same team could check those settings and their scripts into version control and have their scripts run automatically without having to set anything up. He added the .plist.XcAB file format to facilitate storing this on a per-project basis.
While Isaac accomplished such things as “making the app significantly saner under the hood and much more functional,” I took my tiny bit of available time to create our amazing logo. No designers were harmed (or consulted) in the making of this icon.
Throughout this process, we obviously learned a lot from a technical standpoint. From a more general standpoint, we also learned that giving ourselves dedicated time to experiment and build new tools can lead to significant time savings (which turn into financial savings for our clients).
- Automatic running of any executable, including scripts in any language, whenever anything in a folder changes while the project is open.
- Automatic startup/shutdown of file/folder watches tied to when the project opens and closes in Xcode.
- Version-control-committable files to allow any user of XcodeAutoBasher to run your scripts, so long as they are not tied to anything specific on one user’s filesystem.
I’ve now been using it for over six months, and I’ve been pleasantly surprised to find that, with Isaac’s improvements, it’s exceeded my expectations for how much time it can save. If you have any suggestions for further enhancements, please open an issue on our GitHub Page.