Skip to content

Instantly share code, notes, and snippets.

@avaidyam
Last active February 26, 2018 00:56
Show Gist options
  • Save avaidyam/f4aca393fa192d1d7bac1a98c853bfc9 to your computer and use it in GitHub Desktop.
Save avaidyam/f4aca393fa192d1d7bac1a98c853bfc9 to your computer and use it in GitHub Desktop.
Automatically map between an NSObject and a dictionary representation, including custom properties and excluding cyclic ones.
@implementation ExampleClass
+ (void)load {
NSArray *ignoreList = [NSArray arrayWithObjects:
[[NSIgnoredClassKey alloc] initWithClassName:@"GPBOneofDescriptor" key:@"containingOneof"],
nil];
NSArray *includeList = [NSArray arrayWithObjects:
[[NSIncludedClassKey alloc] initWithClassName:@"GPBEnumDescriptor" key:@"namedValues" block:^(id obj)
{
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[obj performSelector:NSSelectorFromString(@"calcValueNameOffsets")];
#pragma clang diagnostic pop
uint32_t valueCount = (uint32_t)[obj instanceVariableForKey:@"valueCount_"];
const int32_t *values = (const int32_t *)[obj instanceVariableForKey:@"values_"];
int32_t *offsets = (int32_t *)[obj instanceVariableForKey:@"nameOffsets_"];
const char *names = (const char *)[obj instanceVariableForKey:@"valueNames_"];
// Wrap into [value: name] dictionary.
NSMutableDictionary *wrapper = [NSMutableDictionary dictionary];
for (uint32_t i = 0; i < valueCount; i++) {
int32_t value = values[i];
int32_t offset = offsets[i];
const char *name = names + offset;
if (name == nil) continue;
[wrapper setObject:[NSString stringWithFormat:@"%s", name]
forKey:[NSString stringWithFormat:@"%d", value]];
}
return wrapper;
}], [[NSIncludedClassKey alloc] initWithClassName:@"GPBDescriptor" key:@"extensionRanges" block:^(id obj)
{
uint32_t valueCount = (uint32_t)[obj instanceVariableForKey:@"extensionRangesCount_"];
const GPBExtensionRange *values = (const GPBExtensionRange *)[obj instanceVariableForKey:@"extensionRanges_"];
NSMutableArray *wrapper = [NSMutableArray array];
// Wrap into [value: name] dictionary.
for (uint32_t i = 0; i < valueCount; i++) {
GPBExtensionRange v = values[i];
[wrapper addObject:[NSString stringWithFormat:@"%u...%u", v.start, v.end]];
}
return wrapper;
}], nil];
// Traverse the entire message hierarchy, touching all file and enum descriptors referenced.
NSMutableArray *descriptors = [NSMutableArray array];
[[NSObject allSubclasses:@"GPBMessage"] enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
id descriptor = [obj safeValueForKey:@"descriptor"];
id mapped = [descriptor propertyDictionaryIgnoringKeys: ignoreList including: includeList];
[descriptors addObject:mapped];
}];
// The extention registry & root hierarchy is a bit trickier; many nested dictionaries.
NSMutableDictionary *extensions = [NSMutableDictionary dictionary];
[[NSObject allSubclasses:@"GPBRootObject"] enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
NSMutableDictionary *registry = [[obj safeValueForKey:@"extensionRegistry"] instanceVariableForKey:@"mutableClassMap_"];
NSMutableDictionary *output = [NSMutableDictionary dictionaryWithCapacity:registry.count];
[registry enumerateKeysAndObjectsUsingBlock:^(id key /* Class */, id obj /* CFDictionaryRef */, BOOL *stop) {
NSMutableDictionary *inner = [NSMutableDictionary dictionary];
[obj enumerateKeysAndObjectsUsingBlock:^(id key2 /* long */, id obj2 /* ExtensionDescriptor */, BOOL *stop2) {
id mapped = [obj2 propertyDictionaryIgnoringKeys: ignoreList including: includeList];
[inner setObject:mapped forKey:[NSString stringWithFormat:@"%@", key2]];
}];
[output setObject:inner forKey:NSStringFromClass(key)];
}];
[extensions setObject:output forKey:NSStringFromClass([obj class])];
}];
[self writeObject:descriptors toFile:@"classeslist-details.txt"];
[self writeObject:extensions toFile:@"classeslist-ext.txt"];
}
+ (void)writeObject:(id)object toFile:(NSString *)filename {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSURL *fileURL = [NSURL fileURLWithPath:[documentsDirectory stringByAppendingPathComponent:filename]];
// Write JSON file and show output.
NSError *error;
NSData *dat = [NSJSONSerialization dataWithJSONObject:object options:NSJSONWritingPrettyPrinted error:&error];
if (error == nil) {
NSString *str = [[NSString alloc] initWithData:dat encoding:NSUTF8StringEncoding];
[str writeToURL:fileURL atomically:YES encoding:NSUTF8StringEncoding error:&error];
if (error == nil) {
[self showText:@"OK!"];
} else {
[self showText:error.debugDescription];
}
} else {
[self showText:error.debugDescription];
}
}
+ (void)showText:(NSString *)text {
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
UIAlertController *alertController = [UIAlertController alertControllerWithTitle: @"Message"
message: text
preferredStyle: UIAlertControllerStyleAlert];
[alertController addAction: [UIAlertAction actionWithTitle: @"OK"
style: UIAlertActionStyleCancel
handler: nil]];
UIViewController *controller = [UIApplication sharedApplication].keyWindow.rootViewController;
while (controller.presentedViewController) {
controller = controller.presentedViewController;
}
[controller presentViewController:alertController animated:YES completion:NULL];
});
}
@end
@import Foundation;
NS_ASSUME_NONNULL_BEGIN
@interface NSIgnoredClassKey: NSObject
@property (retain) NSString *key;
@property (retain) NSString *className;
- (instancetype)initWithClassName:(NSString *)className key:(NSString *)key;
@end
typedef id _Nullable (^NSIncludedClassKeyValueTransformer)(id _Nonnull);
@interface NSIncludedClassKey: NSObject
@property (retain) NSString *key;
@property (retain) NSString *className;
@property (copy) NSIncludedClassKeyValueTransformer block;
- (instancetype)initWithClassName:(NSString *)className key:(NSString *)key
block:(nonnull NSIncludedClassKeyValueTransformer)block;
@end
@interface NSObject (ObjectMap)
+ (NSArray *)allSubclasses:(NSString *)className;
- (void *)instanceVariableForKey:(NSString *)key;
- (nullable id)safeValueForKey:(NSString *)key;
- (NSDictionary *)propertyDictionaryIgnoringKeys:(NSArray *)ignore including:(NSArray *)include;
@end
NS_ASSUME_NONNULL_END
@implementation NSIgnoredClassKey
- (instancetype)initWithClassName:(NSString *)className key:(NSString *)key {
if ((self = [super init])) {
self.className = className;
self.key = key;
}
return self;
}
- (BOOL)isEqual:(id)object {
if (object == nil || ![object isKindOfClass:self.class])
return NO;
return [self.className isEqual:((NSIgnoredClassKey *)object).className] &&
[self.key isEqual:((NSIgnoredClassKey *)object).key];
}
@end
@implementation NSIncludedClassKey
- (instancetype)initWithClassName:(NSString *)className key:(NSString *)key block:(nonnull NSIncludedClassKeyValueTransformer)block {
if ((self = [super init])) {
self.className = className;
self.key = key;
self.block = block;
}
return self;
}
- (BOOL)isEqual:(id)object {
if (object == nil || ![object isKindOfClass:self.class])
return NO;
return [self.className isEqual:((NSIgnoredClassKey *)object).className] &&
[self.key isEqual:((NSIgnoredClassKey *)object).key];
}
@end
@implementation NSObject (ObjectMap)
+ (NSArray *)allSubclasses:(NSString *)className {
Class parentClass = NSClassFromString(className);
int numClasses = objc_getClassList(NULL, 0);
Class *classes = NULL;
classes = (__unsafe_unretained Class *)malloc(sizeof(Class) * numClasses);
numClasses = objc_getClassList(classes, numClasses);
NSMutableArray *result = [NSMutableArray array];
for (NSInteger i = 0; i < numClasses; i++) {
Class superClass = classes[i];
do {
superClass = class_getSuperclass(superClass);
} while(superClass && superClass != parentClass);
if (superClass == nil) continue;
[result addObject:classes[i]];
}
free(classes);
return result;
}
- (void *)instanceVariableForKey:(NSString *)key {
if (key == nil) return NULL;
Ivar ivar = class_getInstanceVariable([self class], [key UTF8String]);
return (__bridge void *)(object_getIvar(self, ivar));
}
// Return a value for the key given, or nil if undefined.
- (nullable id)safeValueForKey:(NSString *)key {
id value = nil;
@try { value = [self valueForKey:key];
} @catch(NSException *e) {} // ignore
return value;
}
// Return a dictionary formed from the object; careful of cyclic references!
- (NSDictionary *)propertyDictionaryIgnoringKeys:(NSArray *)ignore including:(NSArray *)include {
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
unsigned count;
objc_property_t *properties = class_copyPropertyList([self class], &count);
for (int i = 0; i < count; i++) {
// Get the key and the value for the key if any.
NSString *key = [NSString stringWithUTF8String: property_getName(properties[i])];
id value = [self safeValueForKey:key];
if (value == nil) continue;
// Skip any values in the ignore list (if not a class).
NSString *clz = NSStringFromClass([(NSObject *)value class]);
if (!object_isClass(value) && (ignore != nil) &&
[ignore containsObject:[[NSIgnoredClassKey alloc] initWithClassName:clz key:key]]) {
continue;
}
// If a JSON dictionary, encode keys and objects recursively.
if ([value isKindOfClass:[NSDictionary class]]) {
NSMutableDictionary *inner = [NSMutableDictionary dictionary];
[(NSDictionary *)value enumerateKeysAndObjectsUsingBlock:^(id key2, id value2, BOOL* stop) {
[inner setObject:[value2 propertyDictionaryIgnoringKeys:ignore including:include] forKey: key2];
}];
[dict setObject:[NSDictionary dictionaryWithDictionary:inner] forKey:key];
// If a JSON array, encode values recursively.
} else if ([value isKindOfClass:[NSArray class]]) {
NSMutableArray *inner = [NSMutableArray array];
[(NSArray *)value enumerateObjectsUsingBlock:^(id value2, NSUInteger idx2, BOOL* stop) {
[inner addObject:[value2 propertyDictionaryIgnoringKeys:ignore including:include]];
}];
[dict setObject:[NSArray arrayWithArray:inner] forKey:key];
// If Class type, use its string value.
} else if (object_isClass(value)) {
[dict setObject:NSStringFromClass((Class)value) forKey:key];
// If not a JSON primitive, continue encoding recursively.
} else if (!([value isKindOfClass:[NSString class]] || [value isKindOfClass:[NSNumber class]])) {
[dict setObject:[value propertyDictionaryIgnoringKeys:ignore including:include] forKey:key];
// JSON primitive: stop here.
} else {
[dict setObject:value forKey:key];
}
}
// Generate synthetic properties.
if (include != nil) {
[include enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL * _Nonnull stop) {
if (![obj isKindOfClass:[NSIncludedClassKey class]]) return;
if (![NSStringFromClass([self class]) isEqualToString:[(NSIncludedClassKey *)obj className]]) return;
id output = [(NSIncludedClassKey *)obj block](self);
if (output != nil)
[dict setObject: output forKey: [(NSIncludedClassKey *)obj key]];
}];
}
free(properties);
return [NSDictionary dictionaryWithDictionary:dict];
}
@end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment