Created
May 8, 2018 12:36
-
-
Save holtwick/53ac035dd3eff102c68a4470cd195ea3 to your computer and use it in GitHub Desktop.
Cosy NSObject
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// (C)opyright 2018-04-25 Dirk Holtwick, holtwick.it. All rights reserved. | |
#import <Foundation/Foundation.h> | |
/* A dictionary like object that allows to define custom dynamic properties | |
* for typed and easy access. Only simple types like string, number, array and | |
* dictionary are supported. It is mainly thought to be a convenience class that | |
* can be serialized easily to JSON and other formats. | |
* | |
* MAKE SURE to add @dynamic for any property you define in a subclass of SeaObject! | |
*/ | |
@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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// (C)opyright 2018-04-25 Dirk Holtwick, holtwick.it. All rights reserved. | |
#import "SeaObject.h" | |
@implementation SeaObject { | |
__strong NSMutableDictionary *_properties; | |
BOOL _needsSave; | |
} | |
+ (void)initialize { | |
[super initialize]; | |
if (self != [SeaObject self]) { | |
NSArray *danger = [self getDangerousPropertyNames]; | |
if (danger) { | |
NSLog(@"In class <%@> dangerous properties have been identified. Add the follwing code:\n\n@dynamic %@\n", | |
NSStringFromClass(self), | |
[danger componentsJoinedByString:@", "]); | |
id reason = [NSString stringWithFormat:@"In class <%@> dangerous properties have been identified. Add the follwing code: @dynamic %@", | |
NSStringFromClass(self), | |
[danger componentsJoinedByString:@", "]]; | |
@throw [NSException exceptionWithName:@"Missing @dynamic" | |
reason:reason | |
userInfo:nil]; | |
} | |
} | |
} | |
- (void)setJsonDictionary:(NSDictionary *)obj { | |
[self willChangeValueForKey:@"allKeys"]; | |
if (obj) { | |
_properties = [self cleanedObject:obj forImport:YES]; | |
} | |
if (!_properties) { | |
_properties = [NSMutableDictionary dictionary]; | |
} | |
[self didChangeValueForKey:@"allKeys"]; | |
} | |
- (NSMutableDictionary *)jsonDictionary { | |
return [self cleanedObject:_properties forImport:NO] ?: [NSMutableDictionary dictionary]; | |
} | |
- (void)configure { | |
// Can be overridden | |
} | |
- (instancetype)initWithDictionary:(NSDictionary *)dict { | |
self = [super init]; | |
if (self) { | |
[self setJsonDictionary:dict]; | |
[self configure]; | |
} | |
return self; | |
} | |
- (instancetype)init { | |
self = [super init]; | |
if (self) { | |
[self setJsonDictionary:nil]; | |
[self configure]; | |
} | |
return self; | |
} | |
#pragma mark - Subclassing | |
- (NSUInteger)count { | |
return _properties.count; | |
} | |
- (id)objectForKey:(id)aKey { | |
return [_properties objectForKey:aKey]; | |
} | |
- (void)setObject:(id)value forKey:(id<NSCopying>)aKey { | |
NSString *key = (id)aKey; | |
if (!value) { | |
[self removeObjectForKey:key]; | |
} else { | |
// Asset, Reference | |
if (!([value isKindOfClass:[NSNumber class]] || | |
[value isKindOfClass:[NSArray class]] || | |
[value isKindOfClass:[NSDictionary class]] || | |
[value isKindOfClass:[SeaObject class]] || | |
[value isKindOfClass:[NSString class]] | |
)) { | |
@throw [NSString stringWithFormat:@"Unsupported object class %@", NSStringFromClass([value class])]; | |
} | |
id oldValue = [_properties objectForKey:key]; | |
if (oldValue != value && ![oldValue isEqual:key]) { | |
[self willChangeValueForKey:key]; | |
[_properties setObject:value forKey:key]; | |
[self didChangeValueForKey:key]; | |
self.needsSave = YES; | |
} | |
} | |
} | |
- (void)removeObjectForKey:(id)key { | |
id oldValue = [_properties objectForKey:key]; | |
if (oldValue) { | |
[self willChangeValueForKey:key]; | |
[_properties removeObjectForKey:key]; | |
[self didChangeValueForKey:key]; | |
} | |
} | |
- (NSEnumerator *)keyEnumerator { | |
return _properties.keyEnumerator; | |
} | |
- (NSArray<NSString *> *)allKeys { | |
return _properties.allKeys; | |
} | |
- (void)setObject:(id)obj forKeyedSubscript:(NSString *)key { | |
[self setObject:obj forKey:key]; | |
} | |
- (id)objectForKeyedSubscript:(NSString *)key { | |
return [self objectForKey:key]; | |
} | |
- (void)setValue:(id)value forUndefinedKey:(NSString *)key { | |
[self setObject:value forKey:key]; | |
} | |
- (id)valueForUndefinedKey:(NSString *)key { | |
return [self objectForKey:key]; | |
} | |
#pragma mark - Serializing | |
- (id)cleanedObject:(id)objectToBeCleaned forImport:(BOOL)forImport { | |
if (!objectToBeCleaned) { | |
return nil; | |
} | |
__strong id obj = objectToBeCleaned; | |
if ([obj isKindOfClass:[NSDictionary class]]) { | |
if (forImport) { | |
SeaObject *dict = [[SeaObject alloc] initWithDictionary:nil]; | |
for(id key in obj) { | |
id value = [obj objectForKey:key]; | |
if (value) { // Strip null | |
value = [self cleanedObject:value forImport:forImport]; | |
if (value) { // Strip null | |
dict[key] = value; | |
} | |
} | |
} | |
obj = dict; | |
} | |
else { | |
NSMutableDictionary *dict = [NSMutableDictionary dictionary]; | |
for(id key in obj) { | |
id value = [obj objectForKey:key]; | |
if (value) { // Strip null | |
value = [self cleanedObject:value forImport:forImport]; | |
if (value) { // Strip null | |
dict[key] = value; | |
} | |
} | |
} | |
obj = dict; | |
} | |
} | |
else if ([obj isKindOfClass:[SeaObject class]]) { | |
if (!forImport) { | |
obj = [obj jsonDictionary]; | |
} | |
} | |
else if ([obj isKindOfClass:[NSArray class]] || [obj isKindOfClass:[NSSet class]]) { | |
NSMutableArray *array = [NSMutableArray array]; | |
for(id value in obj) { | |
if (value) { // Strip null | |
[array addObject:[self cleanedObject:value forImport:forImport]]; | |
} | |
} | |
obj = array; | |
} | |
return obj; | |
} | |
#pragma mark - Check dynamic properties! | |
+ (NSArray *)getDangerousPropertyNames { // https://stackoverflow.com/a/11774276/140927 | |
unsigned count; | |
objc_property_t *properties = class_copyPropertyList(self, &count); | |
NSMutableArray *dangerousPropertiyNames = [NSMutableArray array]; | |
for (unsigned i = 0; i < count; i++) { | |
objc_property_t property = properties[i]; | |
NSString *name = [NSString stringWithUTF8String:property_getName(property)]; | |
NSString *attr = @(property_getAttributes(property)); | |
if ([attr containsString:@",V_"]) { | |
[dangerousPropertiyNames addObject:name]; | |
} | |
} | |
free(properties); | |
return dangerousPropertiyNames.count > 0 ? dangerousPropertiyNames : nil; | |
} | |
#pragma mark - Dynamic Properties | |
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector { | |
NSString *sel = NSStringFromSelector(selector); | |
// NSLog(@"methodSignatureForSelector:%@", sel); | |
if ([sel rangeOfString:@"set"].location == 0) { | |
return [NSMethodSignature signatureWithObjCTypes:"v@:@"]; | |
} else { | |
return [NSMethodSignature signatureWithObjCTypes:"@@:"]; | |
} | |
} | |
- (void)forwardInvocation:(NSInvocation *)invocation { | |
NSString *sel = NSStringFromSelector(invocation.selector); | |
// NSLog(@"forwardInvocation:%@", sel); | |
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]; | |
} | |
} | |
#pragma mark - Debug | |
- (NSString *)description { | |
return [NSString stringWithFormat:@"<%@\n _properties=%@>", | |
self.className, | |
_properties]; | |
} | |
- (id)copyWithZone:(NSZone *)zone { | |
return [[self.class alloc] initWithDictionary:[self jsonDictionary]]; | |
} | |
#pragma mark - IO | |
- (BOOL)writeAsJSON:(id)path { | |
id data = [NSJSONSerialization dataWithJSONObject:self.jsonDictionary | |
options:NSJSONWritingPrettyPrinted | |
error:nil]; | |
if (data) { | |
if ([data writeToFile:path atomically:YES]) { | |
self.needsSave = NO; | |
return YES; | |
} | |
} | |
return NO; | |
} | |
- (BOOL)readAsJSON:(id)path { | |
NSError *error = nil; | |
id data = [NSData dataWithContentsOfFile:path]; | |
id json = [NSJSONSerialization JSONObjectWithData:data | |
options:NSJSONReadingMutableLeaves | NSJSONReadingMutableContainers | |
error:&error]; | |
self.jsonDictionary = json; | |
self.needsSave = NO; | |
if (!json) { | |
NSLog(@"Error %@", error); | |
return NO; | |
} | |
return YES; | |
} | |
@end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment