Potion Factory Blog

F#$% Nil Outlets

This is for you cocoa developers out there.

A common error I keep making is forgetting to set an outlet in Interface Builder. Usually I catch the problem pretty quickly but once in a blue moon it can leave me scratching my head for too long a period of time to confess here.

I love problems that can be engineered away and after making this mistake yet again, I decided to solve it once and for all. I'm sharing the solution because it has worked out splendidly.

The following code parses your header file for outlets and makes sure that they are set at runtime.

In a header file:

#ifndef RELEASE
#define PFValidateOutlets(obj) PFValidateOutlets_(obj, [NSString stringWithFormat:@"%s", __FILE__], nil)
#define PFValidateOutletsInFile(obj, header) PFValidateOutlets_(obj, [NSString stringWithFormat:@"%s", __FILE__], header)
#else
#define PFValidateOutlets(obj)
#define PFValidateOutletsInFile(obj, header)
#endif

In a .m file:

#import <objc/runtime.h>

void PFValidateOutlets_(id obj, NSString *filepath, NSString *headerFilename)
{
#ifndef RELEASE
    if (headerFilename) {
        filepath = [[filepath stringByDeletingLastPathComponent] stringByAppendingPathComponent:headerFilename];
    }
    if (![filepath hasSuffix:@".h"]) {
        filepath = [NSString stringWithFormat:@"%@.h", [filepath stringByDeletingPathExtension]];
    }

    // Parse the outlets
    if ([[NSFileManager defaultManager] fileExistsAtPath:filepath]) {
        NSString *source = [NSString stringWithContentsOfFile:filepath];
        NSArray *classes = [source componentsSeparatedByString:@"@interface"];

        NSCharacterSet *spacesSet = [NSCharacterSet whitespaceAndNewlineCharacterSet];
        NSCharacterSet *wordTokenizingSet = [NSCharacterSet characterSetWithCharactersInString:@" \t\n\r{}*:;"];
        NSCharacterSet *statementTokenizingSet = [NSCharacterSet characterSetWithCharactersInString:@"{}:;/"];
        NSCharacterSet *lineSeparatingSet = [NSCharacterSet characterSetWithCharactersInString:@"\n\r"];

        for (NSString *classSource in classes) {
            NSScanner *scanner = [NSScanner scannerWithString:classSource];
            [scanner scanCharactersFromSet:spacesSet intoString:nil];

            NSString *className;

            // Proceed if the parsed class matches the object's class
            if ([scanner scanUpToCharactersFromSet:wordTokenizingSet intoString:&className] &&
                [className isEqualToString:NSStringFromClass([obj class])]) {

                NSArray *lines = [classSource componentsSeparatedByCharactersInSet:lineSeparatingSet];

                for (NSString *line in lines) {
                    // Skip comment lines
                    if ([[line stringByTrimmingCharactersInSet:spacesSet] hasPrefix:@"//"]) continue;

                    NSArray *statements = [line componentsSeparatedByCharactersInSet:statementTokenizingSet];
                    for (NSString *statement in statements) {
                        if ([[statement stringByTrimmingCharactersInSet:spacesSet] hasPrefix:@"IBOutlet"]) {
                            NSString *outletName = [[statement componentsSeparatedByCharactersInSet:wordTokenizingSet] lastObject];
                            Ivar outletIvar = class_getInstanceVariable([obj class], [outletName UTF8String]);
                            id outletValue = object_getIvar(obj, outletIvar);
                            if (outletValue == nil)
                                NSLog(@"WARNING -- %@'s outlet %@ is nil", className, outletName, outletValue);
                        }
                    }
                }
            }
        }
    }
#endif
}

Usage:

- (void)awakeFromNib
{
    PFValidateOutlets(self);
    // Rest of the code
}

As you can see, I'm invoking the validation manually in awakeFromNib of classes that have outlets. It would be awesome if there were way to make the check fully automatic but I stopped investigating because this is good enough for me. Let me know if you think of something though.

Comments

Post new comment

The content of this field is kept private and will not be shown publicly.
  • Allowed HTML tags: <a> <em> <strong> <cite> <code> <ul> <ol> <li> <dl> <dt> <dd>
  • Lines and paragraphs break automatically.
  • Web page addresses and e-mail addresses turn into links automatically.

More information about formatting options

1
9
7
7
Enter the code without spaces and pay attention to upper/lower case.