added files via upload

This commit is contained in:
Balackburn
2023-06-27 09:54:41 +02:00
commit 2ff6aac218
1420 changed files with 88898 additions and 0 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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

View 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

View File

@@ -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

View 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

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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