mirror of
				https://github.com/SoPat712/YTLitePlus.git
				synced 2025-10-31 04:44:14 -04:00 
			
		
		
		
	added files via upload
This commit is contained in:
		| @@ -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