mirror of
				https://github.com/SoPat712/YTLitePlus.git
				synced 2025-10-30 20:34:03 -04:00 
			
		
		
		
	added files via upload
This commit is contained in:
		
							
								
								
									
										73
									
								
								Tweaks/FLEX/ObjectExplorers/FLEXObjectExplorer.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								Tweaks/FLEX/ObjectExplorers/FLEXObjectExplorer.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,73 @@ | ||||
| // | ||||
| //  FLEXObjectExplorer.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 8/28/19. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXRuntime+UIKitHelpers.h" | ||||
|  | ||||
| /// Carries state about the current user defaults settings | ||||
| @interface FLEXObjectExplorerDefaults : NSObject | ||||
| + (instancetype)canEdit:(BOOL)editable wantsPreviews:(BOOL)showPreviews; | ||||
|  | ||||
| /// Only \c YES for properties and ivars | ||||
| @property (nonatomic, readonly) BOOL isEditable; | ||||
| /// Only affects properties and ivars | ||||
| @property (nonatomic, readonly) BOOL wantsDynamicPreviews; | ||||
| @end | ||||
|  | ||||
| @interface FLEXObjectExplorer : NSObject | ||||
|  | ||||
| + (instancetype)forObject:(id)objectOrClass; | ||||
|  | ||||
| + (void)configureDefaultsForItems:(NSArray<id<FLEXObjectExplorerItem>> *)items; | ||||
|  | ||||
| @property (nonatomic, readonly) id object; | ||||
| /// Subclasses can override to provide a more useful description | ||||
| @property (nonatomic, readonly) NSString *objectDescription; | ||||
|  | ||||
| /// @return \c YES if \c object is an instance of a class, | ||||
| /// or \c NO if \c object is a class itself. | ||||
| @property (nonatomic, readonly) BOOL objectIsInstance; | ||||
|  | ||||
| /// An index into the `classHierarchy` array. | ||||
| /// | ||||
| /// This property determines which set of data comes out of the metadata arrays below | ||||
| /// For example, \c properties contains the properties of the selected class scope, | ||||
| /// while \c allProperties is an array of arrays where each array is a set of | ||||
| /// properties for a class in the class hierarchy of the current object. | ||||
| @property (nonatomic) NSInteger classScope; | ||||
|  | ||||
| @property (nonatomic, readonly) NSArray<NSArray<FLEXProperty *> *> *allProperties; | ||||
| @property (nonatomic, readonly) NSArray<FLEXProperty *> *properties; | ||||
|  | ||||
| @property (nonatomic, readonly) NSArray<NSArray<FLEXProperty *> *> *allClassProperties; | ||||
| @property (nonatomic, readonly) NSArray<FLEXProperty *> *classProperties; | ||||
|  | ||||
| @property (nonatomic, readonly) NSArray<NSArray<FLEXIvar *> *> *allIvars; | ||||
| @property (nonatomic, readonly) NSArray<FLEXIvar *> *ivars; | ||||
|  | ||||
| @property (nonatomic, readonly) NSArray<NSArray<FLEXMethod *> *> *allMethods; | ||||
| @property (nonatomic, readonly) NSArray<FLEXMethod *> *methods; | ||||
|  | ||||
| @property (nonatomic, readonly) NSArray<NSArray<FLEXMethod *> *> *allClassMethods; | ||||
| @property (nonatomic, readonly) NSArray<FLEXMethod *> *classMethods; | ||||
|  | ||||
| @property (nonatomic, readonly) NSArray<Class> *classHierarchyClasses; | ||||
| @property (nonatomic, readonly) NSArray<FLEXStaticMetadata *> *classHierarchy; | ||||
|  | ||||
| @property (nonatomic, readonly) NSArray<NSArray<FLEXProtocol *> *> *allConformedProtocols; | ||||
| @property (nonatomic, readonly) NSArray<FLEXProtocol *> *conformedProtocols; | ||||
|  | ||||
| @property (nonatomic, readonly) NSArray<FLEXStaticMetadata *> *allInstanceSizes; | ||||
| @property (nonatomic, readonly) FLEXStaticMetadata *instanceSize; | ||||
|  | ||||
| @property (nonatomic, readonly) NSArray<FLEXStaticMetadata *> *allImageNames; | ||||
| @property (nonatomic, readonly) FLEXStaticMetadata *imageName; | ||||
|  | ||||
| - (void)reloadMetadata; | ||||
| - (void)reloadClassHierarchy; | ||||
|  | ||||
| @end | ||||
							
								
								
									
										378
									
								
								Tweaks/FLEX/ObjectExplorers/FLEXObjectExplorer.m
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										378
									
								
								Tweaks/FLEX/ObjectExplorers/FLEXObjectExplorer.m
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,378 @@ | ||||
| // | ||||
| //  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<NSArray<FLEXProperty *> *> *_allProperties; | ||||
|     NSMutableArray<NSArray<FLEXProperty *> *> *_allClassProperties; | ||||
|     NSMutableArray<NSArray<FLEXIvar *> *> *_allIvars; | ||||
|     NSMutableArray<NSArray<FLEXMethod *> *> *_allMethods; | ||||
|     NSMutableArray<NSArray<FLEXMethod *> *> *_allClassMethods; | ||||
|     NSMutableArray<NSArray<FLEXProtocol *> *> *_allConformedProtocols; | ||||
|     NSMutableArray<FLEXStaticMetadata *> *_allInstanceSizes; | ||||
|     NSMutableArray<FLEXStaticMetadata *> *_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<id<FLEXObjectExplorerItem>> *)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<FLEXObjectExplorerItem> 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 hidePrivateMethods = defaults.flex_explorerHidesPrivateMethods; | ||||
|     BOOL showMethodOverrides = defaults.flex_explorerShowsMethodOverrides; | ||||
|      | ||||
|     NSMutableArray<NSArray<FLEXProperty *> *> *allProperties = [NSMutableArray new]; | ||||
|     NSMutableArray<NSArray<FLEXProperty *> *> *allClassProps = [NSMutableArray new]; | ||||
|     NSMutableArray<NSArray<FLEXMethod *> *> *allMethods = [NSMutableArray new]; | ||||
|     NSMutableArray<NSArray<FLEXMethod *> *> *allClassMethods = [NSMutableArray new]; | ||||
|  | ||||
|     // 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 | ||||
|         ]]; | ||||
|         [allClassProps 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<NSArray<FLEXProperty *> *> *properties = allProperties; | ||||
|      | ||||
|     // Potentially filter property-backing ivars | ||||
|     if (hideBackingIvars) { | ||||
|         NSArray<NSArray<FLEXIvar *> *> *ivars = _allIvars.copy; | ||||
|         _allIvars = [ivars flex_mapped:^id(NSArray<FLEXIvar *> *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.likelyIvarName; | ||||
|                 }]; | ||||
|             })]; | ||||
|              | ||||
|             // 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) { | ||||
|         allMethods = [allMethods flex_mapped:^id(NSArray<FLEXMethod *> *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 methods whose name is in the property method names list | ||||
|             return [list flex_filtered:^BOOL(FLEXMethod *method, NSUInteger idx) { | ||||
|                 return ![methodNames containsObject:method.selectorString]; | ||||
|             }]; | ||||
|         }]; | ||||
|     } | ||||
|      | ||||
|     if (hidePrivateMethods) { | ||||
|         id methodMapBlock = ^id(NSArray<FLEXMethod *> *list, NSUInteger idx) { | ||||
|             // Remove methods which contain an underscore | ||||
|             return [list flex_filtered:^BOOL(FLEXMethod *method, NSUInteger idx) { | ||||
|                 return ![method.selectorString containsString:@"_"]; | ||||
|             }]; | ||||
|         }; | ||||
|         id propertyMapBlock = ^id(NSArray<FLEXProperty *> *list, NSUInteger idx) { | ||||
|             // Remove methods which contain an underscore | ||||
|             return [list flex_filtered:^BOOL(FLEXProperty *prop, NSUInteger idx) { | ||||
|                 return ![prop.name containsString:@"_"]; | ||||
|             }]; | ||||
|         }; | ||||
|          | ||||
|         allMethods = [allMethods flex_mapped:methodMapBlock]; | ||||
|         allClassMethods = [allClassMethods flex_mapped:methodMapBlock]; | ||||
|         allProperties = [allProperties flex_mapped:propertyMapBlock]; | ||||
|         allClassProps = [allClassProps flex_mapped:propertyMapBlock]; | ||||
|     } | ||||
|      | ||||
|     _allProperties = allProperties; | ||||
|     _allClassProperties = allClassProps; | ||||
|     _allMethods = allMethods; | ||||
|     _allClassMethods = allClassMethods; | ||||
|  | ||||
|     // Set up UIKit helper data | ||||
|     // Really, we only need to call this on properties and ivars | ||||
|     // because no other metadata types support editing. | ||||
|     NSArray<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)skipUniquing { | ||||
|     if (skipUniquing) { | ||||
|         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 | ||||
							
								
								
									
										30
									
								
								Tweaks/FLEX/ObjectExplorers/FLEXObjectExplorerFactory.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								Tweaks/FLEX/ObjectExplorers/FLEXObjectExplorerFactory.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | ||||
| // | ||||
| //  FLEXObjectExplorerFactory.h | ||||
| //  Flipboard | ||||
| // | ||||
| //  Created by Ryan Olson on 5/15/14. | ||||
| //  Copyright (c) 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXGlobalsEntry.h" | ||||
|  | ||||
| #ifndef _FLEXObjectExplorerViewController_h | ||||
| #import "FLEXObjectExplorerViewController.h" | ||||
| #else | ||||
| @class FLEXObjectExplorerViewController; | ||||
| #endif | ||||
|  | ||||
| NS_ASSUME_NONNULL_BEGIN | ||||
|  | ||||
| @interface FLEXObjectExplorerFactory : NSObject <FLEXGlobalsEntry> | ||||
|  | ||||
| + (nullable FLEXObjectExplorerViewController *)explorerViewControllerForObject:(nullable id)object; | ||||
|  | ||||
| /// Register a specific explorer view controller class to be used when exploring | ||||
| /// an object of a specific class. Calls will overwrite existing registrations. | ||||
| /// Sections must be initialized using \c forObject: like | ||||
| + (void)registerExplorerSection:(Class)sectionClass forClass:(Class)objectClass; | ||||
|  | ||||
| @end | ||||
|  | ||||
| NS_ASSUME_NONNULL_END | ||||
							
								
								
									
										243
									
								
								Tweaks/FLEX/ObjectExplorers/FLEXObjectExplorerFactory.m
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										243
									
								
								Tweaks/FLEX/ObjectExplorers/FLEXObjectExplorerFactory.m
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,243 @@ | ||||
| // | ||||
| //  FLEXObjectExplorerFactory.m | ||||
| //  Flipboard | ||||
| // | ||||
| //  Created by Ryan Olson on 5/15/14. | ||||
| //  Copyright (c) 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXObjectExplorerFactory.h" | ||||
| #import "FLEXGlobalsViewController.h" | ||||
| #import "FLEXClassShortcuts.h" | ||||
| #import "FLEXViewShortcuts.h" | ||||
| #import "FLEXViewControllerShortcuts.h" | ||||
| #import "FLEXUIAppShortcuts.h" | ||||
| #import "FLEXImageShortcuts.h" | ||||
| #import "FLEXLayerShortcuts.h" | ||||
| #import "FLEXColorPreviewSection.h" | ||||
| #import "FLEXDefaultsContentSection.h" | ||||
| #import "FLEXBundleShortcuts.h" | ||||
| #import "FLEXNSStringShortcuts.h" | ||||
| #import "FLEXNSDataShortcuts.h" | ||||
| #import "FLEXBlockShortcuts.h" | ||||
| #import "FLEXUtility.h" | ||||
|  | ||||
| @implementation FLEXObjectExplorerFactory | ||||
| static NSMutableDictionary<id<NSCopying>, Class> *classesToRegisteredSections = nil; | ||||
|  | ||||
| + (void)initialize { | ||||
|     if (self == [FLEXObjectExplorerFactory class]) { | ||||
|         // DO NOT USE STRING KEYS HERE | ||||
|         // We NEED to use the class as a key, because we CANNOT | ||||
|         // differentiate a class's name from the metaclass's name. | ||||
|         // These mappings are per-class-object, not per-class-name. | ||||
|         // | ||||
|         // For example, if we used class names, this would result in | ||||
|         // the object explorer trying to render a color preview for | ||||
|         // the UIColor class object, which is not a color itself. | ||||
|         #define ClassKey(name) (id<NSCopying>)[name class] | ||||
|         #define ClassKeyByName(str) (id<NSCopying>)NSClassFromString(@ #str) | ||||
|         #define MetaclassKey(meta) (id<NSCopying>)object_getClass([meta class]) | ||||
|         classesToRegisteredSections = [NSMutableDictionary dictionaryWithDictionary:@{ | ||||
|             MetaclassKey(NSObject)     : [FLEXClassShortcuts class], | ||||
|             ClassKey(NSArray)          : [FLEXCollectionContentSection class], | ||||
|             ClassKey(NSSet)            : [FLEXCollectionContentSection class], | ||||
|             ClassKey(NSDictionary)     : [FLEXCollectionContentSection class], | ||||
|             ClassKey(NSOrderedSet)     : [FLEXCollectionContentSection class], | ||||
|             ClassKey(NSUserDefaults)   : [FLEXDefaultsContentSection class], | ||||
|             ClassKey(UIViewController) : [FLEXViewControllerShortcuts class], | ||||
|             ClassKey(UIApplication)    : [FLEXUIAppShortcuts class], | ||||
|             ClassKey(UIView)           : [FLEXViewShortcuts class], | ||||
|             ClassKey(UIImage)          : [FLEXImageShortcuts class], | ||||
|             ClassKey(CALayer)          : [FLEXLayerShortcuts class], | ||||
|             ClassKey(UIColor)          : [FLEXColorPreviewSection class], | ||||
|             ClassKey(NSBundle)         : [FLEXBundleShortcuts class], | ||||
|             ClassKey(NSString)         : [FLEXNSStringShortcuts class], | ||||
|             ClassKey(NSData)           : [FLEXNSDataShortcuts class], | ||||
|             ClassKeyByName(NSBlock)    : [FLEXBlockShortcuts class], | ||||
|         }]; | ||||
|         #undef ClassKey | ||||
|         #undef ClassKeyByName | ||||
|         #undef MetaclassKey | ||||
|     } | ||||
| } | ||||
|  | ||||
| + (FLEXObjectExplorerViewController *)explorerViewControllerForObject:(id)object { | ||||
|     // Can't explore nil | ||||
|     if (!object) { | ||||
|         return nil; | ||||
|     } | ||||
|  | ||||
|     // If we're given an object, this will look up it's class hierarchy | ||||
|     // until it finds a registration. This will work for KVC classes, | ||||
|     // since they are children of the original class, and not siblings. | ||||
|     // If we are given an object, object_getClass will return a metaclass, | ||||
|     // and the same thing will happen. FLEXClassShortcuts is the default | ||||
|     // shortcut section for NSObject. | ||||
|     // | ||||
|     // TODO: rename it to FLEXNSObjectShortcuts or something? | ||||
|     FLEXShortcutsSection *shortcutsSection = [FLEXShortcutsSection forObject:object]; | ||||
|     NSArray *sections = @[shortcutsSection]; | ||||
|      | ||||
|     Class customSectionClass = nil; | ||||
|     Class cls = object_getClass(object); | ||||
|     do { | ||||
|         customSectionClass = classesToRegisteredSections[(id<NSCopying>)cls]; | ||||
|     } while (!customSectionClass && (cls = [cls superclass])); | ||||
|  | ||||
|     if (customSectionClass) { | ||||
|         id customSection = [customSectionClass forObject:object]; | ||||
|         BOOL isFLEXShortcutSection = [customSection respondsToSelector:@selector(isNewSection)]; | ||||
|          | ||||
|         // If the section "replaces" the default shortcuts section, | ||||
|         // only return that section. Otherwise, return both this | ||||
|         // section and the default shortcuts section. | ||||
|         if (isFLEXShortcutSection && ![customSection isNewSection]) { | ||||
|             sections = @[customSection]; | ||||
|         } else { | ||||
|             // Custom section will go before shortcuts | ||||
|             sections = @[customSection, shortcutsSection];             | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     return [FLEXObjectExplorerViewController | ||||
|         exploringObject:object | ||||
|         customSections:sections | ||||
|     ]; | ||||
| } | ||||
|  | ||||
| + (void)registerExplorerSection:(Class)explorerClass forClass:(Class)objectClass { | ||||
|     classesToRegisteredSections[(id<NSCopying>)objectClass] = explorerClass; | ||||
| } | ||||
|  | ||||
| #pragma mark - FLEXGlobalsEntry | ||||
|  | ||||
| + (NSString *)globalsEntryTitle:(FLEXGlobalsRow)row  { | ||||
|     switch (row) { | ||||
|         case FLEXGlobalsRowAppDelegate: | ||||
|             return @"🎟  App Delegate"; | ||||
|         case FLEXGlobalsRowKeyWindow: | ||||
|             return @"🔑  Key Window"; | ||||
|         case FLEXGlobalsRowRootViewController: | ||||
|             return @"🌴  Root View Controller"; | ||||
|         case FLEXGlobalsRowProcessInfo: | ||||
|             return @"🚦  NSProcessInfo.processInfo"; | ||||
|         case FLEXGlobalsRowUserDefaults: | ||||
|             return @"💾  Preferences"; | ||||
|         case FLEXGlobalsRowMainBundle: | ||||
|             return @"📦  NSBundle.mainBundle"; | ||||
|         case FLEXGlobalsRowApplication: | ||||
|             return @"🚀  UIApplication.sharedApplication"; | ||||
|         case FLEXGlobalsRowMainScreen: | ||||
|             return @"💻  UIScreen.mainScreen"; | ||||
|         case FLEXGlobalsRowCurrentDevice: | ||||
|             return @"📱  UIDevice.currentDevice"; | ||||
|         case FLEXGlobalsRowPasteboard: | ||||
|             return @"📋  UIPasteboard.generalPasteboard"; | ||||
|         case FLEXGlobalsRowURLSession: | ||||
|             return @"📡  NSURLSession.sharedSession"; | ||||
|         case FLEXGlobalsRowURLCache: | ||||
|             return @"⏳  NSURLCache.sharedURLCache"; | ||||
|         case FLEXGlobalsRowNotificationCenter: | ||||
|             return @"🔔  NSNotificationCenter.defaultCenter"; | ||||
|         case FLEXGlobalsRowMenuController: | ||||
|             return @"📎  UIMenuController.sharedMenuController"; | ||||
|         case FLEXGlobalsRowFileManager: | ||||
|             return @"🗄  NSFileManager.defaultManager"; | ||||
|         case FLEXGlobalsRowTimeZone: | ||||
|             return @"🌎  NSTimeZone.systemTimeZone"; | ||||
|         case FLEXGlobalsRowLocale: | ||||
|             return @"🗣  NSLocale.currentLocale"; | ||||
|         case FLEXGlobalsRowCalendar: | ||||
|             return @"📅  NSCalendar.currentCalendar"; | ||||
|         case FLEXGlobalsRowMainRunLoop: | ||||
|             return @"🏃🏻♂️  NSRunLoop.mainRunLoop"; | ||||
|         case FLEXGlobalsRowMainThread: | ||||
|             return @"🧵  NSThread.mainThread"; | ||||
|         case FLEXGlobalsRowOperationQueue: | ||||
|             return @"📚  NSOperationQueue.mainQueue"; | ||||
|         default: return nil; | ||||
|     } | ||||
| } | ||||
|  | ||||
| + (UIViewController *)globalsEntryViewController:(FLEXGlobalsRow)row  { | ||||
|     switch (row) { | ||||
|         case FLEXGlobalsRowAppDelegate: { | ||||
|             id<UIApplicationDelegate> appDelegate = UIApplication.sharedApplication.delegate; | ||||
|             return [self explorerViewControllerForObject:appDelegate]; | ||||
|         } | ||||
|         case FLEXGlobalsRowProcessInfo: | ||||
|             return [self explorerViewControllerForObject:NSProcessInfo.processInfo]; | ||||
|         case FLEXGlobalsRowUserDefaults: | ||||
|             return [self explorerViewControllerForObject:NSUserDefaults.standardUserDefaults]; | ||||
|         case FLEXGlobalsRowMainBundle: | ||||
|             return [self explorerViewControllerForObject:NSBundle.mainBundle]; | ||||
|         case FLEXGlobalsRowApplication: | ||||
|             return [self explorerViewControllerForObject:UIApplication.sharedApplication]; | ||||
|         case FLEXGlobalsRowMainScreen: | ||||
|             return [self explorerViewControllerForObject:UIScreen.mainScreen]; | ||||
|         case FLEXGlobalsRowCurrentDevice: | ||||
|             return [self explorerViewControllerForObject:UIDevice.currentDevice]; | ||||
|         case FLEXGlobalsRowPasteboard: | ||||
|             return [self explorerViewControllerForObject:UIPasteboard.generalPasteboard]; | ||||
|             case FLEXGlobalsRowURLSession: | ||||
|             return [self explorerViewControllerForObject:NSURLSession.sharedSession]; | ||||
|         case FLEXGlobalsRowURLCache: | ||||
|             return [self explorerViewControllerForObject:NSURLCache.sharedURLCache]; | ||||
|         case FLEXGlobalsRowNotificationCenter: | ||||
|             return [self explorerViewControllerForObject:NSNotificationCenter.defaultCenter]; | ||||
|         case FLEXGlobalsRowMenuController: | ||||
|             return [self explorerViewControllerForObject:UIMenuController.sharedMenuController]; | ||||
|         case FLEXGlobalsRowFileManager: | ||||
|             return [self explorerViewControllerForObject:NSFileManager.defaultManager]; | ||||
|         case FLEXGlobalsRowTimeZone: | ||||
|             return [self explorerViewControllerForObject:NSTimeZone.systemTimeZone]; | ||||
|         case FLEXGlobalsRowLocale: | ||||
|             return [self explorerViewControllerForObject:NSLocale.currentLocale]; | ||||
|         case FLEXGlobalsRowCalendar: | ||||
|             return [self explorerViewControllerForObject:NSCalendar.currentCalendar]; | ||||
|         case FLEXGlobalsRowMainRunLoop: | ||||
|             return [self explorerViewControllerForObject:NSRunLoop.mainRunLoop]; | ||||
|         case FLEXGlobalsRowMainThread: | ||||
|             return [self explorerViewControllerForObject:NSThread.mainThread]; | ||||
|         case FLEXGlobalsRowOperationQueue: | ||||
|             return [self explorerViewControllerForObject:NSOperationQueue.mainQueue]; | ||||
|  | ||||
|         case FLEXGlobalsRowKeyWindow: | ||||
|             return [FLEXObjectExplorerFactory | ||||
|                 explorerViewControllerForObject:FLEXUtility.appKeyWindow | ||||
|             ]; | ||||
|         case FLEXGlobalsRowRootViewController: { | ||||
|             id<UIApplicationDelegate> delegate = UIApplication.sharedApplication.delegate; | ||||
|             if ([delegate respondsToSelector:@selector(window)]) { | ||||
|                 return [self explorerViewControllerForObject:delegate.window.rootViewController]; | ||||
|             } | ||||
|  | ||||
|             return nil; | ||||
|         } | ||||
|         default: return nil; | ||||
|     } | ||||
| } | ||||
|  | ||||
| + (FLEXGlobalsEntryRowAction)globalsEntryRowAction:(FLEXGlobalsRow)row { | ||||
|     switch (row) { | ||||
|         case FLEXGlobalsRowRootViewController: { | ||||
|             // Check if the app delegate responds to -window. If not, present an alert | ||||
|             return ^(UITableViewController *host) { | ||||
|                 id<UIApplicationDelegate> delegate = UIApplication.sharedApplication.delegate; | ||||
|                 if ([delegate respondsToSelector:@selector(window)]) { | ||||
|                     UIViewController *explorer = [self explorerViewControllerForObject: | ||||
|                         delegate.window.rootViewController | ||||
|                     ]; | ||||
|                     [host.navigationController pushViewController:explorer animated:YES]; | ||||
|                 } else { | ||||
|                     NSString *msg = @"The app delegate doesn't respond to -window"; | ||||
|                     [FLEXAlert showAlert:@":(" message:msg from:host]; | ||||
|                 } | ||||
|             }; | ||||
|         } | ||||
|         default: return nil; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,57 @@ | ||||
| // | ||||
| //  FLEXObjectExplorerViewController.h | ||||
| //  Flipboard | ||||
| // | ||||
| //  Created by Ryan Olson on 2014-05-03. | ||||
| //  Copyright (c) 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #ifndef _FLEXObjectExplorerViewController_h | ||||
| #define _FLEXObjectExplorerViewController_h | ||||
| #endif | ||||
|  | ||||
| #import "FLEXFilteringTableViewController.h" | ||||
| #import "FLEXObjectExplorer.h" | ||||
| @class FLEXTableViewSection; | ||||
|  | ||||
| NS_ASSUME_NONNULL_BEGIN | ||||
|  | ||||
| /// A class that displays information about an object or class. | ||||
| /// | ||||
| /// The explorer view controller uses \c FLEXObjectExplorer to provide a description | ||||
| /// of the object and list it's properties, ivars, methods, and it's superclasses. | ||||
| /// Below the description and before properties, some shortcuts will be displayed | ||||
| /// for certain classes like UIViews. At very bottom, there is an option to view | ||||
| /// a list of other objects found to be referencing the object being explored. | ||||
| @interface FLEXObjectExplorerViewController : FLEXFilteringTableViewController | ||||
|  | ||||
| /// Uses the default \c FLEXShortcutsSection for this object as a custom section. | ||||
| + (instancetype)exploringObject:(id)objectOrClass; | ||||
| /// No custom section unless you provide one. | ||||
| + (instancetype)exploringObject:(id)objectOrClass customSection:(nullable FLEXTableViewSection *)customSection; | ||||
| /// No custom sections unless you provide some. | ||||
| + (instancetype)exploringObject:(id)objectOrClass | ||||
|                  customSections:(nullable NSArray<FLEXTableViewSection *> *)customSections; | ||||
|  | ||||
| /// The object being explored, which may be an instance of a class or a class itself. | ||||
| @property (nonatomic, readonly) id object; | ||||
| /// This object provides the object's metadata for the explorer view controller. | ||||
| @property (nonatomic, readonly) FLEXObjectExplorer *explorer; | ||||
|  | ||||
| /// Called once to initialize the list of section objects. | ||||
| /// | ||||
| /// Subclasses can override this to add, remove, or rearrange sections of the explorer. | ||||
| - (NSArray<FLEXTableViewSection *> *)makeSections; | ||||
|  | ||||
| /// Whether to allow showing/drilling in to current values for ivars and properties. Default is YES. | ||||
| @property (nonatomic, readonly) BOOL canHaveInstanceState; | ||||
|  | ||||
| /// Whether to allow drilling in to method calling interfaces for instance methods. Default is YES. | ||||
| @property (nonatomic, readonly) BOOL canCallInstanceMethods; | ||||
|  | ||||
| /// If the custom section data makes the description redundant, subclasses can choose to hide it. Default is YES. | ||||
| @property (nonatomic, readonly) BOOL shouldShowDescription; | ||||
|  | ||||
| @end | ||||
|  | ||||
| NS_ASSUME_NONNULL_END | ||||
							
								
								
									
										393
									
								
								Tweaks/FLEX/ObjectExplorers/FLEXObjectExplorerViewController.m
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										393
									
								
								Tweaks/FLEX/ObjectExplorers/FLEXObjectExplorerViewController.m
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,393 @@ | ||||
| // | ||||
| //  FLEXObjectExplorerViewController.m | ||||
| //  Flipboard | ||||
| // | ||||
| //  Created by Ryan Olson on 2014-05-03. | ||||
| //  Copyright (c) 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXObjectExplorerViewController.h" | ||||
| #import "FLEXUtility.h" | ||||
| #import "FLEXRuntimeUtility.h" | ||||
| #import "UIBarButtonItem+FLEX.h" | ||||
| #import "FLEXMultilineTableViewCell.h" | ||||
| #import "FLEXObjectExplorerFactory.h" | ||||
| #import "FLEXFieldEditorViewController.h" | ||||
| #import "FLEXMethodCallingViewController.h" | ||||
| #import "FLEXObjectListViewController.h" | ||||
| #import "FLEXTabsViewController.h" | ||||
| #import "FLEXBookmarkManager.h" | ||||
| #import "FLEXTableView.h" | ||||
| #import "FLEXResources.h" | ||||
| #import "FLEXTableViewCell.h" | ||||
| #import "FLEXScopeCarousel.h" | ||||
| #import "FLEXMetadataSection.h" | ||||
| #import "FLEXSingleRowSection.h" | ||||
| #import "FLEXShortcutsSection.h" | ||||
| #import "NSUserDefaults+FLEX.h" | ||||
| #import <objc/runtime.h> | ||||
|  | ||||
| #pragma mark - Private properties | ||||
| @interface FLEXObjectExplorerViewController () <UIGestureRecognizerDelegate> | ||||
| @property (nonatomic, readonly) FLEXSingleRowSection *descriptionSection; | ||||
| @property (nonatomic, readonly) NSArray<FLEXTableViewSection *> *customSections; | ||||
| @property (nonatomic) NSIndexSet *customSectionVisibleIndexes; | ||||
|  | ||||
| @property (nonatomic, readonly) NSArray<NSString *> *observedNotifications; | ||||
|  | ||||
| @end | ||||
|  | ||||
| @implementation FLEXObjectExplorerViewController | ||||
|  | ||||
| #pragma mark - Initialization | ||||
|  | ||||
| + (instancetype)exploringObject:(id)target { | ||||
|     return [self exploringObject:target customSection:[FLEXShortcutsSection forObject:target]]; | ||||
| } | ||||
|  | ||||
| + (instancetype)exploringObject:(id)target customSection:(FLEXTableViewSection *)section { | ||||
|     return [self exploringObject:target customSections:@[section]]; | ||||
| } | ||||
|  | ||||
| + (instancetype)exploringObject:(id)target customSections:(NSArray *)customSections { | ||||
|     return [[self alloc] | ||||
|         initWithObject:target | ||||
|         explorer:[FLEXObjectExplorer forObject:target] | ||||
|         customSections:customSections | ||||
|     ]; | ||||
| } | ||||
|  | ||||
| - (id)initWithObject:(id)target | ||||
|             explorer:(__kindof FLEXObjectExplorer *)explorer | ||||
|        customSections:(NSArray<FLEXTableViewSection *> *)customSections { | ||||
|     NSParameterAssert(target); | ||||
|      | ||||
|     self = [super initWithStyle:UITableViewStyleGrouped]; | ||||
|     if (self) { | ||||
|         _object = target; | ||||
|         _explorer = explorer; | ||||
|         _customSections = customSections; | ||||
|     } | ||||
|  | ||||
|     return self; | ||||
| } | ||||
|  | ||||
| - (NSArray<NSString *> *)observedNotifications { | ||||
|     return @[ | ||||
|         kFLEXDefaultsHidePropertyIvarsKey, | ||||
|         kFLEXDefaultsHidePropertyMethodsKey, | ||||
|         kFLEXDefaultsHidePrivateMethodsKey, | ||||
|         kFLEXDefaultsShowMethodOverridesKey, | ||||
|         kFLEXDefaultsHideVariablePreviewsKey, | ||||
|     ]; | ||||
| } | ||||
|  | ||||
| #pragma mark - View controller lifecycle | ||||
|  | ||||
| - (void)viewDidLoad { | ||||
|     [super viewDidLoad]; | ||||
|  | ||||
|     self.showsShareToolbarItem = YES; | ||||
|     self.wantsSectionIndexTitles = YES; | ||||
|  | ||||
|     // Use [object class] here rather than object_getClass | ||||
|     // to avoid the KVO prefix for observed objects | ||||
|     self.title = [FLEXRuntimeUtility safeClassNameForObject:self.object]; | ||||
|  | ||||
|     // Search | ||||
|     self.showsSearchBar = YES; | ||||
|     self.searchBarDebounceInterval = kFLEXDebounceInstant; | ||||
|     self.showsCarousel = YES; | ||||
|  | ||||
|     // Carousel scope bar | ||||
|     [self.explorer reloadClassHierarchy]; | ||||
|     self.carousel.items = [self.explorer.classHierarchyClasses flex_mapped:^id(Class cls, NSUInteger idx) { | ||||
|         return NSStringFromClass(cls); | ||||
|     }]; | ||||
|      | ||||
|     // ... button for extra options | ||||
|     [self addToolbarItems:@[[UIBarButtonItem | ||||
|         flex_itemWithImage:FLEXResources.moreIcon target:self action:@selector(moreButtonPressed:) | ||||
|     ]]]; | ||||
|  | ||||
|     // Swipe gestures to swipe between classes in the hierarchy | ||||
|     UISwipeGestureRecognizer *leftSwipe = [[UISwipeGestureRecognizer alloc] | ||||
|         initWithTarget:self action:@selector(handleSwipeGesture:) | ||||
|     ]; | ||||
|     UISwipeGestureRecognizer *rightSwipe = [[UISwipeGestureRecognizer alloc] | ||||
|         initWithTarget:self action:@selector(handleSwipeGesture:) | ||||
|     ]; | ||||
|     leftSwipe.direction = UISwipeGestureRecognizerDirectionLeft; | ||||
|     rightSwipe.direction = UISwipeGestureRecognizerDirectionRight; | ||||
|     leftSwipe.delegate = self; | ||||
|     rightSwipe.delegate = self; | ||||
|     [self.tableView addGestureRecognizer:leftSwipe]; | ||||
|     [self.tableView addGestureRecognizer:rightSwipe]; | ||||
|      | ||||
|     // Observe preferences which may change on other screens | ||||
|     // | ||||
|     // "If your app targets iOS 9.0 and later or macOS 10.11 and later, | ||||
|     // you don't need to unregister an observer in its dealloc method." | ||||
|     for (NSString *pref in self.observedNotifications) { | ||||
|         [NSNotificationCenter.defaultCenter | ||||
|             addObserver:self | ||||
|             selector:@selector(fullyReloadData) | ||||
|             name:pref | ||||
|             object:nil | ||||
|         ]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (BOOL)scrollViewShouldScrollToTop:(UIScrollView *)scrollView { | ||||
|     [self.navigationController setToolbarHidden:NO animated:YES]; | ||||
|     return YES; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Overrides | ||||
|  | ||||
| /// Override to hide the description section when searching | ||||
| - (NSArray<FLEXTableViewSection *> *)nonemptySections { | ||||
|     if (self.shouldShowDescription) { | ||||
|         return super.nonemptySections; | ||||
|     } | ||||
|      | ||||
|     return [super.nonemptySections flex_filtered:^BOOL(FLEXTableViewSection *section, NSUInteger idx) { | ||||
|         return section != self.descriptionSection; | ||||
|     }]; | ||||
| } | ||||
|  | ||||
| - (NSArray<FLEXTableViewSection *> *)makeSections { | ||||
|     FLEXObjectExplorer *explorer = self.explorer; | ||||
|      | ||||
|     // Description section is only for instances | ||||
|     if (self.explorer.objectIsInstance) { | ||||
|         _descriptionSection = [FLEXSingleRowSection | ||||
|             title:@"Description" reuse:kFLEXMultilineCell cell:^(FLEXTableViewCell *cell) { | ||||
|                 cell.titleLabel.font = UIFont.flex_defaultTableCellFont; | ||||
|                 cell.titleLabel.text = explorer.objectDescription; | ||||
|             } | ||||
|         ]; | ||||
|         self.descriptionSection.filterMatcher = ^BOOL(NSString *filterText) { | ||||
|             return [explorer.objectDescription localizedCaseInsensitiveContainsString:filterText]; | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     // Object graph section | ||||
|     FLEXSingleRowSection *referencesSection = [FLEXSingleRowSection | ||||
|         title:@"Object Graph" reuse:kFLEXDefaultCell cell:^(FLEXTableViewCell *cell) { | ||||
|             cell.titleLabel.text = @"See Objects with References to This Object"; | ||||
|             cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator; | ||||
|         } | ||||
|     ]; | ||||
|     referencesSection.selectionAction = ^(UIViewController *host) { | ||||
|         UIViewController *references = [FLEXObjectListViewController | ||||
|             objectsWithReferencesToObject:explorer.object | ||||
|             retained:NO | ||||
|         ]; | ||||
|         [host.navigationController pushViewController:references animated:YES]; | ||||
|     }; | ||||
|  | ||||
|     NSMutableArray *sections = [NSMutableArray arrayWithArray:@[ | ||||
|         [FLEXMetadataSection explorer:self.explorer kind:FLEXMetadataKindProperties], | ||||
|         [FLEXMetadataSection explorer:self.explorer kind:FLEXMetadataKindClassProperties], | ||||
|         [FLEXMetadataSection explorer:self.explorer kind:FLEXMetadataKindIvars], | ||||
|         [FLEXMetadataSection explorer:self.explorer kind:FLEXMetadataKindMethods], | ||||
|         [FLEXMetadataSection explorer:self.explorer kind:FLEXMetadataKindClassMethods], | ||||
|         [FLEXMetadataSection explorer:self.explorer kind:FLEXMetadataKindClassHierarchy], | ||||
|         [FLEXMetadataSection explorer:self.explorer kind:FLEXMetadataKindProtocols], | ||||
|         [FLEXMetadataSection explorer:self.explorer kind:FLEXMetadataKindOther], | ||||
|         referencesSection | ||||
|     ]]; | ||||
|  | ||||
|     if (self.customSections) { | ||||
|         [sections insertObjects:self.customSections atIndexes:[NSIndexSet | ||||
|             indexSetWithIndexesInRange:NSMakeRange(0, self.customSections.count) | ||||
|         ]]; | ||||
|     } | ||||
|     if (self.descriptionSection) { | ||||
|         [sections insertObject:self.descriptionSection atIndex:0]; | ||||
|     } | ||||
|  | ||||
|     return sections.copy; | ||||
| } | ||||
|  | ||||
| /// In our case, all this does is reload the table view, | ||||
| /// or reload the sections' data if we changed places | ||||
| /// in the class hierarchy. Doesn't refresh \c self.explorer | ||||
| - (void)reloadData { | ||||
|     // Check to see if class scope changed, update accordingly | ||||
|     if (self.explorer.classScope != self.selectedScope) { | ||||
|         self.explorer.classScope = self.selectedScope; | ||||
|         [self reloadSections]; | ||||
|     } | ||||
|      | ||||
|     [super reloadData]; | ||||
| } | ||||
|  | ||||
| - (void)shareButtonPressed:(UIBarButtonItem *)sender { | ||||
|     [FLEXAlert makeSheet:^(FLEXAlert *make) { | ||||
|         make.button(@"Add to Bookmarks").handler(^(NSArray<NSString *> *strings) { | ||||
|             [FLEXBookmarkManager.bookmarks addObject:self.object]; | ||||
|         }); | ||||
|         make.button(@"Copy Description").handler(^(NSArray<NSString *> *strings) { | ||||
|             UIPasteboard.generalPasteboard.string = self.explorer.objectDescription; | ||||
|         }); | ||||
|         make.button(@"Copy Address").handler(^(NSArray<NSString *> *strings) { | ||||
|             UIPasteboard.generalPasteboard.string = [FLEXUtility addressOfObject:self.object]; | ||||
|         }); | ||||
|         make.button(@"Cancel").cancelStyle(); | ||||
|     } showFrom:self source:sender]; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Private | ||||
|  | ||||
| /// Unlike \c -reloadData, this refreshes everything, including the explorer. | ||||
| - (void)fullyReloadData { | ||||
|     [self.explorer reloadMetadata]; | ||||
|     [self reloadSections]; | ||||
|     [self reloadData]; | ||||
| } | ||||
|  | ||||
| - (void)handleSwipeGesture:(UISwipeGestureRecognizer *)gesture { | ||||
|     if (gesture.state == UIGestureRecognizerStateEnded) { | ||||
|         switch (gesture.direction) { | ||||
|             case UISwipeGestureRecognizerDirectionRight: | ||||
|                 if (self.selectedScope > 0) { | ||||
|                     self.selectedScope -= 1; | ||||
|                 } | ||||
|                 break; | ||||
|             case UISwipeGestureRecognizerDirectionLeft: | ||||
|                 if (self.selectedScope != self.explorer.classHierarchy.count - 1) { | ||||
|                     self.selectedScope += 1; | ||||
|                 } | ||||
|                 break; | ||||
|  | ||||
|             default: | ||||
|                 break; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (BOOL)gestureRecognizer:(UIGestureRecognizer *)g1 shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)g2 { | ||||
|     // Prioritize important pan gestures over our swipe gesture | ||||
|     if ([g2 isKindOfClass:[UIPanGestureRecognizer class]]) { | ||||
|         if (g2 == self.navigationController.interactivePopGestureRecognizer) { | ||||
|             return NO; | ||||
|         } | ||||
|          | ||||
|         if (g2 == self.tableView.panGestureRecognizer) { | ||||
|             return NO; | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     return YES; | ||||
| } | ||||
|  | ||||
| - (BOOL)gestureRecognizerShouldBegin:(UISwipeGestureRecognizer *)gesture { | ||||
|     // Don't allow swiping from the carousel | ||||
|     CGPoint location = [gesture locationInView:self.tableView]; | ||||
|     if ([self.carousel hitTest:location withEvent:nil]) { | ||||
|         return NO; | ||||
|     } | ||||
|      | ||||
|     return YES; | ||||
| } | ||||
|      | ||||
| - (void)moreButtonPressed:(UIBarButtonItem *)sender { | ||||
|     NSUserDefaults *defaults = NSUserDefaults.standardUserDefaults; | ||||
|     // Maps preference keys to a description of what they affect | ||||
|     NSDictionary<NSString *, NSString *> *explorerToggles = @{ | ||||
|         kFLEXDefaultsHidePropertyIvarsKey:    @"Property-Backing Ivars", | ||||
|         kFLEXDefaultsHidePropertyMethodsKey:  @"Property-Backing Methods", | ||||
|         kFLEXDefaultsHidePrivateMethodsKey:   @"Likely Private Methods", | ||||
|         kFLEXDefaultsShowMethodOverridesKey:  @"Method Overrides", | ||||
|         kFLEXDefaultsHideVariablePreviewsKey: @"Variable Previews" | ||||
|     }; | ||||
|      | ||||
|     // Maps the key of the action itself to a map of a description | ||||
|     // of the action ("hide X") mapped to the current state. | ||||
|     // | ||||
|     // So keys that are hidden by default have NO mapped to "Show" | ||||
|     NSDictionary<NSString *, NSDictionary *> *nextStateDescriptions = @{ | ||||
|         kFLEXDefaultsHidePropertyIvarsKey:    @{ @NO: @"Hide ", @YES: @"Show " }, | ||||
|         kFLEXDefaultsHidePropertyMethodsKey:  @{ @NO: @"Hide ", @YES: @"Show " }, | ||||
|         kFLEXDefaultsHidePrivateMethodsKey:   @{ @NO: @"Hide ", @YES: @"Show " }, | ||||
|         kFLEXDefaultsShowMethodOverridesKey:  @{ @NO: @"Show ", @YES: @"Hide " }, | ||||
|         kFLEXDefaultsHideVariablePreviewsKey: @{ @NO: @"Hide ", @YES: @"Show " }, | ||||
|     }; | ||||
|      | ||||
|     [FLEXAlert makeSheet:^(FLEXAlert *make) { | ||||
|         make.title(@"Options"); | ||||
|          | ||||
|         for (NSString *option in explorerToggles.allKeys) { | ||||
|             BOOL current = [defaults boolForKey:option]; | ||||
|             NSString *title = [nextStateDescriptions[option][@(current)] | ||||
|                 stringByAppendingString:explorerToggles[option] | ||||
|             ]; | ||||
|             make.button(title).handler(^(NSArray<NSString *> *strings) { | ||||
|                 [NSUserDefaults.standardUserDefaults flex_toggleBoolForKey:option]; | ||||
|                 [self fullyReloadData]; | ||||
|             }); | ||||
|         } | ||||
|          | ||||
|         make.button(@"Cancel").cancelStyle(); | ||||
|     } showFrom:self source:sender]; | ||||
| } | ||||
|  | ||||
| #pragma mark - Description | ||||
|  | ||||
| - (BOOL)shouldShowDescription { | ||||
|     // Hide if we have filter text; it is rarely | ||||
|     // useful to see the description when searching | ||||
|     // since it's already at the top of the screen | ||||
|     if (self.filterText.length) { | ||||
|         return NO; | ||||
|     } | ||||
|  | ||||
|     return YES; | ||||
| } | ||||
|  | ||||
| - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath { | ||||
|     // For the description section, we want that nice slim/snug looking row. | ||||
|     // Other rows use the automatic size. | ||||
|     FLEXTableViewSection *section = self.filterDelegate.sections[indexPath.section]; | ||||
|      | ||||
|     if (section == self.descriptionSection) { | ||||
|         NSAttributedString *attributedText = [[NSAttributedString alloc] | ||||
|             initWithString:self.explorer.objectDescription | ||||
|             attributes:@{ NSFontAttributeName : UIFont.flex_defaultTableCellFont } | ||||
|         ]; | ||||
|          | ||||
|         return [FLEXMultilineTableViewCell | ||||
|             preferredHeightWithAttributedText:attributedText | ||||
|             maxWidth:tableView.frame.size.width - tableView.separatorInset.right | ||||
|             style:tableView.style | ||||
|             showsAccessory:NO | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     return UITableViewAutomaticDimension; | ||||
| } | ||||
|  | ||||
| - (BOOL)tableView:(UITableView *)tableView shouldShowMenuForRowAtIndexPath:(NSIndexPath *)indexPath { | ||||
|     return self.filterDelegate.sections[indexPath.section] == self.descriptionSection; | ||||
| } | ||||
|  | ||||
| - (BOOL)tableView:(UITableView *)tableView canPerformAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender { | ||||
|     // Only the description section has "actions" | ||||
|     if (self.filterDelegate.sections[indexPath.section] == self.descriptionSection) { | ||||
|         return action == @selector(copy:); | ||||
|     } | ||||
|  | ||||
|     return NO; | ||||
| } | ||||
|  | ||||
| - (void)tableView:(UITableView *)tableView performAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender { | ||||
|     if (action == @selector(copy:)) { | ||||
|         UIPasteboard.generalPasteboard.string = self.explorer.objectDescription; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,93 @@ | ||||
| // | ||||
| //  FLEXCollectionContentSection.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 8/28/19. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXTableViewSection.h" | ||||
| #import "FLEXObjectInfoSection.h" | ||||
| @class FLEXCollectionContentSection, FLEXTableViewCell; | ||||
| @protocol FLEXCollection, FLEXMutableCollection; | ||||
|  | ||||
| /// Any foundation collection implicitly conforms to FLEXCollection. | ||||
| /// This future should return one. We don't explicitly put FLEXCollection | ||||
| /// here because making generic collections conform to FLEXCollection breaks | ||||
| /// compile-time features of generic arrays, such as \c someArray[0].property | ||||
| typedef id<NSObject, NSFastEnumeration /* FLEXCollection */>(^FLEXCollectionContentFuture)(__kindof FLEXCollectionContentSection *section); | ||||
|  | ||||
| #pragma mark Collection | ||||
| /// A protocol that enables \c FLEXCollectionContentSection to operate on any arbitrary collection. | ||||
| /// \c NSArray, \c NSDictionary, \c NSSet, and \c NSOrderedSet all conform to this protocol. | ||||
| @protocol FLEXCollection <NSObject, NSFastEnumeration> | ||||
|  | ||||
| @property (nonatomic, readonly) NSUInteger count; | ||||
|  | ||||
| - (id)copy; | ||||
| - (id)mutableCopy; | ||||
|  | ||||
| @optional | ||||
|  | ||||
| /// Unordered, unkeyed collections must implement this | ||||
| @property (nonatomic, readonly) NSArray *allObjects; | ||||
| /// Keyed collections must implement this and \c objectForKeyedSubscript: | ||||
| @property (nonatomic, readonly) NSArray *allKeys; | ||||
|  | ||||
| /// Ordered, indexed collections must implement this. | ||||
| - (id)objectAtIndexedSubscript:(NSUInteger)idx; | ||||
| /// Keyed, unordered collections must implement this and \c allKeys | ||||
| - (id)objectForKeyedSubscript:(id)idx; | ||||
|  | ||||
| @end | ||||
|  | ||||
| @protocol FLEXMutableCollection <FLEXCollection> | ||||
| - (void)filterUsingPredicate:(NSPredicate *)predicate; | ||||
| @end | ||||
|  | ||||
|  | ||||
| #pragma mark - FLEXCollectionContentSection | ||||
| /// A custom section for viewing collection elements. | ||||
| /// | ||||
| /// Tapping on a row pushes an object explorer for that element. | ||||
| @interface FLEXCollectionContentSection<__covariant ObjectType> : FLEXTableViewSection <FLEXObjectInfoSection> { | ||||
|     @protected | ||||
|     /// Unused if initialized with a future | ||||
|     id<FLEXCollection> _collection; | ||||
|     /// Unused if initialized with a collection | ||||
|     FLEXCollectionContentFuture _collectionFuture; | ||||
|     /// The filtered collection from \c _collection or \c _collectionFuture | ||||
|     id<FLEXCollection> _cachedCollection; | ||||
| } | ||||
|  | ||||
| + (instancetype)forCollection:(id)collection; | ||||
| /// The future given should be safe to call more than once. | ||||
| /// The result of calling this future multiple times may yield | ||||
| /// different results each time if the data is changing by nature. | ||||
| + (instancetype)forReusableFuture:(FLEXCollectionContentFuture)collectionFuture; | ||||
|  | ||||
| /// Defaults to \c NO | ||||
| @property (nonatomic) BOOL hideSectionTitle; | ||||
| /// Defaults to \c nil | ||||
| @property (nonatomic, copy) NSString *customTitle; | ||||
| /// Defaults to \c NO | ||||
| /// | ||||
| /// Settings this to \c NO will not display the element index for ordered collections. | ||||
| /// This property only applies to \c NSArray or \c NSOrderedSet and their subclasses. | ||||
| @property (nonatomic) BOOL hideOrderIndexes; | ||||
|  | ||||
| /// Set this property to provide a custom filter matcher. | ||||
| /// | ||||
| /// By default, the collection will filter on the title and subtitle of the row. | ||||
| /// So if you don't ever call \c configureCell: for example, you will need to set | ||||
| /// this property so that your filter logic will match how you're setting up the cell.  | ||||
| @property (nonatomic) BOOL (^customFilter)(NSString *filterText, ObjectType element); | ||||
|  | ||||
| /// Get the object in the collection associated with the given row. | ||||
| /// For dictionaries, this returns the value, not the key. | ||||
| - (ObjectType)objectForRow:(NSInteger)row; | ||||
|  | ||||
| /// Subclasses may override. | ||||
| - (UITableViewCellAccessoryType)accessoryTypeForRow:(NSInteger)row; | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,246 @@ | ||||
| // | ||||
| //  FLEXCollectionContentSection.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 8/28/19. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXCollectionContentSection.h" | ||||
| #import "FLEXUtility.h" | ||||
| #import "FLEXRuntimeUtility.h" | ||||
| #import "FLEXSubtitleTableViewCell.h" | ||||
| #import "FLEXTableView.h" | ||||
| #import "FLEXObjectExplorerFactory.h" | ||||
| #import "FLEXDefaultEditorViewController.h" | ||||
|  | ||||
| typedef NS_ENUM(NSUInteger, FLEXCollectionType) { | ||||
|     FLEXUnsupportedCollection, | ||||
|     FLEXOrderedCollection, | ||||
|     FLEXUnorderedCollection, | ||||
|     FLEXKeyedCollection | ||||
| }; | ||||
|  | ||||
| @interface NSArray (FLEXCollection) <FLEXCollection> @end | ||||
| @interface NSSet (FLEXCollection) <FLEXCollection> @end | ||||
| @interface NSOrderedSet (FLEXCollection) <FLEXCollection> @end | ||||
| @interface NSDictionary (FLEXCollection) <FLEXCollection> @end | ||||
|  | ||||
| @interface NSMutableArray (FLEXMutableCollection) <FLEXMutableCollection> @end | ||||
| @interface NSMutableSet (FLEXMutableCollection) <FLEXMutableCollection> @end | ||||
| @interface NSMutableOrderedSet (FLEXMutableCollection) <FLEXMutableCollection> @end | ||||
| @interface NSMutableDictionary (FLEXMutableCollection) <FLEXMutableCollection> | ||||
| - (void)filterUsingPredicate:(NSPredicate *)predicate; | ||||
| @end | ||||
|  | ||||
| @interface FLEXCollectionContentSection () | ||||
| /// Generated from \c collectionFuture or \c collection | ||||
| @property (nonatomic, copy) id<FLEXCollection> cachedCollection; | ||||
| /// A static collection to display | ||||
| @property (nonatomic, readonly) id<FLEXCollection> collection; | ||||
| /// A collection that may change over time and can be called upon for new data | ||||
| @property (nonatomic, readonly) FLEXCollectionContentFuture collectionFuture; | ||||
| @property (nonatomic, readonly) FLEXCollectionType collectionType; | ||||
| @property (nonatomic, readonly) BOOL isMutable; | ||||
| @end | ||||
|  | ||||
| @implementation FLEXCollectionContentSection | ||||
| @synthesize filterText = _filterText; | ||||
|  | ||||
| #pragma mark Initialization | ||||
|  | ||||
| + (instancetype)forObject:(id)object { | ||||
|     return [self forCollection:object]; | ||||
| } | ||||
|  | ||||
| + (id)forCollection:(id<FLEXCollection>)collection { | ||||
|     FLEXCollectionContentSection *section = [self new]; | ||||
|     section->_collectionType = [self typeForCollection:collection]; | ||||
|     section->_collection = collection; | ||||
|     section.cachedCollection = collection; | ||||
|     section->_isMutable = [collection respondsToSelector:@selector(filterUsingPredicate:)]; | ||||
|     return section; | ||||
| } | ||||
|  | ||||
| + (id)forReusableFuture:(FLEXCollectionContentFuture)collectionFuture { | ||||
|     FLEXCollectionContentSection *section = [self new]; | ||||
|     section->_collectionFuture = collectionFuture; | ||||
|     section.cachedCollection = (id<FLEXCollection>)collectionFuture(section); | ||||
|     section->_collectionType = [self typeForCollection:section.cachedCollection]; | ||||
|     section->_isMutable = [section->_cachedCollection respondsToSelector:@selector(filterUsingPredicate:)]; | ||||
|     return section; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Misc | ||||
|  | ||||
| + (FLEXCollectionType)typeForCollection:(id<FLEXCollection>)collection { | ||||
|     // Order matters here, as NSDictionary is keyed but it responds to allObjects | ||||
|     if ([collection respondsToSelector:@selector(objectAtIndex:)]) { | ||||
|         return FLEXOrderedCollection; | ||||
|     } | ||||
|     if ([collection respondsToSelector:@selector(objectForKey:)]) { | ||||
|         return FLEXKeyedCollection; | ||||
|     } | ||||
|     if ([collection respondsToSelector:@selector(allObjects)]) { | ||||
|         return FLEXUnorderedCollection; | ||||
|     } | ||||
|  | ||||
|     [NSException raise:NSInvalidArgumentException | ||||
|                 format:@"Given collection does not properly conform to FLEXCollection"]; | ||||
|     return FLEXUnsupportedCollection; | ||||
| } | ||||
|  | ||||
| /// Row titles | ||||
| /// - Ordered: the index | ||||
| /// - Unordered: the object | ||||
| /// - Keyed: the key | ||||
| - (NSString *)titleForRow:(NSInteger)row { | ||||
|     switch (self.collectionType) { | ||||
|         case FLEXOrderedCollection: | ||||
|             if (!self.hideOrderIndexes) { | ||||
|                 return @(row).stringValue; | ||||
|             } | ||||
|             // Fall-through | ||||
|         case FLEXUnorderedCollection: | ||||
|             return [self describe:[self objectForRow:row]]; | ||||
|         case FLEXKeyedCollection: | ||||
|             return [self describe:self.cachedCollection.allKeys[row]]; | ||||
|  | ||||
|         case FLEXUnsupportedCollection: | ||||
|             return nil; | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// Row subtitles | ||||
| /// - Ordered: the object | ||||
| /// - Unordered: nothing | ||||
| /// - Keyed: the value | ||||
| - (NSString *)subtitleForRow:(NSInteger)row { | ||||
|     switch (self.collectionType) { | ||||
|         case FLEXOrderedCollection: | ||||
|             if (!self.hideOrderIndexes) { | ||||
|                 nil; | ||||
|             } | ||||
|             // Fall-through | ||||
|         case FLEXKeyedCollection: | ||||
|             return [self describe:[self objectForRow:row]]; | ||||
|         case FLEXUnorderedCollection: | ||||
|             return nil; | ||||
|  | ||||
|         case FLEXUnsupportedCollection: | ||||
|             return nil; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (NSString *)describe:(id)object { | ||||
|     return [FLEXRuntimeUtility summaryForObject:object]; | ||||
| } | ||||
|  | ||||
| - (id)objectForRow:(NSInteger)row { | ||||
|     switch (self.collectionType) { | ||||
|         case FLEXOrderedCollection: | ||||
|             return self.cachedCollection[row]; | ||||
|         case FLEXUnorderedCollection: | ||||
|             return self.cachedCollection.allObjects[row]; | ||||
|         case FLEXKeyedCollection: | ||||
|             return self.cachedCollection[self.cachedCollection.allKeys[row]]; | ||||
|  | ||||
|         case FLEXUnsupportedCollection: | ||||
|             return nil; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (UITableViewCellAccessoryType)accessoryTypeForRow:(NSInteger)row { | ||||
|     return UITableViewCellAccessoryDisclosureIndicator; | ||||
| //    return self.isMutable ? UITableViewCellAccessoryDetailDisclosureButton : UITableViewCellAccessoryDisclosureIndicator; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Overrides | ||||
|  | ||||
| - (NSString *)title { | ||||
|     if (!self.hideSectionTitle) { | ||||
|         if (self.customTitle) { | ||||
|             return self.customTitle; | ||||
|         } | ||||
|          | ||||
|         return FLEXPluralString(self.cachedCollection.count, @"Entries", @"Entry"); | ||||
|     } | ||||
|      | ||||
|     return nil; | ||||
| } | ||||
|  | ||||
| - (NSInteger)numberOfRows { | ||||
|     return self.cachedCollection.count; | ||||
| } | ||||
|  | ||||
| - (void)setFilterText:(NSString *)filterText { | ||||
|     super.filterText = filterText; | ||||
|      | ||||
|     if (filterText.length) { | ||||
|         BOOL (^matcher)(id, id) = self.customFilter ?: ^BOOL(NSString *query, id obj) { | ||||
|             return [[self describe:obj] localizedCaseInsensitiveContainsString:query]; | ||||
|         }; | ||||
|          | ||||
|         NSPredicate *filter = [NSPredicate predicateWithBlock:^BOOL(id obj, NSDictionary *bindings) { | ||||
|             return matcher(filterText, obj); | ||||
|         }]; | ||||
|          | ||||
|         id<FLEXMutableCollection> tmp = self.cachedCollection.mutableCopy; | ||||
|         [tmp filterUsingPredicate:filter]; | ||||
|         self.cachedCollection = tmp; | ||||
|     } else { | ||||
|         self.cachedCollection = self.collection ?: (id<FLEXCollection>)self.collectionFuture(self); | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)reloadData { | ||||
|     if (self.collectionFuture) { | ||||
|         self.cachedCollection = (id<FLEXCollection>)self.collectionFuture(self); | ||||
|     } else { | ||||
|         self.cachedCollection = self.collection.copy; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (BOOL)canSelectRow:(NSInteger)row { | ||||
|     return YES; | ||||
| } | ||||
|  | ||||
| - (UIViewController *)viewControllerToPushForRow:(NSInteger)row { | ||||
|     return [FLEXObjectExplorerFactory explorerViewControllerForObject:[self objectForRow:row]]; | ||||
| } | ||||
|  | ||||
| - (NSString *)reuseIdentifierForRow:(NSInteger)row { | ||||
|     return kFLEXDetailCell; | ||||
| } | ||||
|  | ||||
| - (void)configureCell:(__kindof FLEXTableViewCell *)cell forRow:(NSInteger)row { | ||||
|     cell.titleLabel.text = [self titleForRow:row]; | ||||
|     cell.subtitleLabel.text = [self subtitleForRow:row]; | ||||
|     cell.accessoryType = [self accessoryTypeForRow:row]; | ||||
| } | ||||
|  | ||||
| @end | ||||
|  | ||||
|  | ||||
| #pragma mark - NSMutableDictionary | ||||
|  | ||||
| @implementation NSMutableDictionary (FLEXMutableCollection) | ||||
|  | ||||
| - (void)filterUsingPredicate:(NSPredicate *)predicate { | ||||
|     id test = ^BOOL(id key, NSUInteger idx, BOOL *stop) { | ||||
|         if ([predicate evaluateWithObject:key]) { | ||||
|             return NO; | ||||
|         } | ||||
|          | ||||
|         return ![predicate evaluateWithObject:self[key]]; | ||||
|     }; | ||||
|      | ||||
|     NSArray *keys = self.allKeys; | ||||
|     NSIndexSet *remove = [keys indexesOfObjectsPassingTest:test]; | ||||
|      | ||||
|     [self removeObjectsForKeys:[keys objectsAtIndexes:remove]]; | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,16 @@ | ||||
| // | ||||
| //  FLEXColorPreviewSection.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 12/12/19. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXSingleRowSection.h" | ||||
| #import "FLEXObjectInfoSection.h" | ||||
|  | ||||
| @interface FLEXColorPreviewSection : FLEXSingleRowSection <FLEXObjectInfoSection> | ||||
|  | ||||
| + (instancetype)forObject:(UIColor *)color; | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,30 @@ | ||||
| // | ||||
| //  FLEXColorPreviewSection.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 12/12/19. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXColorPreviewSection.h" | ||||
|  | ||||
| @implementation FLEXColorPreviewSection | ||||
|  | ||||
| + (instancetype)forObject:(UIColor *)color { | ||||
|     return [self title:@"Color" reuse:nil cell:^(__kindof UITableViewCell *cell) { | ||||
|         cell.backgroundColor = color; | ||||
|     }]; | ||||
| } | ||||
|  | ||||
| - (BOOL)canSelectRow:(NSInteger)row { | ||||
|     return NO; | ||||
| } | ||||
|  | ||||
| - (BOOL (^)(NSString *))filterMatcher { | ||||
|     return ^BOOL(NSString *filterText) { | ||||
|         // Hide when searching | ||||
|         return !filterText.length; | ||||
|     }; | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,27 @@ | ||||
| // | ||||
| //  FLEXDefaultsContentSection.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 8/28/19. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXCollectionContentSection.h" | ||||
| #import "FLEXObjectInfoSection.h" | ||||
|  | ||||
| @interface FLEXDefaultsContentSection : FLEXCollectionContentSection <FLEXObjectInfoSection> | ||||
|  | ||||
| /// Uses \c NSUserDefaults.standardUserDefaults | ||||
| + (instancetype)standard; | ||||
| + (instancetype)forDefaults:(NSUserDefaults *)userDefaults; | ||||
|  | ||||
| /// Whether or not to filter out keys not present in the app's user defaults file. | ||||
| /// | ||||
| /// This is useful for filtering out some useless keys that seem to appear | ||||
| /// in every app's defaults but are never actually used or touched by the app. | ||||
| /// Only applies to instances using \c NSUserDefaults.standardUserDefaults. | ||||
| /// This is the default for any instance using \c standardUserDefaults, so | ||||
| /// you must opt-out in those instances if you don't want this behavior. | ||||
| @property (nonatomic) BOOL onlyShowKeysForAppPrefs; | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,117 @@ | ||||
| // | ||||
| //  FLEXDefaultsContentSection.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 8/28/19. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXDefaultsContentSection.h" | ||||
| #import "FLEXDefaultEditorViewController.h" | ||||
| #import "FLEXUtility.h" | ||||
|  | ||||
| @interface FLEXDefaultsContentSection () | ||||
| @property (nonatomic) NSUserDefaults *defaults; | ||||
| @property (nonatomic) NSArray *keys; | ||||
| @property (nonatomic, readonly) NSDictionary *unexcludedDefaults; | ||||
| @end | ||||
|  | ||||
| @implementation FLEXDefaultsContentSection | ||||
| @synthesize keys = _keys; | ||||
|  | ||||
| #pragma mark Initialization | ||||
|  | ||||
| + (instancetype)forObject:(id)object { | ||||
|     return [self forDefaults:object]; | ||||
| } | ||||
|  | ||||
| + (instancetype)standard { | ||||
|     return [self forDefaults:NSUserDefaults.standardUserDefaults]; | ||||
| } | ||||
|  | ||||
| + (instancetype)forDefaults:(NSUserDefaults *)userDefaults { | ||||
|     FLEXDefaultsContentSection *section = [self forReusableFuture:^id(FLEXDefaultsContentSection *section) { | ||||
|         section.defaults = userDefaults; | ||||
|         section.onlyShowKeysForAppPrefs = YES; | ||||
|         return section.unexcludedDefaults; | ||||
|     }]; | ||||
|     return section; | ||||
| } | ||||
|  | ||||
| #pragma mark - Overrides | ||||
|  | ||||
| - (NSString *)title { | ||||
|     return @"Defaults"; | ||||
| } | ||||
|  | ||||
| - (void (^)(__kindof UIViewController *))didPressInfoButtonAction:(NSInteger)row { | ||||
|     return ^(UIViewController *host) { | ||||
|         if ([FLEXDefaultEditorViewController canEditDefaultWithValue:[self objectForRow:row]]) { | ||||
|             // We use titleForRow: to get the key because self.keys is not | ||||
|             // necessarily in the same order as the keys being displayed | ||||
|             FLEXVariableEditorViewController *controller = [FLEXDefaultEditorViewController | ||||
|                 target:self.defaults key:[self titleForRow:row] commitHandler:^{ | ||||
|                     [self reloadData:YES]; | ||||
|                 } | ||||
|             ]; | ||||
|             [host.navigationController pushViewController:controller animated:YES]; | ||||
|         } else { | ||||
|             [FLEXAlert showAlert:@"Oh No…" message:@"We can't edit this entry :(" from:host]; | ||||
|         } | ||||
|     }; | ||||
| } | ||||
|  | ||||
| - (UITableViewCellAccessoryType)accessoryTypeForRow:(NSInteger)row { | ||||
|     return UITableViewCellAccessoryDetailDisclosureButton; | ||||
| } | ||||
|  | ||||
| #pragma mark - Private | ||||
|  | ||||
| - (NSArray *)keys { | ||||
|     if (!_keys) { | ||||
|         if (self.onlyShowKeysForAppPrefs) { | ||||
|             // Read keys from preferences file | ||||
|             NSString *bundle = NSBundle.mainBundle.bundleIdentifier; | ||||
|             NSString *prefsPath = [NSHomeDirectory() stringByAppendingPathComponent:@"Library/Preferences"]; | ||||
|             NSString *filePath = [NSString stringWithFormat:@"%@/%@.plist", prefsPath, bundle]; | ||||
|             self.keys = [NSDictionary dictionaryWithContentsOfFile:filePath].allKeys; | ||||
|         } else { | ||||
|             self.keys = self.defaults.dictionaryRepresentation.allKeys; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     return _keys; | ||||
| } | ||||
|  | ||||
| - (void)setKeys:(NSArray *)keys { | ||||
|     _keys = [keys sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)]; | ||||
| } | ||||
|  | ||||
| - (NSDictionary *)unexcludedDefaults { | ||||
|     // Case: no excluding | ||||
|     if (!self.onlyShowKeysForAppPrefs) { | ||||
|         return self.defaults.dictionaryRepresentation; | ||||
|     } | ||||
|  | ||||
|     // Always regenerate key allowlist when this method is called | ||||
|     _keys = nil; | ||||
|  | ||||
|     // Generate new dictionary from unexcluded keys | ||||
|     NSArray *values = [self.defaults.dictionaryRepresentation | ||||
|         objectsForKeys:self.keys notFoundMarker:NSNull.null | ||||
|     ]; | ||||
|     return [NSDictionary dictionaryWithObjects:values forKeys:self.keys]; | ||||
| } | ||||
|  | ||||
| #pragma mark - Public | ||||
|  | ||||
| - (void)setOnlyShowKeysForAppPrefs:(BOOL)onlyShowKeysForAppPrefs { | ||||
|     if (onlyShowKeysForAppPrefs) { | ||||
|         // This property only applies if we're using standardUserDefaults | ||||
|         if (self.defaults != NSUserDefaults.standardUserDefaults) return; | ||||
|     } | ||||
|  | ||||
|     _onlyShowKeysForAppPrefs = onlyShowKeysForAppPrefs; | ||||
| } | ||||
|  | ||||
| @end | ||||
							
								
								
									
										37
									
								
								Tweaks/FLEX/ObjectExplorers/Sections/FLEXMetadataSection.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								Tweaks/FLEX/ObjectExplorers/Sections/FLEXMetadataSection.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | ||||
| // | ||||
| //  FLEXMetadataSection.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 9/19/19. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXTableViewSection.h" | ||||
| #import "FLEXObjectExplorer.h" | ||||
|  | ||||
| typedef NS_ENUM(NSUInteger, FLEXMetadataKind) { | ||||
|     FLEXMetadataKindProperties = 1, | ||||
|     FLEXMetadataKindClassProperties, | ||||
|     FLEXMetadataKindIvars, | ||||
|     FLEXMetadataKindMethods, | ||||
|     FLEXMetadataKindClassMethods, | ||||
|     FLEXMetadataKindClassHierarchy, | ||||
|     FLEXMetadataKindProtocols, | ||||
|     FLEXMetadataKindOther | ||||
| }; | ||||
|  | ||||
| /// This section is used for displaying ObjC runtime metadata | ||||
| /// about a class or object, such as listing methods, properties, etc. | ||||
| @interface FLEXMetadataSection : FLEXTableViewSection | ||||
|  | ||||
| + (instancetype)explorer:(FLEXObjectExplorer *)explorer kind:(FLEXMetadataKind)metadataKind; | ||||
|  | ||||
| @property (nonatomic, readonly) FLEXMetadataKind metadataKind; | ||||
|  | ||||
| /// The names of metadata to exclude. Useful if you wish to group specific | ||||
| /// properties or methods together in their own section outside of this one. | ||||
| /// | ||||
| /// Setting this property calls \c reloadData on this section. | ||||
| @property (nonatomic) NSSet<NSString *> *excludedMetadata; | ||||
|  | ||||
| @end | ||||
							
								
								
									
										233
									
								
								Tweaks/FLEX/ObjectExplorers/Sections/FLEXMetadataSection.m
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										233
									
								
								Tweaks/FLEX/ObjectExplorers/Sections/FLEXMetadataSection.m
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,233 @@ | ||||
| // | ||||
| //  FLEXMetadataSection.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 9/19/19. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXMetadataSection.h" | ||||
| #import "FLEXTableView.h" | ||||
| #import "FLEXTableViewCell.h" | ||||
| #import "FLEXObjectExplorerFactory.h" | ||||
| #import "FLEXFieldEditorViewController.h" | ||||
| #import "FLEXMethodCallingViewController.h" | ||||
| #import "FLEXIvar.h" | ||||
| #import "NSArray+FLEX.h" | ||||
| #import "FLEXRuntime+UIKitHelpers.h" | ||||
|  | ||||
| @interface FLEXMetadataSection () | ||||
| @property (nonatomic, readonly) FLEXObjectExplorer *explorer; | ||||
| /// Filtered | ||||
| @property (nonatomic, copy) NSArray<id<FLEXRuntimeMetadata>> *metadata; | ||||
| /// Unfiltered | ||||
| @property (nonatomic, copy) NSArray<id<FLEXRuntimeMetadata>> *allMetadata; | ||||
| @end | ||||
|  | ||||
| @implementation FLEXMetadataSection | ||||
|  | ||||
| #pragma mark - Initialization | ||||
|  | ||||
| + (instancetype)explorer:(FLEXObjectExplorer *)explorer kind:(FLEXMetadataKind)metadataKind { | ||||
|     return [[self alloc] initWithExplorer:explorer kind:metadataKind]; | ||||
| } | ||||
|  | ||||
| - (id)initWithExplorer:(FLEXObjectExplorer *)explorer kind:(FLEXMetadataKind)metadataKind { | ||||
|     self = [super init]; | ||||
|     if (self) { | ||||
|         _explorer = explorer; | ||||
|         _metadataKind = metadataKind; | ||||
|  | ||||
|         [self reloadData]; | ||||
|     } | ||||
|  | ||||
|     return self; | ||||
| } | ||||
|  | ||||
| #pragma mark - Private | ||||
|  | ||||
| - (NSString *)titleWithBaseName:(NSString *)baseName { | ||||
|     unsigned long totalCount = self.allMetadata.count; | ||||
|     unsigned long filteredCount = self.metadata.count; | ||||
|  | ||||
|     if (totalCount == filteredCount) { | ||||
|         return [baseName stringByAppendingFormat:@" (%lu)", totalCount]; | ||||
|     } else { | ||||
|         return [baseName stringByAppendingFormat:@" (%lu of %lu)", filteredCount, totalCount]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (UITableViewCellAccessoryType)accessoryTypeForRow:(NSInteger)row { | ||||
|     return [self.metadata[row] suggestedAccessoryTypeWithTarget:self.explorer.object]; | ||||
| } | ||||
|  | ||||
| #pragma mark - Public | ||||
|  | ||||
| - (void)setExcludedMetadata:(NSSet<NSString *> *)excludedMetadata { | ||||
|     _excludedMetadata = excludedMetadata; | ||||
|     [self reloadData]; | ||||
| } | ||||
|  | ||||
| #pragma mark - Overrides | ||||
|  | ||||
| - (NSString *)titleForRow:(NSInteger)row { | ||||
|     return [self.metadata[row] description]; | ||||
| } | ||||
|  | ||||
| - (NSString *)subtitleForRow:(NSInteger)row { | ||||
|     return [self.metadata[row] previewWithTarget:self.explorer.object]; | ||||
| } | ||||
|  | ||||
| - (NSString *)title { | ||||
|     switch (self.metadataKind) { | ||||
|         case FLEXMetadataKindProperties: | ||||
|             return [self titleWithBaseName:@"Properties"]; | ||||
|         case FLEXMetadataKindClassProperties: | ||||
|             return [self titleWithBaseName:@"Class Properties"]; | ||||
|         case FLEXMetadataKindIvars: | ||||
|             return [self titleWithBaseName:@"Ivars"]; | ||||
|         case FLEXMetadataKindMethods: | ||||
|             return [self titleWithBaseName:@"Methods"]; | ||||
|         case FLEXMetadataKindClassMethods: | ||||
|             return [self titleWithBaseName:@"Class Methods"]; | ||||
|         case FLEXMetadataKindClassHierarchy: | ||||
|             return [self titleWithBaseName:@"Class Hierarchy"]; | ||||
|         case FLEXMetadataKindProtocols: | ||||
|             return [self titleWithBaseName:@"Protocols"]; | ||||
|         case FLEXMetadataKindOther: | ||||
|             return @"Miscellaneous"; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (NSInteger)numberOfRows { | ||||
|     return self.metadata.count; | ||||
| } | ||||
|  | ||||
| - (void)setFilterText:(NSString *)filterText { | ||||
|     super.filterText = filterText; | ||||
|  | ||||
|     if (!self.filterText.length) { | ||||
|         self.metadata = self.allMetadata; | ||||
|     } else { | ||||
|         self.metadata = [self.allMetadata flex_filtered:^BOOL(id<FLEXRuntimeMetadata> obj, NSUInteger idx) { | ||||
|             return [obj.description localizedCaseInsensitiveContainsString:self.filterText]; | ||||
|         }]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)reloadData { | ||||
|     switch (self.metadataKind) { | ||||
|         case FLEXMetadataKindProperties: | ||||
|             self.allMetadata = self.explorer.properties; | ||||
|             break; | ||||
|         case FLEXMetadataKindClassProperties: | ||||
|             self.allMetadata = self.explorer.classProperties; | ||||
|             break; | ||||
|         case FLEXMetadataKindIvars: | ||||
|             self.allMetadata = self.explorer.ivars; | ||||
|             break; | ||||
|         case FLEXMetadataKindMethods: | ||||
|             self.allMetadata = self.explorer.methods; | ||||
|             break; | ||||
|         case FLEXMetadataKindClassMethods: | ||||
|             self.allMetadata = self.explorer.classMethods; | ||||
|             break; | ||||
|         case FLEXMetadataKindProtocols: | ||||
|             self.allMetadata = self.explorer.conformedProtocols; | ||||
|             break; | ||||
|         case FLEXMetadataKindClassHierarchy: | ||||
|             self.allMetadata = self.explorer.classHierarchy; | ||||
|             break; | ||||
|         case FLEXMetadataKindOther: | ||||
|             self.allMetadata = @[self.explorer.instanceSize, self.explorer.imageName]; | ||||
|             break; | ||||
|     } | ||||
|  | ||||
|     // Remove excluded metadata | ||||
|     if (self.excludedMetadata.count) { | ||||
|         id filterBlock = ^BOOL(id<FLEXRuntimeMetadata> obj, NSUInteger idx) { | ||||
|             return ![self.excludedMetadata containsObject:obj.name]; | ||||
|         }; | ||||
|  | ||||
|         // Filter exclusions and sort | ||||
|         self.allMetadata = [[self.allMetadata flex_filtered:filterBlock] | ||||
|             sortedArrayUsingSelector:@selector(compare:) | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     // Re-filter data | ||||
|     self.filterText = self.filterText; | ||||
| } | ||||
|  | ||||
| - (BOOL)canSelectRow:(NSInteger)row { | ||||
|     UITableViewCellAccessoryType accessory = [self accessoryTypeForRow:row]; | ||||
|     return accessory == UITableViewCellAccessoryDisclosureIndicator || | ||||
|         accessory == UITableViewCellAccessoryDetailDisclosureButton; | ||||
| } | ||||
|  | ||||
| - (NSString *)reuseIdentifierForRow:(NSInteger)row { | ||||
|     return [self.metadata[row] reuseIdentifierWithTarget:self.explorer.object] ?: kFLEXCodeFontCell; | ||||
| } | ||||
|  | ||||
| - (UIViewController *)viewControllerToPushForRow:(NSInteger)row { | ||||
|     return [self.metadata[row] viewerWithTarget:self.explorer.object]; | ||||
| } | ||||
|  | ||||
| - (void (^)(__kindof UIViewController *))didPressInfoButtonAction:(NSInteger)row { | ||||
|     return ^(UIViewController *host) { | ||||
|         [host.navigationController pushViewController:[self editorForRow:row] animated:YES]; | ||||
|     }; | ||||
| } | ||||
|  | ||||
| - (UIViewController *)editorForRow:(NSInteger)row { | ||||
|     return [self.metadata[row] editorWithTarget:self.explorer.object section:self]; | ||||
| } | ||||
|  | ||||
| - (void)configureCell:(__kindof FLEXTableViewCell *)cell forRow:(NSInteger)row { | ||||
|     cell.titleLabel.text = [self titleForRow:row]; | ||||
|     cell.subtitleLabel.text = [self subtitleForRow:row]; | ||||
|     cell.accessoryType = [self accessoryTypeForRow:row]; | ||||
| } | ||||
|  | ||||
| - (NSString *)menuSubtitleForRow:(NSInteger)row { | ||||
|     return [self.metadata[row] contextualSubtitleWithTarget:self.explorer.object]; | ||||
| } | ||||
|  | ||||
| - (NSArray<UIMenuElement *> *)menuItemsForRow:(NSInteger)row sender:(UIViewController *)sender { | ||||
|     NSArray<UIMenuElement *> *existingItems = [super menuItemsForRow:row sender:sender]; | ||||
|      | ||||
|     // These two metadata kinds don't any of the additional options below | ||||
|     switch (self.metadataKind) { | ||||
|         case FLEXMetadataKindClassHierarchy: | ||||
|         case FLEXMetadataKindOther: | ||||
|             return existingItems; | ||||
|              | ||||
|         default: break; | ||||
|     } | ||||
|      | ||||
|     id<FLEXRuntimeMetadata> metadata = self.metadata[row]; | ||||
|     NSMutableArray<UIMenuElement *> *menuItems = [NSMutableArray new]; | ||||
|      | ||||
|     [menuItems addObject:[UIAction | ||||
|         actionWithTitle:@"Explore Metadata" | ||||
|         image:nil | ||||
|         identifier:nil | ||||
|         handler:^(__kindof UIAction *action) { | ||||
|             [sender.navigationController pushViewController:[FLEXObjectExplorerFactory | ||||
|                 explorerViewControllerForObject:metadata | ||||
|             ] animated:YES]; | ||||
|         } | ||||
|     ]]; | ||||
|     [menuItems addObjectsFromArray:[metadata | ||||
|         additionalActionsWithTarget:self.explorer.object sender:sender | ||||
|     ]]; | ||||
|     [menuItems addObjectsFromArray:existingItems]; | ||||
|      | ||||
|     return menuItems.copy; | ||||
| } | ||||
|  | ||||
| - (NSArray<NSString *> *)copyMenuItemsForRow:(NSInteger)row { | ||||
|     return [self.metadata[row] copiableMetadataWithTarget:self.explorer.object]; | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,58 @@ | ||||
| // | ||||
| //  FLEXMutableListSection.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 3/9/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXCollectionContentSection.h" | ||||
|  | ||||
| typedef void (^FLEXMutableListCellForElement)(__kindof UITableViewCell *cell, id element, NSInteger row); | ||||
|  | ||||
| /// A section aimed at meeting the needs of table views with one section | ||||
| /// (or, a section that shouldn't warrant the code duplication that comes | ||||
| /// with creating a new section just for some specific table view) | ||||
| /// | ||||
| /// Use this section if you want to display a growing list of rows, | ||||
| /// or even if you want to display a static list of rows. | ||||
| /// | ||||
| /// To support editing or inserting, implement the appropriate | ||||
| /// table view delegate methods in your table view delegate class | ||||
| /// and call \c mutate: (or \c setList: ) before updating the table view. | ||||
| /// | ||||
| /// By default, no section title is shown. Assign one to \c customTitle | ||||
| /// | ||||
| /// By default, \c kFLEXDetailCell is the reuse identifier used. If you need | ||||
| /// to support multiple reuse identifiers in a single section, implement the | ||||
| /// \c cellForRowAtIndexPath: method, dequeue the cell yourself and call | ||||
| /// \c -configureCell: on the appropriate section object, passing in the cell | ||||
| @interface FLEXMutableListSection<__covariant ObjectType> : FLEXCollectionContentSection | ||||
|  | ||||
| /// Initializes a section with an empty list. | ||||
| + (instancetype)list:(NSArray<ObjectType> *)list | ||||
|    cellConfiguration:(FLEXMutableListCellForElement)configurationBlock | ||||
|        filterMatcher:(BOOL(^)(NSString *filterText, id element))filterBlock; | ||||
|  | ||||
| /// By default, rows are not selectable. If you want rows | ||||
| /// to be selectable, provide a selection handler here. | ||||
| @property (nonatomic, copy) void (^selectionHandler)(__kindof UIViewController *host, ObjectType element); | ||||
|  | ||||
| /// The objects representing all possible rows in the section. | ||||
| @property (nonatomic) NSArray<ObjectType> *list; | ||||
| /// The objects representing the currently unfiltered rows in the section. | ||||
| @property (nonatomic, readonly) NSArray<ObjectType> *filteredList; | ||||
|  | ||||
| /// A readwrite version of the same property in \c FLEXTableViewSection.h | ||||
| /// | ||||
| /// This property expects one entry. An exception is thrown if more than one | ||||
| /// entry is supplied. If you need more than one reuse identifier within a single | ||||
| /// section, your view probably has more complexity than this class can handle. | ||||
| @property (nonatomic, readwrite) NSDictionary<NSString *, Class> *cellRegistrationMapping; | ||||
|  | ||||
| /// Call this method to mutate the full, unfiltered list. | ||||
| /// This ensures that \c filteredList is updated after any mutations. | ||||
| - (void)mutate:(void(^)(NSMutableArray *list))block; | ||||
|  | ||||
| @end | ||||
|  | ||||
							
								
								
									
										110
									
								
								Tweaks/FLEX/ObjectExplorers/Sections/FLEXMutableListSection.m
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								Tweaks/FLEX/ObjectExplorers/Sections/FLEXMutableListSection.m
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,110 @@ | ||||
| // | ||||
| //  FLEXMutableListSection.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 3/9/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXMutableListSection.h" | ||||
| #import "FLEXMacros.h" | ||||
|  | ||||
| @interface FLEXMutableListSection () | ||||
| @property (nonatomic, readonly) FLEXMutableListCellForElement configureCell; | ||||
| @end | ||||
|  | ||||
| @implementation FLEXMutableListSection | ||||
| @synthesize cellRegistrationMapping = _cellRegistrationMapping; | ||||
|  | ||||
| #pragma mark - Initialization | ||||
|  | ||||
| + (instancetype)list:(NSArray *)list | ||||
|    cellConfiguration:(FLEXMutableListCellForElement)cellConfig | ||||
|        filterMatcher:(BOOL(^)(NSString *, id))filterBlock { | ||||
|     return [[self alloc] initWithList:list configurationBlock:cellConfig filterMatcher:filterBlock]; | ||||
| } | ||||
|  | ||||
| - (id)initWithList:(NSArray *)list | ||||
| configurationBlock:(FLEXMutableListCellForElement)cellConfig | ||||
|      filterMatcher:(BOOL(^)(NSString *, id))filterBlock { | ||||
|     self = [super init]; | ||||
|     if (self) { | ||||
|         _configureCell = cellConfig; | ||||
|  | ||||
|         self.list = list.mutableCopy; | ||||
|         self.customFilter = filterBlock; | ||||
|         self.hideSectionTitle = YES; | ||||
|     } | ||||
|  | ||||
|     return self; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Public | ||||
|  | ||||
| - (NSArray *)list { | ||||
|     return (id)_collection; | ||||
| } | ||||
|  | ||||
| - (void)setList:(NSMutableArray *)list { | ||||
|     NSParameterAssert(list); | ||||
|     _collection = (id)list; | ||||
|  | ||||
|     [self reloadData]; | ||||
| } | ||||
|  | ||||
| - (NSArray *)filteredList { | ||||
|     return (id)_cachedCollection; | ||||
| } | ||||
|  | ||||
| - (void)mutate:(void (^)(NSMutableArray *))block { | ||||
|     block((NSMutableArray *)_collection); | ||||
|     [self reloadData]; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Overrides | ||||
|  | ||||
| - (void)setCustomTitle:(NSString *)customTitle { | ||||
|     super.customTitle = customTitle; | ||||
|     self.hideSectionTitle = customTitle == nil; | ||||
| } | ||||
|  | ||||
| - (BOOL)canSelectRow:(NSInteger)row { | ||||
|     return self.selectionHandler != nil; | ||||
| } | ||||
|  | ||||
| - (UIViewController *)viewControllerToPushForRow:(NSInteger)row { | ||||
|     return nil; | ||||
| } | ||||
|  | ||||
| - (void (^)(__kindof UIViewController *))didSelectRowAction:(NSInteger)row { | ||||
|     if (self.selectionHandler) { weakify(self) | ||||
|         return ^(UIViewController *host) { strongify(self) | ||||
|             if (self) { | ||||
|                 self.selectionHandler(host, self.filteredList[row]); | ||||
|             } | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     return nil; | ||||
| } | ||||
|  | ||||
| - (void)configureCell:(__kindof UITableViewCell *)cell forRow:(NSInteger)row { | ||||
|     self.configureCell(cell, self.filteredList[row], row); | ||||
| } | ||||
|  | ||||
| - (NSString *)reuseIdentifierForRow:(NSInteger)row { | ||||
|     if (self.cellRegistrationMapping.count) { | ||||
|         return self.cellRegistrationMapping.allKeys.firstObject; | ||||
|     } | ||||
|  | ||||
|     return [super reuseIdentifierForRow:row]; | ||||
| } | ||||
|  | ||||
| - (void)setCellRegistrationMapping:(NSDictionary<NSString *,Class> *)cellRegistrationMapping { | ||||
|     NSParameterAssert(cellRegistrationMapping.count <= 1); | ||||
|     _cellRegistrationMapping = cellRegistrationMapping; | ||||
| } | ||||
|  | ||||
| @end | ||||
							
								
								
									
										19
									
								
								Tweaks/FLEX/ObjectExplorers/Sections/FLEXObjectInfoSection.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								Tweaks/FLEX/ObjectExplorers/Sections/FLEXObjectInfoSection.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,19 @@ | ||||
| // | ||||
| //  FLEXObjectInfoSection.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 8/28/19. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import <Foundation/Foundation.h> | ||||
|  | ||||
| /// \c FLEXTableViewSection itself doesn't know about the object being explored. | ||||
| /// Subclasses might need this info to provide useful information about the object. Instead | ||||
| /// of adding an abstract class to the class hierarchy, subclasses can conform to this protocol | ||||
| /// to indicate that the only info they need to be initialized is the object being explored. | ||||
| @protocol FLEXObjectInfoSection <NSObject> | ||||
|  | ||||
| + (instancetype)forObject:(id)object; | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,19 @@ | ||||
| // | ||||
| // FLEXBlockShortcuts.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 1/30/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXShortcutsSection.h" | ||||
|  | ||||
| NS_ASSUME_NONNULL_BEGIN | ||||
|  | ||||
| /// Provides a description of the block's signature | ||||
| /// and access to an NSMethodSignature of the block | ||||
| @interface FLEXBlockShortcuts : FLEXShortcutsSection | ||||
|  | ||||
| @end | ||||
|  | ||||
| NS_ASSUME_NONNULL_END | ||||
| @@ -0,0 +1,59 @@ | ||||
| // | ||||
| // FLEXBlockShortcuts.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 1/30/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXBlockShortcuts.h" | ||||
| #import "FLEXShortcut.h" | ||||
| #import "FLEXBlockDescription.h" | ||||
| #import "FLEXObjectExplorerFactory.h" | ||||
|  | ||||
| #pragma mark -  | ||||
| @implementation FLEXBlockShortcuts | ||||
|  | ||||
| #pragma mark Overrides | ||||
|  | ||||
| + (instancetype)forObject:(id)block { | ||||
|     NSParameterAssert([block isKindOfClass:NSClassFromString(@"NSBlock")]); | ||||
|      | ||||
|     FLEXBlockDescription *blockInfo = [FLEXBlockDescription describing:block]; | ||||
|     NSMethodSignature *signature = blockInfo.signature; | ||||
|     NSArray *blockShortcutRows = @[blockInfo.summary]; | ||||
|      | ||||
|     if (signature) { | ||||
|         blockShortcutRows = @[ | ||||
|             blockInfo.summary, | ||||
|             blockInfo.sourceDeclaration, | ||||
|             signature.debugDescription, | ||||
|             [FLEXActionShortcut title:@"View Method Signature" | ||||
|                 subtitle:^NSString *(id block) { | ||||
|                     return signature.description ?: @"unsupported signature"; | ||||
|                 } | ||||
|                 viewer:^UIViewController *(id block) { | ||||
|                     return [FLEXObjectExplorerFactory explorerViewControllerForObject:signature]; | ||||
|                 } | ||||
|                 accessoryType:^UITableViewCellAccessoryType(id view) { | ||||
|                     if (signature) { | ||||
|                         return UITableViewCellAccessoryDisclosureIndicator; | ||||
|                     } | ||||
|                     return UITableViewCellAccessoryNone; | ||||
|                 } | ||||
|             ] | ||||
|         ]; | ||||
|     } | ||||
|      | ||||
|     return [self forObject:block additionalRows:blockShortcutRows]; | ||||
| } | ||||
|  | ||||
| - (NSString *)title { | ||||
|     return @"Metadata"; | ||||
| } | ||||
|  | ||||
| - (NSInteger)numberOfLines { | ||||
|     return 0; | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,18 @@ | ||||
| // | ||||
| //  FLEXBundleShortcuts.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 12/12/19. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXShortcutsSection.h" | ||||
|  | ||||
| NS_ASSUME_NONNULL_BEGIN | ||||
|  | ||||
| /// Provides a "Browse Bundle Directory" action | ||||
| @interface FLEXBundleShortcuts : FLEXShortcutsSection | ||||
|  | ||||
| @end | ||||
|  | ||||
| NS_ASSUME_NONNULL_END | ||||
| @@ -0,0 +1,114 @@ | ||||
| // | ||||
| //  FLEXBundleShortcuts.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 12/12/19. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXBundleShortcuts.h" | ||||
| #import "FLEXShortcut.h" | ||||
| #import "FLEXAlert.h" | ||||
| #import "FLEXMacros.h" | ||||
| #import "FLEXRuntimeExporter.h" | ||||
| #import "FLEXTableListViewController.h" | ||||
| #import "FLEXFileBrowserController.h" | ||||
|  | ||||
| #pragma mark - | ||||
| @implementation FLEXBundleShortcuts | ||||
| #pragma mark Overrides | ||||
|  | ||||
| + (instancetype)forObject:(NSBundle *)bundle { weakify(self) | ||||
|     return [self forObject:bundle additionalRows:@[ | ||||
|         [FLEXActionShortcut | ||||
|             title:@"Browse Bundle Directory" subtitle:nil | ||||
|             viewer:^UIViewController *(NSBundle *bundle) { | ||||
|                 return [FLEXFileBrowserController path:bundle.bundlePath]; | ||||
|             } | ||||
|             accessoryType:^UITableViewCellAccessoryType(NSBundle *bundle) { | ||||
|                 return UITableViewCellAccessoryDisclosureIndicator; | ||||
|             } | ||||
|         ], | ||||
|         [FLEXActionShortcut title:@"Browse Bundle as Database…" subtitle:nil | ||||
|             selectionHandler:^(UIViewController *host, NSBundle *bundle) { strongify(self) | ||||
|                 [self promptToExportBundleAsDatabase:bundle host:host]; | ||||
|             } | ||||
|             accessoryType:^UITableViewCellAccessoryType(NSBundle *bundle) { | ||||
|                 return UITableViewCellAccessoryDisclosureIndicator; | ||||
|             } | ||||
|         ], | ||||
|     ]]; | ||||
| } | ||||
|  | ||||
| + (void)promptToExportBundleAsDatabase:(NSBundle *)bundle host:(UIViewController *)host { | ||||
|     [FLEXAlert makeAlert:^(FLEXAlert *make) { | ||||
|         make.title(@"Save As…").message( | ||||
|             @"The database be saved in the Library folder. " | ||||
|             "Depending on the number of classes, it may take " | ||||
|             "10 minutes or more to finish exporting. 20,000 " | ||||
|             "classes takes about 7 minutes." | ||||
|         ); | ||||
|         make.configuredTextField(^(UITextField *field) { | ||||
|             field.placeholder = @"FLEXRuntimeExport.objc.db"; | ||||
|             field.text = [NSString stringWithFormat: | ||||
|                 @"%@.objc.db", bundle.executablePath.lastPathComponent | ||||
|             ]; | ||||
|         }); | ||||
|         make.button(@"Start").handler(^(NSArray<NSString *> *strings) { | ||||
|             [self browseBundleAsDatabase:bundle host:host name:strings[0]]; | ||||
|         }); | ||||
|         make.button(@"Cancel").cancelStyle(); | ||||
|     } showFrom:host]; | ||||
| } | ||||
|  | ||||
| + (void)browseBundleAsDatabase:(NSBundle *)bundle host:(UIViewController *)host name:(NSString *)name { | ||||
|     NSParameterAssert(name.length); | ||||
|  | ||||
|     UIAlertController *progress = [FLEXAlert makeAlert:^(FLEXAlert *make) { | ||||
|         make.title(@"Generating Database"); | ||||
|         // Some iOS version glitch out of there is | ||||
|         // no initial message and you add one later | ||||
|         make.message(@"…"); | ||||
|     }]; | ||||
|  | ||||
|     [host presentViewController:progress animated:YES completion:^{ | ||||
|         // Generate path to store db | ||||
|         NSString *path = [NSSearchPathForDirectoriesInDomains( | ||||
|             NSLibraryDirectory, NSUserDomainMask, YES | ||||
|         )[0] stringByAppendingPathComponent:name]; | ||||
|  | ||||
|         progress.message = [path stringByAppendingString:@"\n\nCreating database…"]; | ||||
|  | ||||
|         // Generate db and show progress | ||||
|         [FLEXRuntimeExporter createRuntimeDatabaseAtPath:path | ||||
|             forImages:@[bundle.executablePath] | ||||
|             progressHandler:^(NSString *status) { | ||||
|                 dispatch_async(dispatch_get_main_queue(), ^{ | ||||
|                     progress.message = [progress.message | ||||
|                         stringByAppendingFormat:@"\n%@", status | ||||
|                     ]; | ||||
|                     [progress.view setNeedsLayout]; | ||||
|                     [progress.view layoutIfNeeded]; | ||||
|                 }); | ||||
|             } completion:^(NSString *error) { | ||||
|                 // Display error if any | ||||
|                 if (error) { | ||||
|                     progress.title = @"Error"; | ||||
|                     progress.message = error; | ||||
|                     [progress addAction:[UIAlertAction | ||||
|                         actionWithTitle:@"OK" style:UIAlertActionStyleCancel handler:nil] | ||||
|                     ]; | ||||
|                 } | ||||
|                 // Browse database | ||||
|                 else { | ||||
|                     [progress dismissViewControllerAnimated:YES completion:nil]; | ||||
|                     [host.navigationController pushViewController:[ | ||||
|                         [FLEXTableListViewController alloc] initWithPath:path | ||||
|                     ] animated:YES]; | ||||
|                 } | ||||
|             } | ||||
|         ]; | ||||
|     }]; | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,17 @@ | ||||
| // | ||||
| //  FLEXClassShortcuts.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 11/22/19. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXShortcutsSection.h" | ||||
|  | ||||
| /// Provides handy shortcuts for class objects. | ||||
| /// This is the default section used for all class objects. | ||||
| @interface FLEXClassShortcuts : FLEXShortcutsSection | ||||
|  | ||||
| + (instancetype)forObject:(Class)cls; | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,73 @@ | ||||
| // | ||||
| //  FLEXClassShortcuts.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 11/22/19. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXClassShortcuts.h" | ||||
| #import "FLEXShortcut.h" | ||||
| #import "FLEXObjectExplorerFactory.h" | ||||
| #import "FLEXObjectListViewController.h" | ||||
| #import "NSObject+FLEX_Reflection.h" | ||||
|  | ||||
| @interface FLEXClassShortcuts () | ||||
| @property (nonatomic, readonly) Class cls; | ||||
| @end | ||||
|  | ||||
| @implementation FLEXClassShortcuts | ||||
|  | ||||
| + (instancetype)forObject:(Class)cls { | ||||
|     // These additional rows will appear at the beginning of the shortcuts section. | ||||
|     // The methods below are written in such a way that they will not interfere | ||||
|     // with properties/etc being registered alongside these | ||||
|     return [self forObject:cls additionalRows:@[ | ||||
|         [FLEXActionShortcut title:@"Find Live Instances" subtitle:nil | ||||
|             viewer:^UIViewController *(id obj) { | ||||
|                 return [FLEXObjectListViewController | ||||
|                     instancesOfClassWithName:NSStringFromClass(obj) | ||||
|                     retained:NO | ||||
|                 ]; | ||||
|             } | ||||
|             accessoryType:^UITableViewCellAccessoryType(id obj) { | ||||
|                 return UITableViewCellAccessoryDisclosureIndicator; | ||||
|             } | ||||
|         ], | ||||
|         [FLEXActionShortcut title:@"List Subclasses" subtitle:nil | ||||
|             viewer:^UIViewController *(id obj) { | ||||
|                 NSString *name = NSStringFromClass(obj); | ||||
|                 return [FLEXObjectListViewController subclassesOfClassWithName:name]; | ||||
|             } | ||||
|             accessoryType:^UITableViewCellAccessoryType(id view) { | ||||
|                 return UITableViewCellAccessoryDisclosureIndicator; | ||||
|             } | ||||
|         ], | ||||
|         [FLEXActionShortcut title:@"Explore Bundle for Class" | ||||
|             subtitle:^NSString *(id obj) { | ||||
|                 return [self shortNameForBundlePath:[NSBundle bundleForClass:obj].executablePath]; | ||||
|             } | ||||
|             viewer:^UIViewController *(id obj) { | ||||
|                 NSBundle *bundle = [NSBundle bundleForClass:obj]; | ||||
|                 return [FLEXObjectExplorerFactory explorerViewControllerForObject:bundle]; | ||||
|             } | ||||
|             accessoryType:^UITableViewCellAccessoryType(id view) { | ||||
|                 return UITableViewCellAccessoryDisclosureIndicator; | ||||
|             } | ||||
|         ], | ||||
|     ]]; | ||||
| } | ||||
|  | ||||
| + (NSString *)shortNameForBundlePath:(NSString *)imageName { | ||||
|     NSArray<NSString *> *components = [imageName componentsSeparatedByString:@"/"]; | ||||
|     if (components.count >= 2) { | ||||
|         return [NSString stringWithFormat:@"%@/%@", | ||||
|             components[components.count - 2], | ||||
|             components[components.count - 1] | ||||
|         ]; | ||||
|     } | ||||
|  | ||||
|     return imageName.lastPathComponent; | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,16 @@ | ||||
| // | ||||
| //  FLEXImageShortcuts.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 8/29/19. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXShortcutsSection.h" | ||||
|  | ||||
| /// Provides "view image" and "save image" shortcuts for UIImage objects | ||||
| @interface FLEXImageShortcuts : FLEXShortcutsSection | ||||
|  | ||||
| + (instancetype)forObject:(UIImage *)image; | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,68 @@ | ||||
| // | ||||
| //  FLEXImageShortcuts.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 8/29/19. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXImageShortcuts.h" | ||||
| #import "FLEXImagePreviewViewController.h" | ||||
| #import "FLEXShortcut.h" | ||||
| #import "FLEXAlert.h" | ||||
| #import "FLEXMacros.h" | ||||
|  | ||||
| @interface UIAlertController (FLEXImageShortcuts) | ||||
| - (void)flex_image:(UIImage *)image disSaveWithError:(NSError *)error :(void *)context; | ||||
| @end | ||||
|  | ||||
| @implementation FLEXImageShortcuts | ||||
|  | ||||
| #pragma mark - Overrides | ||||
|  | ||||
| + (instancetype)forObject:(UIImage *)image { | ||||
|     // These additional rows will appear at the beginning of the shortcuts section. | ||||
|     // The methods below are written in such a way that they will not interfere | ||||
|     // with properties/etc being registered alongside these | ||||
|     return [self forObject:image additionalRows:@[ | ||||
|         [FLEXActionShortcut title:@"View Image" subtitle:nil | ||||
|             viewer:^UIViewController *(id image) { | ||||
|                 return [FLEXImagePreviewViewController forImage:image]; | ||||
|             } | ||||
|             accessoryType:^UITableViewCellAccessoryType(id image) { | ||||
|                 return UITableViewCellAccessoryDisclosureIndicator; | ||||
|             } | ||||
|         ], | ||||
|         [FLEXActionShortcut title:@"Save Image" subtitle:nil | ||||
|             selectionHandler:^(UIViewController *host, id image) { | ||||
|                 // Present modal alerting user about saving | ||||
|                 UIAlertController *alert = [FLEXAlert makeAlert:^(FLEXAlert *make) { | ||||
|                     make.title(@"Saving Image…"); | ||||
|                 }]; | ||||
|                 [host presentViewController:alert animated:YES completion:nil]; | ||||
|              | ||||
|                 // Save the image | ||||
|                 UIImageWriteToSavedPhotosAlbum( | ||||
|                     image, alert, @selector(flex_image:disSaveWithError::), nil | ||||
|                 ); | ||||
|             } | ||||
|             accessoryType:^UITableViewCellAccessoryType(id image) { | ||||
|                 return UITableViewCellAccessoryDisclosureIndicator; | ||||
|             } | ||||
|         ] | ||||
|     ]]; | ||||
| } | ||||
|  | ||||
| @end | ||||
|  | ||||
|  | ||||
| @implementation UIAlertController (FLEXImageShortcuts) | ||||
|  | ||||
| - (void)flex_image:(UIImage *)image disSaveWithError:(NSError *)error :(void *)context { | ||||
|     self.title = @"Image Saved"; | ||||
|     flex_dispatch_after(1, dispatch_get_main_queue(), ^{ | ||||
|         [self dismissViewControllerAnimated:YES completion:nil]; | ||||
|     }); | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,15 @@ | ||||
| // | ||||
| //  FLEXLayerShortcuts.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 12/12/19. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXShortcutsSection.h" | ||||
|  | ||||
| @interface FLEXLayerShortcuts : FLEXShortcutsSection | ||||
|  | ||||
| + (instancetype)forObject:(CALayer *)layer; | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,28 @@ | ||||
| // | ||||
| //  FLEXLayerShortcuts.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 12/12/19. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXLayerShortcuts.h" | ||||
| #import "FLEXShortcut.h" | ||||
| #import "FLEXImagePreviewViewController.h" | ||||
|  | ||||
| @implementation FLEXLayerShortcuts | ||||
|  | ||||
| + (instancetype)forObject:(CALayer *)layer { | ||||
|     return [self forObject:layer additionalRows:@[ | ||||
|         [FLEXActionShortcut title:@"Preview Image" subtitle:nil | ||||
|             viewer:^UIViewController *(CALayer *layer) { | ||||
|                 return [FLEXImagePreviewViewController previewForLayer:layer]; | ||||
|             } | ||||
|             accessoryType:^UITableViewCellAccessoryType(CALayer *layer) { | ||||
|                 return CGRectIsEmpty(layer.bounds) ? UITableViewCellAccessoryNone : UITableViewCellAccessoryDisclosureIndicator; | ||||
|             } | ||||
|         ] | ||||
|     ]]; | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,13 @@ | ||||
| // | ||||
| //  FLEXNSDataShortcuts.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 3/29/21. | ||||
| // | ||||
|  | ||||
| #import "FLEXShortcutsSection.h" | ||||
|  | ||||
| /// Adds a "UTF-8 String" shortcut | ||||
| @interface FLEXNSDataShortcuts : FLEXShortcutsSection | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,48 @@ | ||||
| // | ||||
| //  FLEXNSDataShortcuts.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 3/29/21. | ||||
| // | ||||
|  | ||||
| #import "FLEXNSDataShortcuts.h" | ||||
| #import "FLEXObjectExplorerFactory.h" | ||||
| #import "FLEXShortcut.h" | ||||
|  | ||||
| @implementation FLEXNSDataShortcuts | ||||
|  | ||||
| + (instancetype)forObject:(NSData *)data { | ||||
|     NSString *string = [self stringForData:data]; | ||||
|      | ||||
|     return [self forObject:data additionalRows:@[ | ||||
|         [FLEXActionShortcut title:@"UTF-8 String" subtitle:^(NSData *object) { | ||||
|             return string.length ? string : (string ? | ||||
|                 @"Data is not a UTF8 String" : @"Empty string" | ||||
|             ); | ||||
|         } viewer:^UIViewController *(id object) { | ||||
|             return [FLEXObjectExplorerFactory explorerViewControllerForObject:string]; | ||||
|         } accessoryType:^UITableViewCellAccessoryType(NSData *object) { | ||||
|             if (string.length) { | ||||
|                 return UITableViewCellAccessoryDisclosureIndicator; | ||||
|             } | ||||
|              | ||||
|             return UITableViewCellAccessoryNone; | ||||
|         }] | ||||
|     ]]; | ||||
| } | ||||
|  | ||||
| + (NSString *)stringForData:(NSData *)data { | ||||
|     return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; | ||||
| } | ||||
|  | ||||
| @end | ||||
|  | ||||
| @interface NSData (Overrides) @end | ||||
| @implementation NSData (Overrides) | ||||
|  | ||||
| // This normally crashes | ||||
| - (NSUInteger)length { | ||||
|     return 0; | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,13 @@ | ||||
| // | ||||
| //  FLEXNSStringShortcuts.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 3/29/21. | ||||
| // | ||||
|  | ||||
| #import "FLEXShortcutsSection.h" | ||||
|  | ||||
| /// Adds a "UTF-8 Data" shortcut | ||||
| @interface FLEXNSStringShortcuts : FLEXShortcutsSection | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,29 @@ | ||||
| // | ||||
| //  FLEXNSStringShortcuts.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 3/29/21. | ||||
| // | ||||
|  | ||||
| #import "FLEXNSStringShortcuts.h" | ||||
| #import "FLEXObjectExplorerFactory.h" | ||||
| #import "FLEXShortcut.h" | ||||
|  | ||||
| @implementation FLEXNSStringShortcuts | ||||
|  | ||||
| + (instancetype)forObject:(NSString *)string { | ||||
|     NSUInteger length = [string lengthOfBytesUsingEncoding:NSUTF8StringEncoding]; | ||||
|     NSData *data = [NSData dataWithBytesNoCopy:(void *)string.UTF8String length:length freeWhenDone:NO]; | ||||
|      | ||||
|     return [self forObject:string additionalRows:@[ | ||||
|         [FLEXActionShortcut title:@"UTF-8 Data" subtitle:^NSString *(id _) { | ||||
|             return data.description; | ||||
|         } viewer:^UIViewController *(id _) { | ||||
|             return [FLEXObjectExplorerFactory explorerViewControllerForObject:data]; | ||||
|         } accessoryType:^UITableViewCellAccessoryType(id _) { | ||||
|             return UITableViewCellAccessoryDisclosureIndicator; | ||||
|         }] | ||||
|     ]]; | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,71 @@ | ||||
| // | ||||
| //  FLEXShortcut.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 12/10/19. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXObjectExplorer.h" | ||||
|  | ||||
| NS_ASSUME_NONNULL_BEGIN | ||||
|  | ||||
| /// Represents a row in a shortcut section. | ||||
| /// | ||||
| /// The purpsoe of this protocol is to allow delegating a small | ||||
| /// subset of the responsibilities of a \c FLEXShortcutsSection | ||||
| /// to another object, for a single arbitrary row. | ||||
| /// | ||||
| /// It is useful to make your own shortcuts to append/prepend | ||||
| /// them to the existing list of shortcuts for a class. | ||||
| @protocol FLEXShortcut <FLEXObjectExplorerItem> | ||||
|  | ||||
| - (nonnull  NSString *)titleWith:(id)object; | ||||
| - (nullable NSString *)subtitleWith:(id)object; | ||||
| - (nullable void (^)(UIViewController *host))didSelectActionWith:(id)object; | ||||
| /// Called when the row is selected | ||||
| - (nullable UIViewController *)viewerWith:(id)object; | ||||
| /// Basically, whether or not to show a detail disclosure indicator | ||||
| - (UITableViewCellAccessoryType)accessoryTypeWith:(id)object; | ||||
| /// If nil is returned, the default reuse identifier is used | ||||
| - (nullable NSString *)customReuseIdentifierWith:(id)object; | ||||
|  | ||||
| @optional | ||||
| /// Called when the (i) button is pressed if the accessory type includes it | ||||
| - (UIViewController *)editorWith:(id)object forSection:(FLEXTableViewSection *)section; | ||||
|  | ||||
| @end | ||||
|  | ||||
|  | ||||
| /// Provides default behavior for FLEX metadata objects. Also works in a limited way with strings. | ||||
| /// Used internally. If you wish to use this object, only pass in \c FLEX* metadata objects. | ||||
| @interface FLEXShortcut : NSObject <FLEXShortcut> | ||||
|  | ||||
| /// @param item An \c NSString or \c FLEX* metadata object. | ||||
| /// @note You may also pass a \c FLEXShortcut conforming object, | ||||
| /// and that object will be returned instead. | ||||
| + (id<FLEXShortcut>)shortcutFor:(id)item; | ||||
|  | ||||
| @end | ||||
|  | ||||
|  | ||||
| /// Provides a quick and dirty implementation of the \c FLEXShortcut protocol, | ||||
| /// allowing you to specify a static title and dynamic atttributes for everything else. | ||||
| /// The object passed into each block is the object passed to each \c FLEXShortcut method. | ||||
| /// | ||||
| /// Does not support the \c -editorWith: method. | ||||
| @interface FLEXActionShortcut : NSObject <FLEXShortcut> | ||||
|  | ||||
| + (instancetype)title:(NSString *)title | ||||
|              subtitle:(nullable NSString *(^)(id object))subtitleFuture | ||||
|                viewer:(nullable UIViewController *(^)(id object))viewerFuture | ||||
|         accessoryType:(nullable UITableViewCellAccessoryType(^)(id object))accessoryTypeFuture; | ||||
|  | ||||
| + (instancetype)title:(NSString *)title | ||||
|              subtitle:(nullable NSString *(^)(id object))subtitleFuture | ||||
|      selectionHandler:(nullable void (^)(UIViewController *host, id object))tapAction | ||||
|         accessoryType:(nullable UITableViewCellAccessoryType(^)(id object))accessoryTypeFuture; | ||||
|  | ||||
| @end | ||||
|  | ||||
| NS_ASSUME_NONNULL_END | ||||
							
								
								
									
										254
									
								
								Tweaks/FLEX/ObjectExplorers/Sections/Shortcuts/FLEXShortcut.m
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										254
									
								
								Tweaks/FLEX/ObjectExplorers/Sections/Shortcuts/FLEXShortcut.m
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,254 @@ | ||||
| // | ||||
| //  FLEXShortcut.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 12/10/19. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXShortcut.h" | ||||
| #import "FLEXProperty.h" | ||||
| #import "FLEXPropertyAttributes.h" | ||||
| #import "FLEXIvar.h" | ||||
| #import "FLEXMethod.h" | ||||
| #import "FLEXRuntime+UIKitHelpers.h" | ||||
| #import "FLEXObjectExplorerFactory.h" | ||||
| #import "FLEXFieldEditorViewController.h" | ||||
| #import "FLEXMethodCallingViewController.h" | ||||
| #import "FLEXMetadataSection.h" | ||||
| #import "FLEXTableView.h" | ||||
|  | ||||
|  | ||||
| #pragma mark - FLEXShortcut | ||||
|  | ||||
| @interface FLEXShortcut () { | ||||
|     id _item; | ||||
| } | ||||
|  | ||||
| @property (nonatomic, readonly) FLEXMetadataKind metadataKind; | ||||
| @property (nonatomic, readonly) FLEXProperty *property; | ||||
| @property (nonatomic, readonly) FLEXMethod *method; | ||||
| @property (nonatomic, readonly) FLEXIvar *ivar; | ||||
| @property (nonatomic, readonly) id<FLEXRuntimeMetadata> metadata; | ||||
| @end | ||||
|  | ||||
| @implementation FLEXShortcut | ||||
| @synthesize defaults = _defaults; | ||||
|  | ||||
| + (id<FLEXShortcut>)shortcutFor:(id)item { | ||||
|     if ([item conformsToProtocol:@protocol(FLEXShortcut)]) { | ||||
|         return item; | ||||
|     } | ||||
|      | ||||
|     FLEXShortcut *shortcut = [self new]; | ||||
|     shortcut->_item = item; | ||||
|  | ||||
|     if ([item isKindOfClass:[FLEXProperty class]]) { | ||||
|         if (shortcut.property.isClassProperty) { | ||||
|             shortcut->_metadataKind =  FLEXMetadataKindClassProperties; | ||||
|         } else { | ||||
|             shortcut->_metadataKind =  FLEXMetadataKindProperties; | ||||
|         } | ||||
|     } | ||||
|     if ([item isKindOfClass:[FLEXIvar class]]) { | ||||
|         shortcut->_metadataKind = FLEXMetadataKindIvars; | ||||
|     } | ||||
|     if ([item isKindOfClass:[FLEXMethod class]]) { | ||||
|         // We don't care if it's a class method or not | ||||
|         shortcut->_metadataKind = FLEXMetadataKindMethods; | ||||
|     } | ||||
|  | ||||
|     return shortcut; | ||||
| } | ||||
|  | ||||
| - (id)propertyOrIvarValue:(id)object { | ||||
|     return [self.metadata currentValueWithTarget:object]; | ||||
| } | ||||
|  | ||||
| - (NSString *)titleWith:(id)object { | ||||
|     switch (self.metadataKind) { | ||||
|         case FLEXMetadataKindClassProperties: | ||||
|         case FLEXMetadataKindProperties: | ||||
|             // Since we're outside of the "properties" section, prepend @property for clarity. | ||||
|             return [@"@property " stringByAppendingString:[_item description]]; | ||||
|  | ||||
|         default: | ||||
|             return [_item description]; | ||||
|     } | ||||
|  | ||||
|     NSAssert( | ||||
|         [_item isKindOfClass:[NSString class]], | ||||
|         @"Unexpected type: %@", [_item class] | ||||
|     ); | ||||
|  | ||||
|     return _item; | ||||
| } | ||||
|  | ||||
| - (NSString *)subtitleWith:(id)object { | ||||
|     if (self.metadataKind) { | ||||
|         return [self.metadata previewWithTarget:object]; | ||||
|     } | ||||
|  | ||||
|     // Item is probably a string; must return empty string since | ||||
|     // these will be gathered into an array. If the object is a | ||||
|     // just a string, it doesn't get a subtitle. | ||||
|     return @""; | ||||
| } | ||||
|  | ||||
| - (void (^)(UIViewController *))didSelectActionWith:(id)object {  | ||||
|     return nil; | ||||
| } | ||||
|  | ||||
| - (UIViewController *)viewerWith:(id)object { | ||||
|     NSAssert(self.metadataKind, @"Static titles cannot be viewed"); | ||||
|     return [self.metadata viewerWithTarget:object]; | ||||
| } | ||||
|  | ||||
| - (UIViewController *)editorWith:(id)object forSection:(FLEXTableViewSection *)section { | ||||
|     NSAssert(self.metadataKind, @"Static titles cannot be edited"); | ||||
|     return [self.metadata editorWithTarget:object section:section]; | ||||
| } | ||||
|  | ||||
| - (UITableViewCellAccessoryType)accessoryTypeWith:(id)object { | ||||
|     if (self.metadataKind) { | ||||
|         return [self.metadata suggestedAccessoryTypeWithTarget:object]; | ||||
|     } | ||||
|  | ||||
|     return UITableViewCellAccessoryNone; | ||||
| } | ||||
|  | ||||
| - (NSString *)customReuseIdentifierWith:(id)object { | ||||
|     if (self.metadataKind) { | ||||
|         return kFLEXCodeFontCell; | ||||
|     } | ||||
|  | ||||
|     return kFLEXMultilineCell; | ||||
| } | ||||
|  | ||||
| #pragma mark FLEXObjectExplorerDefaults | ||||
|  | ||||
| - (void)setDefaults:(FLEXObjectExplorerDefaults *)defaults { | ||||
|     _defaults = defaults; | ||||
|      | ||||
|     if (_metadataKind) { | ||||
|         self.metadata.defaults = defaults; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (BOOL)isEditable { | ||||
|     if (_metadataKind) { | ||||
|         return self.metadata.isEditable; | ||||
|     } | ||||
|      | ||||
|     return NO; | ||||
| } | ||||
|  | ||||
| - (BOOL)isCallable { | ||||
|     if (_metadataKind) { | ||||
|         return self.metadata.isCallable; | ||||
|     } | ||||
|      | ||||
|     return NO; | ||||
| } | ||||
|  | ||||
| #pragma mark - Helpers | ||||
|  | ||||
| - (FLEXProperty *)property { return _item; } | ||||
| - (FLEXMethodBase *)method { return _item; } | ||||
| - (FLEXIvar *)ivar { return _item; } | ||||
| - (id<FLEXRuntimeMetadata>)metadata { return _item; } | ||||
|  | ||||
| @end | ||||
|  | ||||
|  | ||||
| #pragma mark - FLEXActionShortcut | ||||
|  | ||||
| @interface FLEXActionShortcut () | ||||
| @property (nonatomic, readonly) NSString *title; | ||||
| @property (nonatomic, readonly) NSString *(^subtitleFuture)(id); | ||||
| @property (nonatomic, readonly) UIViewController *(^viewerFuture)(id); | ||||
| @property (nonatomic, readonly) void (^selectionHandler)(UIViewController *, id); | ||||
| @property (nonatomic, readonly) UITableViewCellAccessoryType (^accessoryTypeFuture)(id); | ||||
| @end | ||||
|  | ||||
| @implementation FLEXActionShortcut | ||||
| @synthesize defaults = _defaults; | ||||
|  | ||||
| + (instancetype)title:(NSString *)title | ||||
|              subtitle:(NSString *(^)(id))subtitle | ||||
|                viewer:(UIViewController *(^)(id))viewer | ||||
|         accessoryType:(UITableViewCellAccessoryType (^)(id))type { | ||||
|     return [[self alloc] initWithTitle:title subtitle:subtitle viewer:viewer selectionHandler:nil accessoryType:type]; | ||||
| } | ||||
|  | ||||
| + (instancetype)title:(NSString *)title | ||||
|              subtitle:(NSString * (^)(id))subtitle | ||||
|      selectionHandler:(void (^)(UIViewController *, id))tapAction | ||||
|         accessoryType:(UITableViewCellAccessoryType (^)(id))type { | ||||
|     return [[self alloc] initWithTitle:title subtitle:subtitle viewer:nil selectionHandler:tapAction accessoryType:type]; | ||||
| } | ||||
|  | ||||
| - (id)initWithTitle:(NSString *)title | ||||
|            subtitle:(id)subtitleFuture | ||||
|              viewer:(id)viewerFuture | ||||
|    selectionHandler:(id)tapAction | ||||
|       accessoryType:(id)accessoryTypeFuture { | ||||
|     NSParameterAssert(title.length); | ||||
|  | ||||
|     self = [super init]; | ||||
|     if (self) { | ||||
|         id nilBlock = ^id (id obj) { return nil; }; | ||||
|          | ||||
|         _title = title; | ||||
|         _subtitleFuture = subtitleFuture ?: nilBlock; | ||||
|         _viewerFuture = viewerFuture ?: nilBlock; | ||||
|         _selectionHandler = tapAction; | ||||
|         _accessoryTypeFuture = accessoryTypeFuture ?: nilBlock; | ||||
|     } | ||||
|  | ||||
|     return self; | ||||
| } | ||||
|  | ||||
| - (NSString *)titleWith:(id)object { | ||||
|     return self.title; | ||||
| } | ||||
|  | ||||
| - (NSString *)subtitleWith:(id)object { | ||||
|     if (self.defaults.wantsDynamicPreviews) { | ||||
|         return self.subtitleFuture(object); | ||||
|     } | ||||
|      | ||||
|     return nil; | ||||
| } | ||||
|  | ||||
| - (void (^)(UIViewController *))didSelectActionWith:(id)object { | ||||
|     if (self.selectionHandler) { | ||||
|         return ^(UIViewController *host) { | ||||
|             self.selectionHandler(host, object); | ||||
|         }; | ||||
|     } | ||||
|      | ||||
|     return nil; | ||||
| } | ||||
|  | ||||
| - (UIViewController *)viewerWith:(id)object { | ||||
|     return self.viewerFuture(object); | ||||
| } | ||||
|  | ||||
| - (UITableViewCellAccessoryType)accessoryTypeWith:(id)object { | ||||
|     return self.accessoryTypeFuture(object); | ||||
| } | ||||
|  | ||||
| - (NSString *)customReuseIdentifierWith:(id)object { | ||||
|     if (!self.subtitleFuture(object)) { | ||||
|         // The text is more centered with this style if there is no subtitle | ||||
|         return kFLEXDefaultCell; | ||||
|     } | ||||
|  | ||||
|     return nil; | ||||
| } | ||||
|  | ||||
| - (BOOL)isEditable { return NO; } | ||||
| - (BOOL)isCallable { return NO; } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,33 @@ | ||||
| // | ||||
| //  FLEXShortcutsFactory+Defaults.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 8/29/19. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXShortcutsSection.h" | ||||
|  | ||||
| @interface FLEXShortcutsFactory (UIApplication) @end | ||||
|  | ||||
| @interface FLEXShortcutsFactory (Views) @end | ||||
|  | ||||
| @interface FLEXShortcutsFactory (ViewControllers) @end | ||||
|  | ||||
| @interface FLEXShortcutsFactory (UIImage) @end | ||||
|  | ||||
| @interface FLEXShortcutsFactory (NSBundle) @end | ||||
|  | ||||
| @interface FLEXShortcutsFactory (Classes) @end | ||||
|  | ||||
| @interface FLEXShortcutsFactory (Activities) @end | ||||
|  | ||||
| @interface FLEXShortcutsFactory (Blocks) @end | ||||
|  | ||||
| @interface FLEXShortcutsFactory (Foundation) @end | ||||
|  | ||||
| @interface FLEXShortcutsFactory (WebKit_Safari) @end | ||||
|  | ||||
| @interface FLEXShortcutsFactory (Pasteboard) @end | ||||
|  | ||||
| @interface FLEXShortcutsFactory (FirebaseFirestore) @end | ||||
| @@ -0,0 +1,461 @@ | ||||
| // | ||||
| //  FLEXShortcutsFactory+Defaults.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 8/29/19. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXShortcutsFactory+Defaults.h" | ||||
| #import "FLEXShortcut.h" | ||||
| #import "FLEXMacros.h" | ||||
| #import "FLEXRuntimeUtility.h" | ||||
| #import "NSArray+FLEX.h" | ||||
| #import "NSObject+FLEX_Reflection.h" | ||||
| #import "FLEXObjcInternal.h" | ||||
| #import "Cocoa+FLEXShortcuts.h" | ||||
|  | ||||
| #pragma mark - UIApplication | ||||
|  | ||||
| @implementation FLEXShortcutsFactory (UIApplication) | ||||
|  | ||||
| + (void)load { FLEX_EXIT_IF_NO_CTORS() | ||||
|     // sharedApplication class property possibly not added | ||||
|     // as a literal class property until iOS 10 | ||||
|     FLEXRuntimeUtilityTryAddObjectProperty( | ||||
|         2, sharedApplication, UIApplication.flex_metaclass, UIApplication, PropertyKey(ReadOnly) | ||||
|     ); | ||||
|      | ||||
|     self.append.classProperties(@[@"sharedApplication"]).forClass(UIApplication.flex_metaclass); | ||||
|     self.append.properties(@[ | ||||
|         @"delegate", @"keyWindow", @"windows" | ||||
|     ]).forClass(UIApplication.class); | ||||
|  | ||||
|     if (@available(iOS 13, *)) { | ||||
|         self.append.properties(@[ | ||||
|             @"connectedScenes", @"openSessions", @"supportsMultipleScenes" | ||||
|         ]).forClass(UIApplication.class); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @end | ||||
|  | ||||
| #pragma mark - Views | ||||
|  | ||||
| @implementation FLEXShortcutsFactory (Views) | ||||
|  | ||||
| + (void)load { FLEX_EXIT_IF_NO_CTORS() | ||||
|     // A quirk of UIView and some other classes: a lot of the `@property`s are | ||||
|     // not actually properties from the perspective of the runtime. | ||||
|     // | ||||
|     // We add these properties to the class at runtime if they haven't been added yet. | ||||
|     // This way, we can use our property editor to access and change them. | ||||
|     // The property attributes match the declared attributes in their headers. | ||||
|  | ||||
|     // UIView, public | ||||
|     Class UIView_ = UIView.class; | ||||
|     FLEXRuntimeUtilityTryAddNonatomicProperty(2, frame, UIView_, CGRect); | ||||
|     FLEXRuntimeUtilityTryAddNonatomicProperty(2, alpha, UIView_, CGFloat); | ||||
|     FLEXRuntimeUtilityTryAddNonatomicProperty(2, clipsToBounds, UIView_, BOOL); | ||||
|     FLEXRuntimeUtilityTryAddNonatomicProperty(2, opaque, UIView_, BOOL, PropertyKeyGetter(isOpaque)); | ||||
|     FLEXRuntimeUtilityTryAddNonatomicProperty(2, hidden, UIView_, BOOL, PropertyKeyGetter(isHidden)); | ||||
|     FLEXRuntimeUtilityTryAddObjectProperty(2, backgroundColor, UIView_, UIColor, PropertyKey(Copy)); | ||||
|     FLEXRuntimeUtilityTryAddObjectProperty(6, constraints, UIView_, NSArray, PropertyKey(ReadOnly)); | ||||
|     FLEXRuntimeUtilityTryAddObjectProperty(2, subviews, UIView_, NSArray, PropertyKey(ReadOnly)); | ||||
|     FLEXRuntimeUtilityTryAddObjectProperty(2, superview, UIView_, UIView, PropertyKey(ReadOnly)); | ||||
|     FLEXRuntimeUtilityTryAddObjectProperty(7, tintColor, UIView_, UIView); | ||||
|  | ||||
|     // UIButton, private | ||||
|     FLEXRuntimeUtilityTryAddObjectProperty(2, font, UIButton.class, UIFont, PropertyKey(ReadOnly)); | ||||
|      | ||||
|     // Only available since iOS 3.2, but we never supported iOS 3, so who cares | ||||
|     NSArray *ivars = @[@"_gestureRecognizers"]; | ||||
|     NSArray *methods = @[@"sizeToFit", @"setNeedsLayout", @"removeFromSuperview"]; | ||||
|  | ||||
|     // UIView | ||||
|     self.append.ivars(ivars).methods(methods).properties(@[ | ||||
|         @"frame", @"bounds", @"center", @"transform", | ||||
|         @"backgroundColor", @"alpha", @"opaque", @"hidden", | ||||
|         @"clipsToBounds", @"userInteractionEnabled", @"layer", | ||||
|         @"superview", @"subviews", | ||||
|         @"accessibilityIdentifier", @"accessibilityLabel" | ||||
|     ]).forClass(UIView.class); | ||||
|  | ||||
|     // UILabel | ||||
|     self.append.ivars(ivars).methods(methods).properties(@[ | ||||
|         @"text", @"attributedText", @"font", @"frame", | ||||
|         @"textColor", @"textAlignment", @"numberOfLines", | ||||
|         @"lineBreakMode", @"enabled", @"backgroundColor", | ||||
|         @"alpha", @"hidden", @"preferredMaxLayoutWidth", | ||||
|         @"superview", @"subviews", | ||||
|         @"accessibilityIdentifier", @"accessibilityLabel" | ||||
|     ]).forClass(UILabel.class); | ||||
|  | ||||
|     // UIWindow | ||||
|     self.append.ivars(ivars).properties(@[ | ||||
|         @"rootViewController", @"windowLevel", @"keyWindow", | ||||
|         @"frame", @"bounds", @"center", @"transform", | ||||
|         @"backgroundColor", @"alpha", @"opaque", @"hidden", | ||||
|         @"clipsToBounds", @"userInteractionEnabled", @"layer", | ||||
|         @"subviews" | ||||
|     ]).forClass(UIWindow.class); | ||||
|  | ||||
|     if (@available(iOS 13, *)) { | ||||
|         self.append.properties(@[@"windowScene"]).forClass(UIWindow.class); | ||||
|     } | ||||
|  | ||||
|     ivars = @[@"_targetActions", @"_gestureRecognizers"]; | ||||
|      | ||||
|     // Property was added in iOS 10 but we want it on iOS 9 too | ||||
|     FLEXRuntimeUtilityTryAddObjectProperty(9, allTargets, UIControl.class, NSArray, PropertyKey(ReadOnly)); | ||||
|  | ||||
|     // UIControl | ||||
|     self.append.ivars(ivars).methods(methods).properties(@[ | ||||
|         @"enabled", @"allTargets", @"frame", | ||||
|         @"backgroundColor", @"hidden", @"clipsToBounds", | ||||
|         @"userInteractionEnabled", @"superview", @"subviews", | ||||
|         @"accessibilityIdentifier", @"accessibilityLabel" | ||||
|     ]).forClass(UIControl.class); | ||||
|  | ||||
|     // UIButton | ||||
|     self.append.ivars(ivars).properties(@[ | ||||
|         @"titleLabel", @"font", @"imageView", @"tintColor", | ||||
|         @"currentTitle", @"currentImage", @"enabled", @"frame", | ||||
|         @"superview", @"subviews", | ||||
|         @"accessibilityIdentifier", @"accessibilityLabel" | ||||
|     ]).forClass(UIButton.class); | ||||
|      | ||||
|     // UIImageView | ||||
|     self.append.properties(@[ | ||||
|         @"image", @"animationImages", @"frame", @"bounds", @"center", | ||||
|         @"transform", @"alpha", @"hidden", @"clipsToBounds", | ||||
|         @"userInteractionEnabled", @"layer", @"superview", @"subviews", | ||||
|         @"accessibilityIdentifier", @"accessibilityLabel" | ||||
|     ]).forClass(UIImageView.class); | ||||
| } | ||||
|  | ||||
| @end | ||||
|  | ||||
|  | ||||
| #pragma mark - View Controllers | ||||
|  | ||||
| @implementation FLEXShortcutsFactory (ViewControllers) | ||||
|  | ||||
| + (void)load { FLEX_EXIT_IF_NO_CTORS() | ||||
|     // toolbarItems is not really a property, make it one  | ||||
|     FLEXRuntimeUtilityTryAddObjectProperty(3, toolbarItems, UIViewController.class, NSArray); | ||||
|      | ||||
|     // UIViewController | ||||
|     self.append | ||||
|         .properties(@[ | ||||
|             @"viewIfLoaded", @"title", @"navigationItem", @"toolbarItems", @"tabBarItem", | ||||
|             @"childViewControllers", @"navigationController", @"tabBarController", @"splitViewController", | ||||
|             @"parentViewController", @"presentedViewController", @"presentingViewController", | ||||
|         ]) | ||||
|         .methods(@[@"view"]) | ||||
|         .forClass(UIViewController.class); | ||||
|      | ||||
|     // UIAlertController | ||||
|     NSMutableArray *alertControllerProps = @[ | ||||
|         @"title", @"message", @"actions", @"textFields", | ||||
|         @"preferredAction", @"presentingViewController", @"viewIfLoaded", | ||||
|     ].mutableCopy; | ||||
|     if (@available(iOS 14.0, *)) { | ||||
|         [alertControllerProps insertObject:@"image" atIndex:4]; | ||||
|     } | ||||
|     self.append | ||||
|         .properties(alertControllerProps) | ||||
|         .methods(@[@"addAction:"]) | ||||
|         .forClass(UIAlertController.class); | ||||
|     self.append.properties(@[ | ||||
|         @"title", @"style", @"enabled", @"flex_styleName", | ||||
|         @"image", @"keyCommandInput", @"_isPreferred", @"_alertController", | ||||
|     ]).forClass(UIAlertAction.class); | ||||
| } | ||||
|  | ||||
| @end | ||||
|  | ||||
|  | ||||
| #pragma mark - UIImage | ||||
|  | ||||
| @implementation FLEXShortcutsFactory (UIImage) | ||||
|  | ||||
| + (void)load { FLEX_EXIT_IF_NO_CTORS() | ||||
|     self.append.methods(@[ | ||||
|         @"CGImage", @"CIImage" | ||||
|     ]).properties(@[ | ||||
|         @"scale", @"size", @"capInsets", | ||||
|         @"alignmentRectInsets", @"duration", @"images" | ||||
|     ]).forClass(UIImage.class); | ||||
|  | ||||
|     if (@available(iOS 13, *)) { | ||||
|         self.append.properties(@[@"symbolImage"]).forClass(UIImage.class); | ||||
|     } | ||||
| } | ||||
|  | ||||
| @end | ||||
|  | ||||
|  | ||||
| #pragma mark - NSBundle | ||||
|  | ||||
| @implementation FLEXShortcutsFactory (NSBundle) | ||||
|  | ||||
| + (void)load { FLEX_EXIT_IF_NO_CTORS() | ||||
|     self.append.properties(@[ | ||||
|         @"bundleIdentifier", @"principalClass", | ||||
|         @"infoDictionary", @"bundlePath", | ||||
|         @"executablePath", @"loaded" | ||||
|     ]).forClass(NSBundle.class); | ||||
| } | ||||
|  | ||||
| @end | ||||
|  | ||||
|  | ||||
| #pragma mark - Classes | ||||
|  | ||||
| @implementation FLEXShortcutsFactory (Classes) | ||||
|  | ||||
| + (void)load { FLEX_EXIT_IF_NO_CTORS() | ||||
|     self.append.classMethods(@[@"new", @"alloc"]).forClass(NSObject.flex_metaclass); | ||||
| } | ||||
|  | ||||
| @end | ||||
|  | ||||
|  | ||||
| #pragma mark - Activities | ||||
|  | ||||
| @implementation FLEXShortcutsFactory (Activities) | ||||
|  | ||||
| + (void)load { FLEX_EXIT_IF_NO_CTORS() | ||||
|     // Property was added in iOS 10 but we want it on iOS 9 too | ||||
|     FLEXRuntimeUtilityTryAddNonatomicProperty(9, item, UIActivityItemProvider.class, id, PropertyKey(ReadOnly)); | ||||
|      | ||||
|     self.append.properties(@[ | ||||
|         @"item", @"placeholderItem", @"activityType" | ||||
|     ]).forClass(UIActivityItemProvider.class); | ||||
|  | ||||
|     self.append.properties(@[ | ||||
|         @"activityItems", @"applicationActivities", @"excludedActivityTypes", @"completionHandler" | ||||
|     ]).forClass(UIActivityViewController.class); | ||||
| } | ||||
|  | ||||
| @end | ||||
|  | ||||
|  | ||||
| #pragma mark - Blocks | ||||
|  | ||||
| @implementation FLEXShortcutsFactory (Blocks) | ||||
|  | ||||
| + (void)load { FLEX_EXIT_IF_NO_CTORS() | ||||
|     self.append.methods(@[@"invoke"]).forClass(NSClassFromString(@"NSBlock")); | ||||
| } | ||||
|  | ||||
| @end | ||||
|  | ||||
| #pragma mark - Foundation | ||||
|  | ||||
| @implementation FLEXShortcutsFactory (Foundation) | ||||
|  | ||||
| + (void)load { FLEX_EXIT_IF_NO_CTORS() | ||||
|     self.append.properties(@[ | ||||
|         @"configuration", @"delegate", @"delegateQueue", @"sessionDescription", | ||||
|     ]).methods(@[ | ||||
|         @"dataTaskWithURL:", @"finishTasksAndInvalidate", @"invalidateAndCancel", | ||||
|     ]).forClass(NSURLSession.class); | ||||
|      | ||||
|     self.append.methods(@[ | ||||
|         @"cachedResponseForRequest:", @"storeCachedResponse:forRequest:", | ||||
|         @"storeCachedResponse:forDataTask:", @"removeCachedResponseForRequest:", | ||||
|         @"removeCachedResponseForDataTask:", @"removeCachedResponsesSinceDate:", | ||||
|         @"removeAllCachedResponses", | ||||
|     ]).forClass(NSURLCache.class); | ||||
|      | ||||
|      | ||||
|     self.append.methods(@[ | ||||
|         @"postNotification:", @"postNotificationName:object:userInfo:", | ||||
|         @"addObserver:selector:name:object:", @"removeObserver:", | ||||
|         @"removeObserver:name:object:", | ||||
|     ]).forClass(NSNotificationCenter.class); | ||||
|      | ||||
|     // NSTimeZone class properties aren't real properties | ||||
|     FLEXRuntimeUtilityTryAddObjectProperty(2, localTimeZone, NSTimeZone.flex_metaclass, NSTimeZone); | ||||
|     FLEXRuntimeUtilityTryAddObjectProperty(2, systemTimeZone, NSTimeZone.flex_metaclass, NSTimeZone); | ||||
|     FLEXRuntimeUtilityTryAddObjectProperty(2, defaultTimeZone, NSTimeZone.flex_metaclass, NSTimeZone); | ||||
|     FLEXRuntimeUtilityTryAddObjectProperty(2, knownTimeZoneNames, NSTimeZone.flex_metaclass, NSArray); | ||||
|     FLEXRuntimeUtilityTryAddObjectProperty(2, abbreviationDictionary, NSTimeZone.flex_metaclass, NSDictionary); | ||||
|      | ||||
|     self.append.classMethods(@[ | ||||
|         @"timeZoneWithName:", @"timeZoneWithAbbreviation:", @"timeZoneForSecondsFromGMT:", | ||||
|     ]).forClass(NSTimeZone.flex_metaclass); | ||||
|      | ||||
|     self.append.classProperties(@[ | ||||
|         @"defaultTimeZone", @"systemTimeZone", @"localTimeZone", | ||||
|     ]).forClass(NSTimeZone.class); | ||||
|      | ||||
|     // UTF8String is not a real property under the hood | ||||
|     FLEXRuntimeUtilityTryAddNonatomicProperty(2, UTF8String, NSString.class, const char *, PropertyKey(ReadOnly)); | ||||
|      | ||||
|     self.append.properties(@[@"length"]).methods(@[@"characterAtIndex:"]).forClass(NSString.class); | ||||
|     self.append.methods(@[ | ||||
|         @"writeToFile:atomically:", @"subdataWithRange:", @"isEqualToData:", | ||||
|     ]).properties(@[ | ||||
|         @"length", @"bytes", | ||||
|     ]).forClass(NSData.class); | ||||
|      | ||||
|     self.append.classMethods(@[ | ||||
|         @"dataWithJSONObject:options:error:", | ||||
|         @"JSONObjectWithData:options:error:", | ||||
|         @"isValidJSONObject:", | ||||
|     ]).forClass(NSJSONSerialization.class); | ||||
|      | ||||
|     // NSArray | ||||
|     self.append.classMethods(@[ | ||||
|         @"arrayWithObject:", @"arrayWithContentsOfFile:" | ||||
|     ]).forClass(NSArray.flex_metaclass); | ||||
|     self.append.methods(@[ | ||||
|         @"valueForKeyPath:", @"subarrayWithRange:", | ||||
|         @"arrayByAddingObject:", @"arrayByAddingObjectsFromArray:", | ||||
|         @"filteredArrayUsingPredicate:", @"subarrayWithRange:", | ||||
|         @"containsObject:", @"objectAtIndex:", @"indexOfObject:", | ||||
|         @"makeObjectsPerformSelector:", @"makeObjectsPerformSelector:withObject:", | ||||
|         @"sortedArrayUsingSelector:", @"reverseObjectEnumerator", | ||||
|         @"isEqualToArray:", @"mutableCopy", | ||||
|     ]).forClass(NSArray.class); | ||||
|     // NSDictionary | ||||
|     self.append.methods(@[ | ||||
|         @"objectForKey:", @"valueForKeyPath:", | ||||
|         @"isEqualToDictionary:", @"mutableCopy", | ||||
|     ]).forClass(NSDictionary.class); | ||||
|     // NSSet | ||||
|     self.append.classMethods(@[ | ||||
|         @"setWithObject:", @"setWithArray:" | ||||
|     ]).forClass(NSSet.flex_metaclass); | ||||
|     self.append.methods(@[ | ||||
|         @"allObjects", @"valueForKeyPath:", @"containsObject:", | ||||
|         @"setByAddingObject:", @"setByAddingObjectsFromArray:", | ||||
|         @"filteredSetUsingPredicate:", @"isSubsetOfSet:", | ||||
|         @"makeObjectsPerformSelector:", @"makeObjectsPerformSelector:withObject:", | ||||
|         @"reverseObjectEnumerator", @"isEqualToSet:", @"mutableCopy", | ||||
|     ]).forClass(NSSet.class); | ||||
|      | ||||
|     // NSMutableArray | ||||
|     self.prepend.methods(@[ | ||||
|         @"addObject:", @"insertObject:atIndex:", @"addObjectsFromArray:",  | ||||
|         @"removeObject:", @"removeObjectAtIndex:", | ||||
|         @"removeObjectsInArray:", @"removeAllObjects",  | ||||
|         @"removeLastObject", @"filterUsingPredicate:", | ||||
|         @"sortUsingSelector:", @"copy", | ||||
|     ]).forClass(NSMutableArray.class); | ||||
|     // NSMutableDictionary | ||||
|     self.prepend.methods(@[ | ||||
|         @"setObject:forKey:", @"removeObjectForKey:", | ||||
|         @"removeAllObjects", @"removeObjectsForKeys:", @"copy", | ||||
|     ]).forClass(NSMutableDictionary.class); | ||||
|     // NSMutableSet | ||||
|     self.prepend.methods(@[ | ||||
|         @"addObject:", @"removeObject:", @"filterUsingPredicate:", | ||||
|         @"removeAllObjects", @"addObjectsFromArray:", | ||||
|         @"unionSet:", @"minusSet:", @"intersectSet:", @"copy" | ||||
|     ]).forClass(NSMutableSet.class); | ||||
|      | ||||
|     self.append.methods(@[@"nextObject", @"allObjects"]).forClass(NSEnumerator.class); | ||||
|      | ||||
|     self.append.properties(@[@"flex_observers"]).forClass(NSNotificationCenter.class); | ||||
| } | ||||
|  | ||||
| @end | ||||
|  | ||||
| #pragma mark - WebKit / Safari | ||||
|  | ||||
| @implementation FLEXShortcutsFactory (WebKit_Safari) | ||||
|  | ||||
| + (void)load { FLEX_EXIT_IF_NO_CTORS() | ||||
|     Class WKWebView = NSClassFromString(@"WKWebView"); | ||||
|     Class SafariVC = NSClassFromString(@"SFSafariViewController"); | ||||
|      | ||||
|     if (WKWebView) { | ||||
|         self.append.properties(@[ | ||||
|             @"configuration", @"scrollView", @"title", @"URL", | ||||
|             @"customUserAgent", @"navigationDelegate" | ||||
|         ]).methods(@[@"reload", @"stopLoading"]).forClass(WKWebView); | ||||
|     } | ||||
|      | ||||
|     if (SafariVC) { | ||||
|         self.append.properties(@[ | ||||
|             @"delegate" | ||||
|         ]).forClass(SafariVC); | ||||
|         if (@available(iOS 10.0, *)) { | ||||
|             self.append.properties(@[ | ||||
|                 @"preferredBarTintColor", @"preferredControlTintColor" | ||||
|             ]).forClass(SafariVC); | ||||
|         } | ||||
|         if (@available(iOS 11.0, *)) { | ||||
|             self.append.properties(@[ | ||||
|                 @"configuration", @"dismissButtonStyle" | ||||
|             ]).forClass(SafariVC); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| @end | ||||
|  | ||||
| #pragma mark - Pasteboard | ||||
|  | ||||
| @implementation FLEXShortcutsFactory (Pasteboard) | ||||
|  | ||||
| + (void)load { FLEX_EXIT_IF_NO_CTORS() | ||||
|     self.append.properties(@[ | ||||
|         @"name", @"numberOfItems", @"items", | ||||
|         @"string", @"image", @"color", @"URL", | ||||
|     ]).forClass(UIPasteboard.class); | ||||
| } | ||||
|  | ||||
| @end | ||||
|  | ||||
| @interface NSNotificationCenter (Observers) | ||||
| @property (readonly) NSArray<NSString *> *flex_observers; | ||||
| @end | ||||
|  | ||||
| @implementation NSNotificationCenter (Observers) | ||||
| - (id)flex_observers { | ||||
|     NSString *debug = self.debugDescription; | ||||
|     NSArray<NSString *> *observers = [debug componentsSeparatedByString:@"\n"]; | ||||
|     NSArray<NSArray<NSString *> *> *splitObservers = [observers flex_mapped:^id(NSString *entry, NSUInteger idx) { | ||||
|         return [entry componentsSeparatedByString:@","]; | ||||
|     }]; | ||||
|      | ||||
|     NSArray *names = [splitObservers flex_mapped:^id(NSArray<NSString *> *entry, NSUInteger idx) { | ||||
|         return entry[0]; | ||||
|     }]; | ||||
|     NSArray *objects = [splitObservers flex_mapped:^id(NSArray<NSString *> *entry, NSUInteger idx) { | ||||
|         if (entry.count < 2) return NSNull.null; | ||||
|         NSScanner *scanner = [NSScanner scannerWithString:entry[1]]; | ||||
|  | ||||
|         unsigned long long objectPointerValue; | ||||
|         if ([scanner scanHexLongLong:&objectPointerValue]) { | ||||
|             void *objectPointer = (void *)objectPointerValue; | ||||
|             if (FLEXPointerIsValidObjcObject(objectPointer)) | ||||
|                 return (__bridge id)(void *)objectPointer; | ||||
|         } | ||||
|          | ||||
|         return NSNull.null; | ||||
|     }]; | ||||
|      | ||||
|     return [NSArray flex_forEachUpTo:names.count map:^id(NSUInteger i) { | ||||
|         return @[names[i], objects[i]]; | ||||
|     }]; | ||||
| } | ||||
| @end | ||||
|  | ||||
| #pragma mark - Firebase Firestore | ||||
|  | ||||
| @implementation FLEXShortcutsFactory (FirebaseFirestore) | ||||
|  | ||||
| + (void)load { FLEX_EXIT_IF_NO_CTORS() | ||||
|     Class FIRDocumentSnap = NSClassFromString(@"FIRDocumentSnapshot"); | ||||
|     if (FIRDocumentSnap) { | ||||
|         FLEXRuntimeUtilityTryAddObjectProperty(2, data, FIRDocumentSnap, NSDictionary, PropertyKey(ReadOnly));         | ||||
|     } | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,134 @@ | ||||
| // | ||||
| //  FLEXShortcutsSection.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 8/29/19. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXTableViewSection.h" | ||||
| #import "FLEXObjectInfoSection.h" | ||||
| @class FLEXProperty, FLEXIvar, FLEXMethod; | ||||
|  | ||||
| NS_ASSUME_NONNULL_BEGIN | ||||
|  | ||||
| /// An abstract base class for custom object "shortcuts" where every | ||||
| /// row can possibly have some action. The section title is "Shortcuts". | ||||
| /// | ||||
| /// You should only subclass this class if you need simple shortcuts | ||||
| /// with plain titles and/or subtitles. This class will automatically | ||||
| /// configure each cell appropriately. Since this is intended as a | ||||
| /// static section, subclasses should only need to implement the | ||||
| /// \c viewControllerToPushForRow: and/or \c didSelectRowAction: methods. | ||||
| /// | ||||
| /// If you create the section using \c forObject:rows:numberOfLines: | ||||
| /// then it will provide a view controller from \c viewControllerToPushForRow: | ||||
| /// automatically for rows that are a property/ivar/method. | ||||
| @interface FLEXShortcutsSection : FLEXTableViewSection <FLEXObjectInfoSection> | ||||
|  | ||||
| /// Uses \c kFLEXDefaultCell | ||||
| + (instancetype)forObject:(id)objectOrClass rowTitles:(nullable NSArray<NSString *> *)titles; | ||||
| /// Uses \c kFLEXDetailCell for non-empty subtitles, otherwise uses \c kFLEXDefaultCell | ||||
| + (instancetype)forObject:(id)objectOrClass | ||||
|                 rowTitles:(nullable NSArray<NSString *> *)titles | ||||
|              rowSubtitles:(nullable NSArray<NSString *> *)subtitles; | ||||
|  | ||||
| /// Uses \c kFLEXDefaultCell for rows that are given a title, otherwise | ||||
| /// this uses \c kFLEXDetailCell for any other allowed object. | ||||
| /// | ||||
| /// The section provide a view controller from \c viewControllerToPushForRow: | ||||
| /// automatically for rows that are a property/ivar/method. | ||||
| /// | ||||
| /// @param rows A mixed array containing any of the following: | ||||
| /// - any \c FLEXShortcut conforming object | ||||
| /// - an \c NSString | ||||
| /// - a \c FLEXProperty | ||||
| /// - a \c FLEXIvar | ||||
| /// - a \c FLEXMethodBase (includes \c FLEXMethod of course) | ||||
| /// Passing one of the latter 3 will provide a shortcut to that property/ivar/method. | ||||
| + (instancetype)forObject:(id)objectOrClass rows:(nullable NSArray *)rows; | ||||
|  | ||||
| /// Same as \c forObject:rows: but the given rows are prepended | ||||
| /// to the shortcuts already registered for the object's class. | ||||
| /// \c forObject:rows: does not use the registered shortcuts at all. | ||||
| + (instancetype)forObject:(id)objectOrClass additionalRows:(nullable NSArray *)rows; | ||||
|  | ||||
| /// Calls into \c forObject:rows: using the registered shortcuts for the object's class. | ||||
| /// @return An empty section if the object has no shortcuts registered at all. | ||||
| + (instancetype)forObject:(id)objectOrClass; | ||||
|  | ||||
| /// Subclasses \e may override this to hide the disclosure indicator | ||||
| /// for some rows. It is shown for all rows by default, unless | ||||
| /// you initialize it with \c forObject:rowTitles:rowSubtitles: | ||||
| /// | ||||
| /// When you hide the disclosure indicator, the row is not selectable. | ||||
| - (UITableViewCellAccessoryType)accessoryTypeForRow:(NSInteger)row; | ||||
|  | ||||
| /// The number of lines for the title and subtitle labels. Defaults to 1. | ||||
| @property (nonatomic, readonly) NSInteger numberOfLines; | ||||
| /// The object used to initialize this section. | ||||
| @property (nonatomic, readonly) id object; | ||||
|  | ||||
| /// Whether dynamic subtitles should always be computed as a cell is configured. | ||||
| /// Defaults to NO. Has no effect on static subtitles that are passed explicitly. | ||||
| @property (nonatomic) BOOL cacheSubtitles; | ||||
|  | ||||
| /// Whether this shortcut section overrides the default section or not. | ||||
| /// Subclasses should not override this method. To provide a second | ||||
| /// section alongside the default shortcuts section, use \c forObject:rows: | ||||
| /// @return \c NO if initialized with \c forObject: or \c forObject:additionalRows: | ||||
| @property (nonatomic, readonly) BOOL isNewSection; | ||||
|  | ||||
| @end | ||||
|  | ||||
| @class FLEXShortcutsFactory; | ||||
| typedef FLEXShortcutsFactory *_Nonnull(^FLEXShortcutsFactoryNames)(NSArray *names); | ||||
| typedef void (^FLEXShortcutsFactoryTarget)(Class targetClass); | ||||
|  | ||||
| /// The block properties below are to be used like SnapKit or Masonry. | ||||
| /// \c FLEXShortcutsSection.append.properties(@[@"frame",@"bounds"]).forClass(UIView.class); | ||||
| /// | ||||
| /// To safely register your own classes at launch, subclass this class, | ||||
| /// override \c +load, and call the appropriate methods on \c self | ||||
| @interface FLEXShortcutsFactory : NSObject | ||||
|  | ||||
| /// Returns the list of all registered shortcuts for the given object in this order: | ||||
| /// Properties, ivars, methods. | ||||
| /// | ||||
| /// This method traverses up the object's class hierarchy until it finds | ||||
| /// something registered. This allows you to show different shortcuts for | ||||
| /// the same object in different parts of the class hierarchy. | ||||
| /// | ||||
| /// As an example, UIView may have a -layer shortcut registered. But if | ||||
| /// you're inspecting a UIControl, you may not care about the layer or other | ||||
| /// UIView-specific things; you might rather see the target-actions registered | ||||
| /// for this control, and so you would register that property or ivar to UIControl, | ||||
| /// And you would still be able to see the UIView-registered shorcuts by clicking | ||||
| /// on the UIView "lens" at the top the explorer view controller screen. | ||||
| + (NSArray *)shortcutsForObjectOrClass:(id)objectOrClass; | ||||
|  | ||||
| @property (nonatomic, readonly, class) FLEXShortcutsFactory *append; | ||||
| @property (nonatomic, readonly, class) FLEXShortcutsFactory *prepend; | ||||
| @property (nonatomic, readonly, class) FLEXShortcutsFactory *replace; | ||||
|  | ||||
| @property (nonatomic, readonly) FLEXShortcutsFactoryNames properties; | ||||
| /// Do not try to set \c classProperties at the same time as \c ivars or other instance things. | ||||
| @property (nonatomic, readonly) FLEXShortcutsFactoryNames classProperties; | ||||
| @property (nonatomic, readonly) FLEXShortcutsFactoryNames ivars; | ||||
| @property (nonatomic, readonly) FLEXShortcutsFactoryNames methods; | ||||
| /// Do not try to set \c classMethods at the same time as \c ivars or other instance things. | ||||
| @property (nonatomic, readonly) FLEXShortcutsFactoryNames classMethods; | ||||
|  | ||||
| /// Accepts the target class. If you pass a regular class object, | ||||
| /// shortcuts will appear on instances. If you pass a metaclass object, | ||||
| /// shortcuts will appear when exploring a class object. | ||||
| /// | ||||
| /// For example, some class method shortcuts are added to the NSObject meta | ||||
| /// class by default so that you can see +alloc and +new when exploring | ||||
| /// a class object. If you wanted these to show up when exploring | ||||
| /// instances you would pass them to the classMethods method above. | ||||
| @property (nonatomic, readonly) FLEXShortcutsFactoryTarget forClass; | ||||
|  | ||||
| @end | ||||
|  | ||||
| NS_ASSUME_NONNULL_END | ||||
| @@ -0,0 +1,482 @@ | ||||
| // | ||||
| //  FLEXShortcutsSection.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 8/29/19. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXShortcutsSection.h" | ||||
| #import "FLEXTableView.h" | ||||
| #import "FLEXTableViewCell.h" | ||||
| #import "FLEXUtility.h" | ||||
| #import "FLEXShortcut.h" | ||||
| #import "FLEXProperty.h" | ||||
| #import "FLEXPropertyAttributes.h" | ||||
| #import "FLEXIvar.h" | ||||
| #import "FLEXMethod.h" | ||||
| #import "FLEXRuntime+UIKitHelpers.h" | ||||
| #import "FLEXObjectExplorer.h" | ||||
|  | ||||
| #pragma mark Private | ||||
|  | ||||
| @interface FLEXShortcutsSection () | ||||
| @property (nonatomic, copy) NSArray<NSString *> *titles; | ||||
| @property (nonatomic, copy) NSArray<NSString *> *subtitles; | ||||
|  | ||||
| @property (nonatomic, copy) NSArray<NSString *> *allTitles; | ||||
| @property (nonatomic, copy) NSArray<NSString *> *allSubtitles; | ||||
|  | ||||
| // Shortcuts are not used if initialized with static titles and subtitles | ||||
| @property (nonatomic, copy) NSArray<id<FLEXShortcut>> *shortcuts; | ||||
| @property (nonatomic, readonly) NSArray<id<FLEXShortcut>> *allShortcuts; | ||||
| @end | ||||
|  | ||||
| @implementation FLEXShortcutsSection | ||||
| @synthesize isNewSection = _isNewSection; | ||||
|  | ||||
| #pragma mark Initialization | ||||
|  | ||||
| + (instancetype)forObject:(id)objectOrClass rowTitles:(NSArray<NSString *> *)titles { | ||||
|     return [self forObject:objectOrClass rowTitles:titles rowSubtitles:nil]; | ||||
| } | ||||
|  | ||||
| + (instancetype)forObject:(id)objectOrClass | ||||
|                 rowTitles:(NSArray<NSString *> *)titles | ||||
|              rowSubtitles:(NSArray<NSString *> *)subtitles { | ||||
|     return [[self alloc] initWithObject:objectOrClass titles:titles subtitles:subtitles]; | ||||
| } | ||||
|  | ||||
| + (instancetype)forObject:(id)objectOrClass rows:(NSArray *)rows { | ||||
|     return [[self alloc] initWithObject:objectOrClass rows:rows isNewSection:YES]; | ||||
| } | ||||
|  | ||||
| + (instancetype)forObject:(id)objectOrClass additionalRows:(NSArray *)toPrepend { | ||||
|     NSArray *rows = [FLEXShortcutsFactory shortcutsForObjectOrClass:objectOrClass]; | ||||
|     NSArray *allRows = [toPrepend arrayByAddingObjectsFromArray:rows] ?: rows; | ||||
|     return [[self alloc] initWithObject:objectOrClass rows:allRows isNewSection:NO]; | ||||
| } | ||||
|  | ||||
| + (instancetype)forObject:(id)objectOrClass { | ||||
|     return [self forObject:objectOrClass additionalRows:nil]; | ||||
| } | ||||
|  | ||||
| - (id)initWithObject:(id)object | ||||
|               titles:(NSArray<NSString *> *)titles | ||||
|            subtitles:(NSArray<NSString *> *)subtitles { | ||||
|  | ||||
|     NSParameterAssert(titles.count == subtitles.count || !subtitles); | ||||
|     NSParameterAssert(titles.count); | ||||
|  | ||||
|     self = [super init]; | ||||
|     if (self) { | ||||
|         _object = object; | ||||
|         _allTitles = titles.copy; | ||||
|         _allSubtitles = subtitles.copy; | ||||
|         _isNewSection = YES; | ||||
|         _numberOfLines = 1; | ||||
|     } | ||||
|  | ||||
|     return self; | ||||
| } | ||||
|  | ||||
| - (id)initWithObject:object rows:(NSArray *)rows isNewSection:(BOOL)newSection { | ||||
|     self = [super init]; | ||||
|     if (self) { | ||||
|         _object = object; | ||||
|         _isNewSection = newSection; | ||||
|          | ||||
|         _allShortcuts = [rows flex_mapped:^id(id obj, NSUInteger idx) { | ||||
|             return [FLEXShortcut shortcutFor:obj]; | ||||
|         }]; | ||||
|         _numberOfLines = 1; | ||||
|          | ||||
|         // Populate titles and subtitles | ||||
|         [self reloadData]; | ||||
|     } | ||||
|  | ||||
|     return self; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Public | ||||
|  | ||||
| - (void)setCacheSubtitles:(BOOL)cacheSubtitles { | ||||
|     if (_cacheSubtitles == cacheSubtitles) return; | ||||
|  | ||||
|     // cacheSubtitles only applies if we have shortcut objects | ||||
|     if (self.allShortcuts) { | ||||
|         _cacheSubtitles = cacheSubtitles; | ||||
|         [self reloadData]; | ||||
|     } else { | ||||
|         NSLog(@"Warning: setting 'cacheSubtitles' on a shortcut section with static subtitles"); | ||||
|     } | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Overrides | ||||
|  | ||||
| - (UITableViewCellAccessoryType)accessoryTypeForRow:(NSInteger)row { | ||||
|     if (_allShortcuts) { | ||||
|         return [self.shortcuts[row] accessoryTypeWith:self.object]; | ||||
|     } | ||||
|      | ||||
|     return UITableViewCellAccessoryNone; | ||||
| } | ||||
|  | ||||
| - (void)setFilterText:(NSString *)filterText { | ||||
|     super.filterText = filterText; | ||||
|  | ||||
|     NSAssert( | ||||
|         self.allTitles.count == self.allSubtitles.count, | ||||
|         @"Each title needs a (possibly empty) subtitle" | ||||
|     ); | ||||
|  | ||||
|     if (filterText.length) { | ||||
|         // Tally up indexes of titles and subtitles matching the filter | ||||
|         NSMutableIndexSet *filterMatches = [NSMutableIndexSet new]; | ||||
|         id filterBlock = ^BOOL(NSString *obj, NSUInteger idx) { | ||||
|             if ([obj localizedCaseInsensitiveContainsString:filterText]) { | ||||
|                 [filterMatches addIndex:idx]; | ||||
|                 return YES; | ||||
|             } | ||||
|  | ||||
|             return NO; | ||||
|         }; | ||||
|  | ||||
|         // Get all matching indexes, including subtitles | ||||
|         [self.allTitles flex_forEach:filterBlock]; | ||||
|         [self.allSubtitles flex_forEach:filterBlock]; | ||||
|         // Filter to matching indexes only | ||||
|         self.titles    = [self.allTitles objectsAtIndexes:filterMatches]; | ||||
|         self.subtitles = [self.allSubtitles objectsAtIndexes:filterMatches]; | ||||
|         self.shortcuts = [self.allShortcuts objectsAtIndexes:filterMatches]; | ||||
|     } else { | ||||
|         self.shortcuts = self.allShortcuts; | ||||
|         self.titles    = self.allTitles; | ||||
|         self.subtitles = [self.allSubtitles flex_filtered:^BOOL(NSString *sub, NSUInteger idx) { | ||||
|             return sub.length > 0; | ||||
|         }]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)reloadData { | ||||
|     [FLEXObjectExplorer configureDefaultsForItems:self.allShortcuts]; | ||||
|      | ||||
|     // Generate all (sub)titles from shortcuts | ||||
|     if (self.allShortcuts) { | ||||
|         self.allTitles = [self.allShortcuts flex_mapped:^id(id<FLEXShortcut> s, NSUInteger idx) { | ||||
|             return [s titleWith:self.object]; | ||||
|         }]; | ||||
|         self.allSubtitles = [self.allShortcuts flex_mapped:^id(id<FLEXShortcut> s, NSUInteger idx) { | ||||
|             return [s subtitleWith:self.object] ?: @""; | ||||
|         }]; | ||||
|     } | ||||
|  | ||||
|     // Re-generate filtered (sub)titles and shortcuts | ||||
|     self.filterText = self.filterText; | ||||
| } | ||||
|  | ||||
| - (NSString *)title { | ||||
|     return @"Shortcuts"; | ||||
| } | ||||
|  | ||||
| - (NSInteger)numberOfRows { | ||||
|     return self.titles.count; | ||||
| } | ||||
|  | ||||
| - (BOOL)canSelectRow:(NSInteger)row { | ||||
|     UITableViewCellAccessoryType type = [self.shortcuts[row] accessoryTypeWith:self.object]; | ||||
|     BOOL hasDisclosure = NO; | ||||
|     hasDisclosure |= type == UITableViewCellAccessoryDisclosureIndicator; | ||||
|     hasDisclosure |= type == UITableViewCellAccessoryDetailDisclosureButton; | ||||
|     return hasDisclosure; | ||||
| } | ||||
|  | ||||
| - (void (^)(__kindof UIViewController *))didSelectRowAction:(NSInteger)row { | ||||
|     return [self.shortcuts[row] didSelectActionWith:self.object]; | ||||
| } | ||||
|  | ||||
| - (UIViewController *)viewControllerToPushForRow:(NSInteger)row { | ||||
|     /// Nil if shortcuts is nil, i.e. if initialized with forObject:rowTitles:rowSubtitles: | ||||
|     return [self.shortcuts[row] viewerWith:self.object]; | ||||
| } | ||||
|  | ||||
| - (void (^)(__kindof UIViewController *))didPressInfoButtonAction:(NSInteger)row { | ||||
|     id<FLEXShortcut> shortcut = self.shortcuts[row]; | ||||
|     if ([shortcut respondsToSelector:@selector(editorWith:forSection:)]) { | ||||
|         id object = self.object; | ||||
|         return ^(UIViewController *host) { | ||||
|             UIViewController *editor = [shortcut editorWith:object forSection:self]; | ||||
|             [host.navigationController pushViewController:editor animated:YES]; | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     return nil; | ||||
| } | ||||
|  | ||||
| - (NSString *)reuseIdentifierForRow:(NSInteger)row { | ||||
|     FLEXTableViewCellReuseIdentifier defaultReuse = kFLEXDetailCell; | ||||
|     if (@available(iOS 11, *)) { | ||||
|         defaultReuse = kFLEXMultilineDetailCell; | ||||
|     } | ||||
|      | ||||
|     return [self.shortcuts[row] customReuseIdentifierWith:self.object] ?: defaultReuse; | ||||
| } | ||||
|  | ||||
| - (void)configureCell:(__kindof FLEXTableViewCell *)cell forRow:(NSInteger)row { | ||||
|     cell.titleLabel.text = [self titleForRow:row]; | ||||
|     cell.titleLabel.numberOfLines = self.numberOfLines; | ||||
|     cell.subtitleLabel.text = [self subtitleForRow:row]; | ||||
|     cell.subtitleLabel.numberOfLines = self.numberOfLines; | ||||
|     cell.accessoryType = [self accessoryTypeForRow:row]; | ||||
| } | ||||
|  | ||||
| - (NSString *)titleForRow:(NSInteger)row { | ||||
|     return self.titles[row]; | ||||
| } | ||||
|  | ||||
| - (NSString *)subtitleForRow:(NSInteger)row { | ||||
|     // Case: dynamic, uncached subtitles | ||||
|     if (!self.cacheSubtitles) { | ||||
|         NSString *subtitle = [self.shortcuts[row] subtitleWith:self.object]; | ||||
|         return subtitle.length ? subtitle : nil; | ||||
|     } | ||||
|  | ||||
|     // Case: static subtitles, or cached subtitles | ||||
|     return self.subtitles[row]; | ||||
| } | ||||
|  | ||||
| @end | ||||
|  | ||||
|  | ||||
| #pragma mark - Global shortcut registration | ||||
|  | ||||
| @interface FLEXShortcutsFactory () { | ||||
|     BOOL _append, _prepend, _replace, _notInstance; | ||||
|     NSArray<NSString *> *_properties, *_ivars, *_methods; | ||||
| } | ||||
| @end | ||||
|  | ||||
| #define NewAndSet(ivar) ({ FLEXShortcutsFactory *r = [self sharedFactory]; r->ivar = YES; r; }) | ||||
| #define SetIvar(ivar) ({ self->ivar = YES; self; }) | ||||
| #define SetParamBlock(ivar) ^(NSArray *p) { self->ivar = p; return self; } | ||||
|  | ||||
| typedef NSMutableDictionary<Class, NSMutableArray<id<FLEXRuntimeMetadata>> *> RegistrationBuckets; | ||||
|  | ||||
| @implementation FLEXShortcutsFactory { | ||||
|     // Class buckets | ||||
|     RegistrationBuckets *cProperties; | ||||
|     RegistrationBuckets *cIvars; | ||||
|     RegistrationBuckets *cMethods; | ||||
|     // Metaclass buckets | ||||
|     RegistrationBuckets *mProperties; | ||||
|     RegistrationBuckets *mMethods; | ||||
| } | ||||
|  | ||||
| + (instancetype)sharedFactory { | ||||
|     static FLEXShortcutsFactory *shared = nil; | ||||
|     static dispatch_once_t onceToken; | ||||
|     dispatch_once(&onceToken, ^{ | ||||
|         shared = [self new]; | ||||
|     }); | ||||
|      | ||||
|     return shared; | ||||
| } | ||||
|  | ||||
| - (id)init { | ||||
|     self = [super init]; | ||||
|     if (self) { | ||||
|         cProperties = [NSMutableDictionary new]; | ||||
|         cIvars = [NSMutableDictionary new]; | ||||
|         cMethods = [NSMutableDictionary new]; | ||||
|  | ||||
|         mProperties = [NSMutableDictionary new]; | ||||
|         mMethods = [NSMutableDictionary new]; | ||||
|     } | ||||
|      | ||||
|     return self; | ||||
| } | ||||
|  | ||||
| + (NSArray<id<FLEXRuntimeMetadata>> *)shortcutsForObjectOrClass:(id)objectOrClass { | ||||
|     return [[self sharedFactory] shortcutsForObjectOrClass:objectOrClass]; | ||||
| } | ||||
|  | ||||
| - (NSArray<id<FLEXRuntimeMetadata>> *)shortcutsForObjectOrClass:(id)objectOrClass { | ||||
|     NSParameterAssert(objectOrClass); | ||||
|  | ||||
|     NSMutableArray<id<FLEXRuntimeMetadata>> *shortcuts = [NSMutableArray new]; | ||||
|     BOOL isClass = object_isClass(objectOrClass); | ||||
|     // The -class does not give you a metaclass, and we want a metaclass | ||||
|     // if a class is passed in, or a class if an object is passed in | ||||
|     Class classKey = object_getClass(objectOrClass); | ||||
|      | ||||
|     RegistrationBuckets *propertyBucket = isClass ? mProperties : cProperties; | ||||
|     RegistrationBuckets *methodBucket = isClass ? mMethods : cMethods; | ||||
|     RegistrationBuckets *ivarBucket = isClass ? nil : cIvars; | ||||
|  | ||||
|     BOOL stop = NO; | ||||
|     while (!stop && classKey) { | ||||
|         NSArray *properties = propertyBucket[classKey]; | ||||
|         NSArray *ivars = ivarBucket[classKey]; | ||||
|         NSArray *methods = methodBucket[classKey]; | ||||
|  | ||||
|         // Stop if we found anything | ||||
|         stop = properties || ivars || methods; | ||||
|         if (stop) { | ||||
|             // Add things we found to the list | ||||
|             [shortcuts addObjectsFromArray:properties]; | ||||
|             [shortcuts addObjectsFromArray:ivars]; | ||||
|             [shortcuts addObjectsFromArray:methods]; | ||||
|         } else { | ||||
|             classKey = class_getSuperclass(classKey); | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     [FLEXObjectExplorer configureDefaultsForItems:shortcuts]; | ||||
|     return shortcuts; | ||||
| } | ||||
|  | ||||
| + (FLEXShortcutsFactory *)append { | ||||
|     return NewAndSet(_append); | ||||
| } | ||||
|  | ||||
| + (FLEXShortcutsFactory *)prepend { | ||||
|     return NewAndSet(_prepend); | ||||
| } | ||||
|  | ||||
| + (FLEXShortcutsFactory *)replace { | ||||
|     return NewAndSet(_replace); | ||||
| } | ||||
|  | ||||
| - (void)_register:(NSArray<id<FLEXRuntimeMetadata>> *)items to:(RegistrationBuckets *)global class:(Class)key { | ||||
|     @synchronized (self) { | ||||
|         // Get (or initialize) the bucket for this class | ||||
|         NSMutableArray *bucket = ({ | ||||
|             id bucket = global[key]; | ||||
|             if (!bucket) { | ||||
|                 bucket = [NSMutableArray new]; | ||||
|                 global[(id)key] = bucket; | ||||
|             } | ||||
|             bucket; | ||||
|         }); | ||||
|  | ||||
|         if (self->_append)  { [bucket addObjectsFromArray:items]; } | ||||
|         if (self->_replace) { [bucket setArray:items]; } | ||||
|         if (self->_prepend) { | ||||
|             if (bucket.count) { | ||||
|                 // Set new items as array, add old items behind them | ||||
|                 id copy = bucket.copy; | ||||
|                 [bucket setArray:items]; | ||||
|                 [bucket addObjectsFromArray:copy]; | ||||
|             } else { | ||||
|                 [bucket addObjectsFromArray:items]; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| - (void)reset { | ||||
|     _append = NO; | ||||
|     _prepend = NO; | ||||
|     _replace = NO; | ||||
|     _notInstance = NO; | ||||
|      | ||||
|     _properties = nil; | ||||
|     _ivars = nil; | ||||
|     _methods = nil; | ||||
| } | ||||
|  | ||||
| - (FLEXShortcutsFactory *)class { | ||||
|     return SetIvar(_notInstance); | ||||
| } | ||||
|  | ||||
| - (FLEXShortcutsFactoryNames)properties { | ||||
|     NSAssert(!_notInstance, @"Do not try to set properties+classProperties at the same time"); | ||||
|     return SetParamBlock(_properties); | ||||
| } | ||||
|  | ||||
| - (FLEXShortcutsFactoryNames)classProperties { | ||||
|     _notInstance = YES; | ||||
|     return SetParamBlock(_properties); | ||||
| } | ||||
|  | ||||
| - (FLEXShortcutsFactoryNames)ivars { | ||||
|     return SetParamBlock(_ivars); | ||||
| } | ||||
|  | ||||
| - (FLEXShortcutsFactoryNames)methods { | ||||
|     NSAssert(!_notInstance, @"Do not try to set methods+classMethods at the same time"); | ||||
|     return SetParamBlock(_methods); | ||||
| } | ||||
|  | ||||
| - (FLEXShortcutsFactoryNames)classMethods { | ||||
|     _notInstance = YES; | ||||
|     return SetParamBlock(_methods); | ||||
| } | ||||
|  | ||||
| - (FLEXShortcutsFactoryTarget)forClass { | ||||
|     return ^(Class cls) { | ||||
|         NSAssert( | ||||
|             ( self->_append && !self->_prepend && !self->_replace) || | ||||
|             (!self->_append &&  self->_prepend && !self->_replace) || | ||||
|             (!self->_append && !self->_prepend &&  self->_replace), | ||||
|             @"You can only do one of [append, prepend, replace]" | ||||
|         ); | ||||
|  | ||||
|          | ||||
|         /// Whether the metadata we're about to add is instance or | ||||
|         /// class metadata, i.e. class properties vs instance properties | ||||
|         BOOL instanceMetadata = !self->_notInstance; | ||||
|         /// Whether the given class is a metaclass or not; we need to switch to | ||||
|         /// the metaclass to add class metadata if we are given the normal class object | ||||
|         BOOL isMeta = class_isMetaClass(cls); | ||||
|         /// Whether the shortcuts we're about to add should appear for classes or instances | ||||
|         BOOL instanceShortcut = !isMeta; | ||||
|          | ||||
|         if (instanceMetadata) { | ||||
|             NSAssert(!isMeta, | ||||
|                 @"Instance metadata can only be added as an instance shortcut" | ||||
|             ); | ||||
|         } | ||||
|          | ||||
|         Class metaclass = isMeta ? cls : object_getClass(cls); | ||||
|         Class clsForMetadata = instanceMetadata ? cls : metaclass; | ||||
|          | ||||
|         // The factory is a singleton so we don't need to worry about "leaking" it | ||||
|         #pragma clang diagnostic push | ||||
|         #pragma clang diagnostic ignored "-Wimplicit-retain-self" | ||||
|          | ||||
|         RegistrationBuckets *propertyBucket = instanceShortcut ? cProperties : mProperties; | ||||
|         RegistrationBuckets *methodBucket = instanceShortcut ? cMethods : mMethods; | ||||
|         RegistrationBuckets *ivarBucket = instanceShortcut ? cIvars : nil; | ||||
|          | ||||
|         #pragma clang diagnostic pop | ||||
|  | ||||
|         if (self->_properties) { | ||||
|             NSArray *items = [self->_properties flex_mapped:^id(NSString *name, NSUInteger idx) { | ||||
|                 return [FLEXProperty named:name onClass:clsForMetadata]; | ||||
|             }]; | ||||
|             [self _register:items to:propertyBucket class:cls]; | ||||
|         } | ||||
|  | ||||
|         if (self->_methods) { | ||||
|             NSArray *items = [self->_methods flex_mapped:^id(NSString *name, NSUInteger idx) { | ||||
|                 return [FLEXMethod selector:NSSelectorFromString(name) class:clsForMetadata]; | ||||
|             }]; | ||||
|             [self _register:items to:methodBucket class:cls]; | ||||
|         } | ||||
|  | ||||
|         if (self->_ivars) { | ||||
|             NSAssert(instanceMetadata, @"Instance metadata can only be added as an instance shortcut (%@)", cls); | ||||
|             NSArray *items = [self->_ivars flex_mapped:^id(NSString *name, NSUInteger idx) { | ||||
|                 return [FLEXIvar named:name onClass:clsForMetadata]; | ||||
|             }]; | ||||
|             [self _register:items to:ivarBucket class:cls]; | ||||
|         } | ||||
|          | ||||
|         [self reset]; | ||||
|     }; | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,13 @@ | ||||
| // | ||||
| //  FLEXUIAppShortcuts.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 5/25/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXShortcutsSection.h" | ||||
|  | ||||
| @interface FLEXUIAppShortcuts : FLEXShortcutsSection | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,77 @@ | ||||
| // | ||||
| //  FLEXUIAppShortcuts.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner on 5/25/20. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXUIAppShortcuts.h" | ||||
| #import "FLEXRuntimeUtility.h" | ||||
| #import "FLEXShortcut.h" | ||||
| #import "FLEXAlert.h" | ||||
|  | ||||
| @implementation FLEXUIAppShortcuts | ||||
|  | ||||
| #pragma mark - Overrides | ||||
|  | ||||
| + (instancetype)forObject:(UIApplication *)application { | ||||
|     return [self forObject:application additionalRows:@[ | ||||
|         [FLEXActionShortcut title:@"Open URL…" | ||||
|             subtitle:^NSString *(UIViewController *controller) { | ||||
|                 return nil; | ||||
|             } | ||||
|             selectionHandler:^void(UIViewController *host, UIApplication *app) { | ||||
|                 [FLEXAlert makeAlert:^(FLEXAlert *make) { | ||||
|                     make.title(@"Open URL"); | ||||
|                     make.message( | ||||
|                         @"This will call openURL: or openURL:options:completion: " | ||||
|                          "with the string below. 'Open if Universal' will only open " | ||||
|                          "the URL if it is a registered Universal Link." | ||||
|                     ); | ||||
|                      | ||||
|                     make.textField(@"twitter://user?id=12345"); | ||||
|                     make.button(@"Open").handler(^(NSArray<NSString *> *strings) { | ||||
|                         [self openURL:strings[0] inApp:app onlyIfUniveral:NO host:host]; | ||||
|                     }); | ||||
|                     make.button(@"Open if Universal").handler(^(NSArray<NSString *> *strings) { | ||||
|                         [self openURL:strings[0] inApp:app onlyIfUniveral:YES host:host]; | ||||
|                     }); | ||||
|                     make.button(@"Cancel").cancelStyle(); | ||||
|                 } showFrom:host]; | ||||
|             } | ||||
|             accessoryType:^UITableViewCellAccessoryType(UIViewController *controller) { | ||||
|                 return UITableViewCellAccessoryDisclosureIndicator; | ||||
|             } | ||||
|         ] | ||||
|     ]]; | ||||
| } | ||||
|  | ||||
| + (void)openURL:(NSString *)urlString | ||||
|           inApp:(UIApplication *)app | ||||
|  onlyIfUniveral:(BOOL)universalOnly | ||||
|            host:(UIViewController *)host { | ||||
|     NSURL *url = [NSURL URLWithString:urlString]; | ||||
|      | ||||
|     if (url) { | ||||
|         if (@available(iOS 10, *)) { | ||||
|             [app openURL:url options:@{ | ||||
|                 UIApplicationOpenURLOptionUniversalLinksOnly: @(universalOnly) | ||||
|             } completionHandler:^(BOOL success) { | ||||
|                 if (!success) { | ||||
|                     [FLEXAlert showAlert:@"No Universal Link Handler" | ||||
|                         message:@"No installed application is registered to handle this link." | ||||
|                         from:host | ||||
|                     ]; | ||||
|                 } | ||||
|             }]; | ||||
|         } else { | ||||
|             [app openURL:url]; | ||||
|         } | ||||
|     } else { | ||||
|         [FLEXAlert showAlert:@"Error" message:@"Invalid URL" from:host]; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @end | ||||
|  | ||||
| @@ -0,0 +1,15 @@ | ||||
| // | ||||
| //  FLEXViewControllerShortcuts.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 12/12/19. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXShortcutsSection.h" | ||||
|  | ||||
| @interface FLEXViewControllerShortcuts : FLEXShortcutsSection | ||||
|  | ||||
| + (instancetype)forObject:(UIViewController *)viewController; | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,58 @@ | ||||
| // | ||||
| //  FLEXViewControllerShortcuts.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 12/12/19. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXViewControllerShortcuts.h" | ||||
| #import "FLEXObjectExplorerFactory.h" | ||||
| #import "FLEXRuntimeUtility.h" | ||||
| #import "FLEXShortcut.h" | ||||
| #import "FLEXAlert.h" | ||||
|  | ||||
| @interface FLEXViewControllerShortcuts () | ||||
| @end | ||||
|  | ||||
| @implementation FLEXViewControllerShortcuts | ||||
|  | ||||
| #pragma mark - Overrides | ||||
|  | ||||
| + (instancetype)forObject:(UIViewController *)viewController { | ||||
|     BOOL (^vcIsInuse)(UIViewController *) = ^BOOL(UIViewController *controller) { | ||||
|         if (controller.viewIfLoaded.window) { | ||||
|             return YES; | ||||
|         } | ||||
|  | ||||
|         return controller.navigationController != nil; | ||||
|     }; | ||||
|      | ||||
|     return [self forObject:viewController additionalRows:@[ | ||||
|         [FLEXActionShortcut title:@"Push View Controller" | ||||
|             subtitle:^NSString *(UIViewController *controller) { | ||||
|                 return vcIsInuse(controller) ? @"In use, cannot push" : nil; | ||||
|             } | ||||
|             selectionHandler:^void(UIViewController *host, UIViewController *controller) { | ||||
|                 if (!vcIsInuse(controller)) { | ||||
|                     [host.navigationController pushViewController:controller animated:YES]; | ||||
|                 } else { | ||||
|                     [FLEXAlert | ||||
|                         showAlert:@"Cannot Push View Controller" | ||||
|                         message:@"This view controller's view is currently in use." | ||||
|                         from:host | ||||
|                     ]; | ||||
|                 } | ||||
|             } | ||||
|             accessoryType:^UITableViewCellAccessoryType(UIViewController *controller) { | ||||
|                 if (!vcIsInuse(controller)) { | ||||
|                     return UITableViewCellAccessoryDisclosureIndicator; | ||||
|                 } else { | ||||
|                     return UITableViewCellAccessoryNone; | ||||
|                 } | ||||
|             } | ||||
|         ] | ||||
|     ]]; | ||||
| } | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,14 @@ | ||||
| // | ||||
| //  FLEXViewShortcuts.h | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 12/11/19. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXShortcutsSection.h" | ||||
|  | ||||
| /// Adds "Nearest View Controller" and "Preview Image" shortcuts to all views | ||||
| @interface FLEXViewShortcuts : FLEXShortcutsSection | ||||
|  | ||||
| @end | ||||
| @@ -0,0 +1,90 @@ | ||||
| // | ||||
| //  FLEXViewShortcuts.m | ||||
| //  FLEX | ||||
| // | ||||
| //  Created by Tanner Bennett on 12/11/19. | ||||
| //  Copyright © 2020 FLEX Team. All rights reserved. | ||||
| // | ||||
|  | ||||
| #import "FLEXViewShortcuts.h" | ||||
| #import "FLEXShortcut.h" | ||||
| #import "FLEXRuntimeUtility.h" | ||||
| #import "FLEXObjectExplorerFactory.h" | ||||
| #import "FLEXImagePreviewViewController.h" | ||||
|  | ||||
| @interface FLEXViewShortcuts () | ||||
| @property (nonatomic, readonly) UIView *view; | ||||
| @end | ||||
|  | ||||
| @implementation FLEXViewShortcuts | ||||
|  | ||||
| #pragma mark - Internal | ||||
|  | ||||
| - (UIView *)view { | ||||
|     return self.object; | ||||
| } | ||||
|  | ||||
| + (UIViewController *)viewControllerForView:(UIView *)view { | ||||
|     NSString *viewDelegate = @"viewDelegate"; | ||||
|     if ([view respondsToSelector:NSSelectorFromString(viewDelegate)]) { | ||||
|         return [view valueForKey:viewDelegate]; | ||||
|     } | ||||
|  | ||||
|     return nil; | ||||
| } | ||||
|  | ||||
| + (UIViewController *)viewControllerForAncestralView:(UIView *)view { | ||||
|     NSString *_viewControllerForAncestor = @"_viewControllerForAncestor"; | ||||
|     if ([view respondsToSelector:NSSelectorFromString(_viewControllerForAncestor)]) { | ||||
|         return [view valueForKey:_viewControllerForAncestor]; | ||||
|     } | ||||
|  | ||||
|     return nil; | ||||
| } | ||||
|  | ||||
| + (UIViewController *)nearestViewControllerForView:(UIView *)view { | ||||
|     return [self viewControllerForView:view] ?: [self viewControllerForAncestralView:view]; | ||||
| } | ||||
|  | ||||
|  | ||||
| #pragma mark - Overrides | ||||
|  | ||||
| + (instancetype)forObject:(UIView *)view { | ||||
|     // In the past, FLEX would not hold a strong reference to something like this. | ||||
|     // After using FLEX for so long, I am certain it is more useful to eagerly | ||||
|     // reference something as useful as a view controller so that the reference | ||||
|     // is not lost and swept out from under you before you can access it. | ||||
|     // | ||||
|     // The alternative here is to use a future in place of `controller` which would | ||||
|     // dynamically grab a reference to the view controller. 99% of the time, however, | ||||
|     // it is not all that useful. If you need it to refresh, you can simply go back | ||||
|     // and go forward again and it will show if the view controller is nil or changed. | ||||
|     UIViewController *controller = [FLEXViewShortcuts nearestViewControllerForView:view]; | ||||
|  | ||||
|     return [self forObject:view additionalRows:@[ | ||||
|         [FLEXActionShortcut title:@"Nearest View Controller" | ||||
|             subtitle:^NSString *(id view) { | ||||
|                 return [FLEXRuntimeUtility safeDescriptionForObject:controller]; | ||||
|             } | ||||
|             viewer:^UIViewController *(id view) { | ||||
|                 return [FLEXObjectExplorerFactory explorerViewControllerForObject:controller]; | ||||
|             } | ||||
|             accessoryType:^UITableViewCellAccessoryType(id view) { | ||||
|                 return controller ? UITableViewCellAccessoryDisclosureIndicator : UITableViewCellAccessoryNone; | ||||
|             } | ||||
|         ], | ||||
|         [FLEXActionShortcut title:@"Preview Image" subtitle:^NSString *(UIView *view) { | ||||
|                 return !CGRectIsEmpty(view.bounds) ? @"" : @"Unavailable with empty bounds"; | ||||
|             } | ||||
|             viewer:^UIViewController *(UIView *view) { | ||||
|                 return [FLEXImagePreviewViewController previewForView:view]; | ||||
|             } | ||||
|             accessoryType:^UITableViewCellAccessoryType(UIView *view) { | ||||
|                 // Disable preview if bounds are CGRectZero | ||||
|                 return !CGRectIsEmpty(view.bounds) ? UITableViewCellAccessoryDisclosureIndicator : UITableViewCellAccessoryNone; | ||||
|             } | ||||
|         ] | ||||
|     ]]; | ||||
| } | ||||
|  | ||||
| @end | ||||
		Reference in New Issue
	
	Block a user
	 Balackburn
					Balackburn