Cosy NSObject

Often I'm finding myself in a situation, where a lot of nonsense repeating code needs to be written, like for example when implementing NSCoder for an object or when the syntax does not appeal me like keyed subscription for a dictionary like object.

Since I'm still coding in Objective-C, I tried to find an easy solution for these requirements:

  • An object with simple properties like NSString and NSNumber
  • Allowing deeper levels by adding NSArray or NSDictionary properties
  • Should serialize and deserialize to JSON or MessagePack easily
  • Should have type check and autocompletion in the editor i.e. instead of obj[@"name"] I would like to write obj.name
  • It shouldn't be to strict about everything :)

I know there is a lot of prior work doing similar magic and also CoreData comes with similar features, but hey, sometimes it is fun to just do it yourself.

So I started with a base class SeaObject derived from NSObject. This is what the header looks like:

@interface SeaObject : NSObject <NSCopying>

@property (nonatomic, assign) BOOL needsSave;
@property (nonatomic, readonly) NSUInteger count;
@property (nonatomic, readonly) NSEnumerator *keyEnumerator;
@property (nonatomic, readonly) NSArray<NSString *> *allKeys;
@property (nonatomic, copy) NSDictionary *jsonDictionary;

- (instancetype)initWithDictionary:(NSDictionary *)dict;
- (void)configure;

- (id)objectForKey:(id)aKey;
- (void)setObject:(id)anObject forKey:(id<NSCopying>)aKey;
- (void)removeObjectForKey:(id)key;
- (void)setObject:(id)obj forKeyedSubscript:(NSString *)key;
- (id)objectForKeyedSubscript:(NSString *)key;

- (BOOL)writeAsJSON:(id)path;
- (BOOL)readAsJSON:(id)path;

@end

If e.g. I want to build a todo list I can do like this

@interface TodoItem : SeaObject
@property NSString *title;
@property NSNumber *done;
@end

The implementations requires some @dynamic declarations in order to get through to my fallback algorithms I'll describe later:

@implementation SeaObject
@dynamic title, done;
@end

Now I can nicely set properties like this:

item.title = @"Clean kitchen";
item.done = @NO;

On the implementation side of SeaObject all data is stored in NSMutableDictionary *_properties. The glue code for the keyed subscription part is trivial.

The magic is in the code that handle the access to the properties. Since we used @dynamic before, there is no counterpart on the implementation side for those properties. This is why we can override some fallbacks and voila everything ends up in setObject:forKey and objectForKey:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
    NSString *sel = NSStringFromSelector(selector); 
    if ([sel rangeOfString:@"set"].location == 0) {
        return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
    } else {
        return [NSMethodSignature signatureWithObjCTypes:"@@:"];
    }
}

- (void)forwardInvocation:(NSInvocation *)invocation {
    NSString *sel = NSStringFromSelector(invocation.selector);
    if ([sel rangeOfString:@"set"].location == 0) {
        sel = [NSString stringWithFormat:@"%@%@",
               [sel substringWithRange:NSMakeRange(3, 1)].lowercaseString,
               [sel substringWithRange:NSMakeRange(4, sel.length-5)]];
        id __unsafe_unretained obj;
        [invocation getArgument:&obj atIndex:2];
        [self setObject:obj forKey:sel];
    } else {
        id obj = [_properties objectForKey:sel];
        [invocation setReturnValue:&obj];
    }
}

Some more magic reading and writing dictionaries and this nice little helper is doing its job. Also adding categories works nicely.

Source Code You get the full source code at GitHub.

Please leave your comments below. I'm looking forward to your feedback.