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
andNSNumber
- Allowing deeper levels by adding
NSArray
orNSDictionary
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 writeobj.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.
Published on May 8, 2018