Ironically, for an article about concise error reporting, this entry is rather long. But don’t worry, I’ve given you a ‘get out of jail free card’ a few paragraphs down, if you wish to use it! :p
I added some code today which reads game level data from a property list. At some point between now and when Animal Tracker is submitted to the App Store, it’s entirely possible, even probable that there will be errors in the data. As a result, I need to check all the data as they are read in and log any errors that may have occurred.
Now, that in itself isn’t a problem. It’s just that if I’m not careful, I’ll be writing as many lines of error checking code for that part of the program as there are lines of actual game code, making it difficult to see the proverbial wood for the trees.
Another factor to consider is that when the game is actually running on an iDevice, any error logs that are generated never get seen and just slow the game down. So, ideally, when the game is compiled for a release build (where it will run on an actual iDevice, as opposed to a debug build, where it will just run in a simulator on the Mac), there shouldn’t be any error logs generated at all.
Well, what follows is quite technical (although I’ve endeavoured to explain things for the non-technically minded), so if you’re not into all that stuff, I’ll forgive you if you stop reading now… and I’ll see you next time! lol
And for those of you who already know about programming or who would like to learn a little, read on! 🙂
Here’s an example of what I’m talking about:
if ((regionName = [regionDictionary objectForKey:@"Name"])) {
[newRegion setName:regionName];
} else {
NSLog(@"[RootViewController parseRegions] Region %d: 'Name' key not found", i);
}
Firstly, don’t worry about trying to understand what the code does as that’s not important. What is important, however, is that the first three lines are actually doing some useful work, but the last three lines only log an error should one occur – and even then, only when the game is run on the simulator on the Mac.
So that the log code disappears when the game is built to run on an actual iDevice, I changed the code to:
if ((regionName = [regionDictionary objectForKey:@"Name"])) {
[newRegion setName:regionName];
}
#if DEBUG
else { NSLog(@"[RootViewController parseRegions] Region %d: 'Name' key not found", i); }
#endif
To understand the last three lines, I need to introduce the ‘preprocessor’. It’s a utility which looks through your program code for special preprocessor commands and then alters that code depending on whether certain conditions have been met.
The ‘#if’ and ‘#endif’ keywords are preprocessor commands, which in this case tell the preprocessor to only include the ‘else’ command in the final program if we’re in DEBUG mode. When in RELEASE mode (for running on an actual iDevice), the ‘else’ line isn’t included in the program at all, resulting in a smaller executable program. The down side now though is that I’ve doubled the number of lines of code in the text editor.
What I need, then, is to boil those last three lines down to just one, which I chose to do so as follows:
if ((regionName = [regionDictionary objectForKey:@"Name"])) {
[newRegion setName:regionName];
}
ELSE_LOG1(@"[RootViewController parseRegions] Region %d: 'Name' key not found", i)
First, I need to introduce the concept of a ‘macro’. It’s really just a copy and paste mechanism that is performed by our friend the preprocessor. Here, the macro called ‘ELSE_LOG1’ takes two arguments (objects which are passed to the macro for later use). What it actually does, we don’t know yet, because I haven’t defined it yet, so I’ll do that now:
#ifdef DEBUG
#define ELSE_LOG1(string,a) else { NSLog(string, a); }
#else
#define ELSE_LOG1(string,a)
#endif
What this basically says to the preprocessor is, ‘If we’re in DEBUG mode, replace every occurence of ‘ELSE_LOG1’ you find in the program with the code ‘else { NSLog(string, a); }, but instead of writing ‘string’, write the first object that was passed in to the macro, and instead of ‘a’, write the second object passed in (as separated by a comma)’.
So, in our case, ‘string’ will be replaced with:
@"[RootViewController parseRegions] Region %d: 'Name' key not found"
and ‘a’ will be replaced with:
i
In DEBUG mode, then, the preprocessor will effectively expand our code from:
ELSE_LOG1(@"[RootViewController parseRegions] Region %d: 'Name' key not found", i)
to:
else { NSLog(@"[RootViewController parseRegions] Region %d: 'Name' key not found", i); }
The last three lines of the preprocessor code tell the preprocessor, ‘If we’re not in DEBUG mode, replace all occurences of the ‘ELSE_LOG1’ macro (and its arguments) with… nothing! Nada. Empty space. This effectively removes the whole line from the program when it’s built in any mode other than DEBUG (i.e. RELEASE)… which is what we want!
So, the original code:
if ((regionName = [regionDictionary objectForKey:@"Name"])) {
[newRegion setName:regionName];
}
ELSE_LOG1(@"[RootViewController parseRegions] Region %d: 'Name' key not found", i)
… in DEBUG mode becomes:
if ((regionName = [regionDictionary objectForKey:@"Name"])) {
[newRegion setName:regionName];
}
else { NSLog(@"[RootViewController parseRegions] Region %d: 'Name' key not found", i); }
… and in RELEASE mode becomes:
if ((regionName = [regionDictionary objectForKey:@"Name"])) {
[newRegion setName:regionName];
}
Well, congratulations if you’ve made it this far… woohoo! 🙂 See you next time!
Read Full Post »