We all repeat ourselves.

Repetitive, menial, mind numbingly brainless repetitive coding tasks. You know what I’m talking about.. copying a list of variable declarations and pasting them right below just to turn them into a list of @property statements, then taking that same list and copying it into an implementation file to turn them into @synthesize statements. You turn the repetitiveness into a kind of mental macro, turn off your brain and let your fingers fly.. “Cmd-Left arrow, type ‘@synthesize’, select the type def, delete, down arrow, Cmd-Left arrow, type ‘@synthesize’, select the type def, delete, down arrow..”

Then, after you’re a few lines in you might think “Wait a minute.. I’m a programmer. I hold the power to bend this pile of silicon and plastic to my will. Shouldn’t my machine be doing this stuff for me? Oh well, only a few lines to go, then I can finally implement the bounce and splatter effects on the zombie brain explosions!”

Enough is enough.

The example of declaring properties and @synthesize statements has never really bothered be too much, mostly because this mental macro is so well rehearsed that by the time I start to feel like I might be wasting my time, I’m finished and can move on. Recently, however, I hit a repetitive coding task that I could not fathom doing by hand.

I was quite a ways into development on Dungeon Delver and came to a point where I realized that people may actually want to save their game. Dungeon Delver is very class heavy (sorry @SnappyTouch) and, since everything is randomly generated, there is a lot of hierarchical data that needs to be saved to cryogenically freeze and reconstitute a user’s unique game world. Luckily, Objective C has a handy way of doing this via NSCoder.

All I had to do was implement the encodeWithCoder and decodeWithCoder methods in the classes that need to be stored and it would take care of all the messy bits of writing and reading data, no muss, no fuss. This would make the whole process of saving and restoring a game session relatively simple, save for one facepalmingly obvious fact.. it was going to be a monumental task to add these functions to each of the relevant classes.

As an example, here is one of the smallest interfaces I needed to store:

@interface Dialog : NSObject {
   NSArray *textStrings;
   BOOL pausesGame;
   float timeToShow;
   
   float timer;
   int index;
   
   BOOL hasBeenRead;
   BOOL repeats;
}

And here are the ecode and decode methods that needed to be created for it:

////////////////////////////////////////////////
// DATA ENCODING / DECODING
////////////////////////////////////////////////

- (void)encodeWithCoder:(NSCoder *)coder
{
   [coder encodeObject:self.textStrings forKey:@"textStrings"];
   [coder encodeBool:self.pausesGame forKey:@"pausesGame"];
   [coder encodeFloat:self.timeToShow forKey:@"timeToShow"];
   [coder encodeFloat:self.timer forKey:@"timer"];
   [coder encodeInt:self.index forKey:@"index"];
   [coder encodeBool:self.hasBeenRead forKey:@"hasBeenRead"];
   [coder encodeBool:self.repeats forKey:@"repeats"];
}

- (id)initWithCoder:(NSCoder *)coder
{
   self = [super init];
   if (self)
   {
      self.textStrings = [coder decodeObjectForKey:@"textStrings"];
      self.pausesGame = [coder decodeBoolForKey:@"pausesGame"];
      self.timeToShow = [coder decodeFloatForKey:@"timeToShow"];
      self.timer = [coder decodeFloatForKey:@"timer"];
      self.index = [coder decodeIntForKey:@"index"];
      self.hasBeenRead = [coder decodeBoolForKey:@"hasBeenRead"];
      self.repeats = [coder decodeBoolForKey:@"repeats"];
   }
   return self;
}

As you can see, the functions themselves are fairly straight forward, but require just a little bit of logic when creating them. For each variable in the class that you want to store you have to determine the type and use the appropriate method of NSCoder to encode and decode it. Doing this by hand for as many classes and variables as I had would take forever.

Scripting to the rescue.

I’ve blabbered on long enough, so let’s cut to the chase.

Xcode’s scripting menu is accessed through the little scripting icon in the apple menu.. this little guy: script menu icon

In there you’ll find a bunch of handy scripts already waiting for you that conduct a variety of tasks, from commenting/uncommenting selected lines, sorting a block of selected lines alphabetically, and creating accessor definitions and declarations. It was this last one that caught my eye. In order to create accessor definitions, the script needs to be aware of the variable’s type, name, and whether it is an object or scalar type.. all of the information required to generate the encoding functions.

So, I cloned the Create Accessor Defs script and modified it to create the functions I required. The whole task took less than an hour and the end result was a script that gets me 95% of the way to having save and restore functionality in my game.

Here’s the completed script:

#! /usr/bin/python
# -*- coding: utf-8 -*-

# Known limitations:
#   - Multiword types (like "unsigned int") are not handled.
#   - The script will not recognise multiple variabes per line/type.
#      In order for the script to work you must change declarations like
#         int x, y;
#      to
#         int x;
#         int y;
#   - The script attempts to determine if the type is an object or not, but it is imperfect at detecting this

import sys
import string
import re

# ================== Script data ==================

# List of known scalar types (no ref-counting in accessors for these types)
knownScalarTypes = ("int", "unsigned", "char", "short", "long", "float", "double", "CGRect", "CGPoint", "CGSize", "NSRect", "NSPoint", "NSSize", "NSRange", "BOOL")


# ================== Templates ==================

