// // FLEXObjectExplorer.m // FLEX // // Created by Tanner Bennett on 8/28/19. // Copyright © 2020 FLEX Team. All rights reserved. // #import "FLEXObjectExplorer.h" #import "FLEXUtility.h" #import "FLEXRuntimeUtility.h" #import "NSObject+FLEX_Reflection.h" #import "FLEXRuntime+Compare.h" #import "FLEXRuntime+UIKitHelpers.h" #import "FLEXPropertyAttributes.h" #import "FLEXMetadataSection.h" #import "NSUserDefaults+FLEX.h" @implementation FLEXObjectExplorerDefaults + (instancetype)canEdit:(BOOL)editable wantsPreviews:(BOOL)showPreviews { FLEXObjectExplorerDefaults *defaults = [self new]; defaults->_isEditable = editable; defaults->_wantsDynamicPreviews = showPreviews; return defaults; } @end @interface FLEXObjectExplorer () { NSMutableArray *> *_allProperties; NSMutableArray *> *_allClassProperties; NSMutableArray *> *_allIvars; NSMutableArray *> *_allMethods; NSMutableArray *> *_allClassMethods; NSMutableArray *> *_allConformedProtocols; NSMutableArray *_allInstanceSizes; NSMutableArray *_allImageNames; NSString *_objectDescription; } @end @implementation FLEXObjectExplorer #pragma mark - Initialization + (id)forObject:(id)objectOrClass { return [[self alloc] initWithObject:objectOrClass]; } - (id)initWithObject:(id)objectOrClass { NSParameterAssert(objectOrClass); self = [super init]; if (self) { _object = objectOrClass; _objectIsInstance = !object_isClass(objectOrClass); [self reloadMetadata]; } return self; } #pragma mark - Public + (void)configureDefaultsForItems:(NSArray> *)items { BOOL hidePreviews = NSUserDefaults.standardUserDefaults.flex_explorerHidesVariablePreviews; FLEXObjectExplorerDefaults *mutable = [FLEXObjectExplorerDefaults canEdit:YES wantsPreviews:!hidePreviews ]; FLEXObjectExplorerDefaults *immutable = [FLEXObjectExplorerDefaults canEdit:NO wantsPreviews:!hidePreviews ]; // .tag is used to cache whether the value of .isEditable; // This could change at runtime so it is important that // it is cached every time shortcuts are requeted and not // just once at as shortcuts are initially registered for (id metadata in items) { metadata.defaults = metadata.isEditable ? mutable : immutable; } } - (NSString *)objectDescription { if (!_objectDescription) { // Hard-code UIColor description if ([FLEXRuntimeUtility safeObject:self.object isKindOfClass:[UIColor class]]) { CGFloat h, s, l, r, g, b, a; [self.object getRed:&r green:&g blue:&b alpha:&a]; [self.object getHue:&h saturation:&s brightness:&l alpha:nil]; return [NSString stringWithFormat: @"HSL: (%.3f, %.3f, %.3f)\nRGB: (%.3f, %.3f, %.3f)\nAlpha: %.3f", h, s, l, r, g, b, a ]; } NSString *description = [FLEXRuntimeUtility safeDescriptionForObject:self.object]; if (!description.length) { NSString *address = [FLEXUtility addressOfObject:self.object]; return [NSString stringWithFormat:@"Object at %@ returned empty description", address]; } if (description.length > 10000) { description = [description substringToIndex:10000]; } _objectDescription = description; } return _objectDescription; } - (void)setClassScope:(NSInteger)classScope { _classScope = classScope; [self reloadScopedMetadata]; } - (void)reloadMetadata { _allProperties = [NSMutableArray new]; _allClassProperties = [NSMutableArray new]; _allIvars = [NSMutableArray new]; _allMethods = [NSMutableArray new]; _allClassMethods = [NSMutableArray new]; _allConformedProtocols = [NSMutableArray new]; _allInstanceSizes = [NSMutableArray new]; _allImageNames = [NSMutableArray new]; _objectDescription = nil; [self reloadClassHierarchy]; NSUserDefaults *defaults = NSUserDefaults.standardUserDefaults; BOOL hideBackingIvars = defaults.flex_explorerHidesPropertyIvars; BOOL hidePropertyMethods = defaults.flex_explorerHidesPropertyMethods; BOOL showMethodOverrides = defaults.flex_explorerShowsMethodOverrides; // Loop over each class and each superclass, collect // the fresh and unique metadata in each category Class superclass = nil; NSInteger count = self.classHierarchyClasses.count; NSInteger rootIdx = count - 1; for (NSInteger i = 0; i < count; i++) { Class cls = self.classHierarchyClasses[i]; superclass = (i < rootIdx) ? self.classHierarchyClasses[i+1] : nil; [_allProperties addObject:[self metadataUniquedByName:[cls flex_allInstanceProperties] superclass:superclass kind:FLEXMetadataKindProperties skip:showMethodOverrides ]]; [_allClassProperties addObject:[self metadataUniquedByName:[cls flex_allClassProperties] superclass:superclass kind:FLEXMetadataKindClassProperties skip:showMethodOverrides ]]; [_allIvars addObject:[self metadataUniquedByName:[cls flex_allIvars] superclass:nil kind:FLEXMetadataKindIvars skip:NO ]]; [_allMethods addObject:[self metadataUniquedByName:[cls flex_allInstanceMethods] superclass:superclass kind:FLEXMetadataKindMethods skip:showMethodOverrides ]]; [_allClassMethods addObject:[self metadataUniquedByName:[cls flex_allClassMethods] superclass:superclass kind:FLEXMetadataKindClassMethods skip:showMethodOverrides ]]; [_allConformedProtocols addObject:[self metadataUniquedByName:[cls flex_protocols] superclass:superclass kind:FLEXMetadataKindProtocols skip:NO ]]; // TODO: join instance size, image name, and class hierarchy into a single model object // This would greatly reduce the laziness that has begun to manifest itself here [_allInstanceSizes addObject:[FLEXStaticMetadata style:FLEXStaticMetadataRowStyleKeyValue title:@"Instance Size" number:@(class_getInstanceSize(cls)) ]]; [_allImageNames addObject:[FLEXStaticMetadata style:FLEXStaticMetadataRowStyleDefault title:@"Image Name" string:@(class_getImageName(cls) ?: "Created at Runtime") ]]; } _classHierarchy = [FLEXStaticMetadata classHierarchy:self.classHierarchyClasses]; NSArray *> *properties = _allProperties; // Potentially filter property-backing ivars if (hideBackingIvars) { NSArray *> *ivars = _allIvars.copy; _allIvars = [ivars flex_mapped:^id(NSArray *list, NSUInteger idx) { // Get a set of all backing ivar names for the current class in the hierarchy NSSet *ivarNames = [NSSet setWithArray:({ [properties[idx] flex_mapped:^id(FLEXProperty *p, NSUInteger idx) { // Nil if no ivar, and array is flatted return p.attributes.backingIvar; }]; })]; // Remove ivars whose name is in the ivar names list return [list flex_filtered:^BOOL(FLEXIvar *ivar, NSUInteger idx) { return ![ivarNames containsObject:ivar.name]; }]; }]; } // Potentially filter property-backing methods if (hidePropertyMethods) { NSArray *> *methods = _allMethods.copy; _allMethods = [methods flex_mapped:^id(NSArray *list, NSUInteger idx) { // Get a set of all property method names for the current class in the hierarchy NSSet *methodNames = [NSSet setWithArray:({ [properties[idx] flex_flatmapped:^NSArray *(FLEXProperty *p, NSUInteger idx) { if (p.likelyGetterExists) { if (p.likelySetterExists) { return @[p.likelyGetterString, p.likelySetterString]; } return @[p.likelyGetterString]; } else if (p.likelySetterExists) { return @[p.likelySetterString]; } return nil; }]; })]; // Remove ivars whose name is in the ivar names list return [list flex_filtered:^BOOL(FLEXMethod *method, NSUInteger idx) { return ![methodNames containsObject:method.selectorString]; }]; }]; } // Set up UIKit helper data // Really, we only need to call this on properties and ivars // because no other metadata types support editing. NSArray*metadatas = @[ _allProperties, _allClassProperties, _allIvars, /* _allMethods, _allClassMethods, _allConformedProtocols */ ]; for (NSArray *matrix in metadatas) { for (NSArray *metadataByClass in matrix) { [FLEXObjectExplorer configureDefaultsForItems:metadataByClass]; } } [self reloadScopedMetadata]; } #pragma mark - Private - (void)reloadScopedMetadata { _properties = self.allProperties[self.classScope]; _classProperties = self.allClassProperties[self.classScope]; _ivars = self.allIvars[self.classScope]; _methods = self.allMethods[self.classScope]; _classMethods = self.allClassMethods[self.classScope]; _conformedProtocols = self.allConformedProtocols[self.classScope]; _instanceSize = self.allInstanceSizes[self.classScope]; _imageName = self.allImageNames[self.classScope]; } /// Accepts an array of flex metadata objects and discards objects /// with duplicate names, as well as properties and methods which /// aren't "new" (i.e. those which the superclass responds to) - (NSArray *)metadataUniquedByName:(NSArray *)list superclass:(Class)superclass kind:(FLEXMetadataKind)kind skip:(BOOL)skip { if (skip) { return list; } // Remove items with same name and return filtered list NSMutableSet *names = [NSMutableSet new]; return [list flex_filtered:^BOOL(id obj, NSUInteger idx) { NSString *name = [obj name]; if ([names containsObject:name]) { return NO; } else { [names addObject:name]; // Skip methods and properties which are just overrides, // potentially skip ivars and methods associated with properties switch (kind) { case FLEXMetadataKindProperties: if ([superclass instancesRespondToSelector:[obj likelyGetter]]) { return NO; } break; case FLEXMetadataKindClassProperties: if ([superclass respondsToSelector:[obj likelyGetter]]) { return NO; } break; case FLEXMetadataKindMethods: if ([superclass instancesRespondToSelector:NSSelectorFromString(name)]) { return NO; } break; case FLEXMetadataKindClassMethods: if ([superclass respondsToSelector:NSSelectorFromString(name)]) { return NO; } break; case FLEXMetadataKindProtocols: case FLEXMetadataKindClassHierarchy: case FLEXMetadataKindOther: return YES; // These types are already uniqued break; // Ivars cannot be overidden case FLEXMetadataKindIvars: break; } return YES; } }]; } #pragma mark - Superclasses - (void)reloadClassHierarchy { // The class hierarchy will never contain metaclass objects by this logic; // it is always the same for a given class and instances of it _classHierarchyClasses = [[self.object class] flex_classHierarchy]; } @end