January 22, 2014

Juggling a full time job with aspirations of becoming a full time indie developer means using time as efficiently as possible. I generally plan work around one or two fleeting hours per day. When the stars align, I am allowed half a day, or even a full day to dedicate to my current project. A snow day today allowed me to dedicate an entire day to knocking out a few features. I ran into a somewhat frustrating, and subtle gotcha whose cause is not immediately obvious.

The current, conventional, approach to implementing an Objective-C singleton leverages Grand Central Dispatch (GCD) to control access to a static pointer, pointing to the instantiated object. This approach promises thread-safe access to the singleton, in addition to being an extremely neat and concise implementation. A typical implementation's accessor method should look similar:

+ (instancetype)sharedInstance {
   static dispatch_once_t pred = 0;
   static TPLSettingsManager *_sharedObject = nil;

   dispatch_once(&pred, ^{
      if ( !_sharedObject ) {
         _sharedObject = [[super alloc] init];
      }
   });

   return _sharedObject;
}

Checking whether _sharedObject is a valid address is not necessary, but the extra check does not hurt. Notice that I am calling the alloc method on the the class' super class. My singleton implementation overrides alloc to prevent client code from erroneously invoking it. So far so good, right?

Here's where things get interesting. My settings manager class is Key Value Coding (KVC) compliant, so naturally I implement:

- (void)setValue:(id)value forKey:(NSString *)key
- (void)setValuesForKeysWithDictionary:(NSDictionary *)keyedValues
... 

Read more: Apple Documentation

Internally, I represent settings using an immutable NSDictionary instance. In a moment of distraction, I implemented setValue:forKey by referencing the shared instance instead of self:

- (void)setValue:(id)value forKey:(NSString *)key {
   [self immutableSetValue:value forKey:key];
}

- (void)immutableSetValue:(id)value forKey:(NSString *)key {
   [self willChangeValueForKey:key];

   self.settingsDictionary = [[TPLSettingsManager sharedInstance]] 
     mergeDictionaries:self.settingsDictionary with:@{key: value}];

   [self didChangeValueForKey:key];
}

The error is subtle, and it is transient like an electrical problem in a British, well, anything. What happens is that the call to sharedInstance from self, despite overriding alloc reinitializes _sharedObject. I stepped through the code countless times in the debugger and have verified each time that _sharedObject is set to nil as soon as execution enters sharedInstance. It is very Peculiar. I can only guess the behavior is a result of a scoping conflict for the static reference to the singleton. I suppose it's safer to initialize a static variable to nil than it is to assume an address in an unknown context.

The following change to the the code fixed the issue:

    self.settingsDictionary = [self mergeDictionaries:self.settingsDictionary with:@{key: value}];

Referencing self from within the instance rather than relying on the returned value from sharedInstance is a fix, but I'm not completely sure why.