preEncodeText = """\
////////////////////////////////////////////////
// DATA ENCODING / DECODING
////////////////////////////////////////////////

- (void)encodeWithCoder:(NSCoder *)coder
{
"""


postEncodeText = """\
}

// init this instances data from the NSCoder
- (id)initWithCoder:(NSCoder *)coder
{
   self = [super init];
   if (self)
   {
"""


postDecodeText = """\
   }
   return self;
}
"""


objectEncodeDecls = """\
   [coder encodeObject:<ivar> forKey:@"<ivar>"];
"""


objectDecodeDecls = """\
   <ivar> = [coder decodeObjectForKey:@"<ivar>"];
"""


scalarEncodeDecls = """\
   [coder encode<type>:<ivar> forKey:@"<ivar>"];
"""


scalarDecodeDecls = """\
   <ivar> = [coder decode<type>ForKey:@"<ivar>"];
"""



# ================== Script ==================

# Get input lines
inputLines = sys.stdin.readlines()

# Strip comments and extra whitespace
commentRE1 = re.compile(r"[   ]*\/\/.*$")
commentRE2 = re.compile(r"[   ]*\/\*.*\*\/[  ]*")

newInputLines = []
for curLine in inputLines:
    curLine = re.sub(commentRE1, "", curLine)
    curLine = re.sub(commentRE2, "", curLine)
    curLine = curLine.strip()
    if curLine != "":
        newInputLines.append(curLine)

inputLines = newInputLines

# Process each line

# Subexpressions:
#     1 - IBOutlet decl
#     2 - First letter of Type name (without pointer *'s)
#     3 - Type name (without pointer *'s)
#     4 - Pointer *'s from type
#     5 - Leading underbar(s) of variable name
#     6 - First letter of variable name
#     7 - Rest of variable name
declRE = re.compile(r"^(IBOutlet)?[    ]*([a-zA-Z])([_a-zA-Z][_a-zA-Z0-9]*)[  ]*(\**)[    ]*([_]*)([a-zA-Z])([_a-zA-Z0-9]*)[  ]*;")

typeRE = re.compile(r"<type>", re.MULTILINE)
keyRE = re.compile(r"<key>", re.MULTILINE)
capKeyRE = re.compile(r"<capKey>", re.MULTILINE)
ivarRE = re.compile(r"<ivar>", re.MULTILINE)

encodeText = ""
decodeText = ""

for curLine in inputLines:
    # Match the line and extract the subexpressions
    matchObj = re.match(declRE, curLine)
    if matchObj != None:
        # Note indices are 1 less than subexpression numbers in the comment above
        subexps = matchObj.groups()
       
        isObject = True
        isArray = False
     
        t = subexps[1] + subexps[2]
     
        if t in knownScalarTypes:
            isObject = False
     
        # Figure out the substitution strings for the accessor templates
        if subexps[3] != "":
            curType = string.upper(subexps[1]) + subexps[2] + " " + subexps[3]
        else:
            curType = string.upper(subexps[1]) + subexps[2]
         
      # Fix BOOL type for use in the coder function name
        if curType == "BOOL":
            curType = "Bool";

        curKey = subexps[5] + subexps[6]
        curCapKey = string.upper(subexps[5]) + subexps[6]
        curIvar = subexps[4] + subexps[5] + subexps[6]
       
        # Build the result
        if isObject:
            curEncodeText = objectEncodeDecls
            curDecodeText = objectDecodeDecls
        else:
            curEncodeText = scalarEncodeDecls
            curDecodeText = scalarDecodeDecls
       
        curEncodeText = re.sub(typeRE, curType, curEncodeText)
        curEncodeText = re.sub(keyRE, curKey, curEncodeText)
        curEncodeText = re.sub(capKeyRE, curCapKey, curEncodeText)
        curEncodeText = re.sub(ivarRE, curIvar, curEncodeText)
       
        curDecodeText = re.sub(typeRE, curType, curDecodeText)
        curDecodeText = re.sub(keyRE, curKey, curDecodeText)
        curDecodeText = re.sub(capKeyRE, curCapKey, curDecodeText)
        curDecodeText = re.sub(ivarRE, curIvar, curDecodeText)
       
        encodeText += curEncodeText
        decodeText += curDecodeText
     
code = preEncodeText + encodeText + postEncodeText + decodeText + postDecodeText
print code,

So there you have it. All I had to do is copy the variable declarations for an interface to my clipboard and hit the shortcut key I assigned to the script in the scripts window to put the functions on the clipboard. A huge step towards saving my game and my sanity.

Unlimited possibilities.

Scripting is one of those things that is either ingrained into your psyche as a developer or not. I know plenty of programmers that would be perfectly happy copying and pasting and modifying code over and over and never give it a second thought. I also know a much smaller group of guys that won’t even move a file without considering writing a Bash script to do it for them.

As you can see, the potential for what you can do with scripting in Xcode is pretty incredible, and if you can get yourself comfortable with one of the common scripting languages (the sample scripts in Xcode are a mix of Bash scripts, Python and Perl) and force yourself to look at the act of coding as something that can be improved by more coding then you’ll be well on your way to a higher plane of development.

So, the next time you find yourself executing one of those ‘mental macros’ over 50 lines of code, and the realization strikes you that your fingers are getting sore and you’re not getting paid by the hour, hopefully you’ll realize that a better way is waiting for you up there in that mysterious little squiggly scroll icon.

« »