mirror of
https://github.com/SoPat712/YTLitePlus.git
synced 2025-08-24 11:28:52 -04:00
added files via upload
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
//
|
||||
// FLEXCollectionContentSection.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 8/28/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXTableViewSection.h"
|
||||
#import "FLEXObjectInfoSection.h"
|
||||
@class FLEXCollectionContentSection, FLEXTableViewCell;
|
||||
@protocol FLEXCollection, FLEXMutableCollection;
|
||||
|
||||
/// Any foundation collection implicitly conforms to FLEXCollection.
|
||||
/// This future should return one. We don't explicitly put FLEXCollection
|
||||
/// here because making generic collections conform to FLEXCollection breaks
|
||||
/// compile-time features of generic arrays, such as \c someArray[0].property
|
||||
typedef id<NSObject, NSFastEnumeration /* FLEXCollection */>(^FLEXCollectionContentFuture)(__kindof FLEXCollectionContentSection *section);
|
||||
|
||||
#pragma mark Collection
|
||||
/// A protocol that enables \c FLEXCollectionContentSection to operate on any arbitrary collection.
|
||||
/// \c NSArray, \c NSDictionary, \c NSSet, and \c NSOrderedSet all conform to this protocol.
|
||||
@protocol FLEXCollection <NSObject, NSFastEnumeration>
|
||||
|
||||
@property (nonatomic, readonly) NSUInteger count;
|
||||
|
||||
- (id)copy;
|
||||
- (id)mutableCopy;
|
||||
|
||||
@optional
|
||||
|
||||
/// Unordered, unkeyed collections must implement this
|
||||
@property (nonatomic, readonly) NSArray *allObjects;
|
||||
/// Keyed collections must implement this and \c objectForKeyedSubscript:
|
||||
@property (nonatomic, readonly) NSArray *allKeys;
|
||||
|
||||
/// Ordered, indexed collections must implement this.
|
||||
- (id)objectAtIndexedSubscript:(NSUInteger)idx;
|
||||
/// Keyed, unordered collections must implement this and \c allKeys
|
||||
- (id)objectForKeyedSubscript:(id)idx;
|
||||
|
||||
@end
|
||||
|
||||
@protocol FLEXMutableCollection <FLEXCollection>
|
||||
- (void)filterUsingPredicate:(NSPredicate *)predicate;
|
||||
@end
|
||||
|
||||
|
||||
#pragma mark - FLEXCollectionContentSection
|
||||
/// A custom section for viewing collection elements.
|
||||
///
|
||||
/// Tapping on a row pushes an object explorer for that element.
|
||||
@interface FLEXCollectionContentSection<__covariant ObjectType> : FLEXTableViewSection <FLEXObjectInfoSection> {
|
||||
@protected
|
||||
/// Unused if initialized with a future
|
||||
id<FLEXCollection> _collection;
|
||||
/// Unused if initialized with a collection
|
||||
FLEXCollectionContentFuture _collectionFuture;
|
||||
/// The filtered collection from \c _collection or \c _collectionFuture
|
||||
id<FLEXCollection> _cachedCollection;
|
||||
}
|
||||
|
||||
+ (instancetype)forCollection:(id)collection;
|
||||
/// The future given should be safe to call more than once.
|
||||
/// The result of calling this future multiple times may yield
|
||||
/// different results each time if the data is changing by nature.
|
||||
+ (instancetype)forReusableFuture:(FLEXCollectionContentFuture)collectionFuture;
|
||||
|
||||
/// Defaults to \c NO
|
||||
@property (nonatomic) BOOL hideSectionTitle;
|
||||
/// Defaults to \c nil
|
||||
@property (nonatomic, copy) NSString *customTitle;
|
||||
/// Defaults to \c NO
|
||||
///
|
||||
/// Settings this to \c NO will not display the element index for ordered collections.
|
||||
/// This property only applies to \c NSArray or \c NSOrderedSet and their subclasses.
|
||||
@property (nonatomic) BOOL hideOrderIndexes;
|
||||
|
||||
/// Set this property to provide a custom filter matcher.
|
||||
///
|
||||
/// By default, the collection will filter on the title and subtitle of the row.
|
||||
/// So if you don't ever call \c configureCell: for example, you will need to set
|
||||
/// this property so that your filter logic will match how you're setting up the cell.
|
||||
@property (nonatomic) BOOL (^customFilter)(NSString *filterText, ObjectType element);
|
||||
|
||||
/// Get the object in the collection associated with the given row.
|
||||
/// For dictionaries, this returns the value, not the key.
|
||||
- (ObjectType)objectForRow:(NSInteger)row;
|
||||
|
||||
/// Subclasses may override.
|
||||
- (UITableViewCellAccessoryType)accessoryTypeForRow:(NSInteger)row;
|
||||
|
||||
@end
|
@@ -0,0 +1,246 @@
|
||||
//
|
||||
// FLEXCollectionContentSection.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 8/28/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXCollectionContentSection.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
#import "FLEXSubtitleTableViewCell.h"
|
||||
#import "FLEXTableView.h"
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
#import "FLEXDefaultEditorViewController.h"
|
||||
|
||||
typedef NS_ENUM(NSUInteger, FLEXCollectionType) {
|
||||
FLEXUnsupportedCollection,
|
||||
FLEXOrderedCollection,
|
||||
FLEXUnorderedCollection,
|
||||
FLEXKeyedCollection
|
||||
};
|
||||
|
||||
@interface NSArray (FLEXCollection) <FLEXCollection> @end
|
||||
@interface NSSet (FLEXCollection) <FLEXCollection> @end
|
||||
@interface NSOrderedSet (FLEXCollection) <FLEXCollection> @end
|
||||
@interface NSDictionary (FLEXCollection) <FLEXCollection> @end
|
||||
|
||||
@interface NSMutableArray (FLEXMutableCollection) <FLEXMutableCollection> @end
|
||||
@interface NSMutableSet (FLEXMutableCollection) <FLEXMutableCollection> @end
|
||||
@interface NSMutableOrderedSet (FLEXMutableCollection) <FLEXMutableCollection> @end
|
||||
@interface NSMutableDictionary (FLEXMutableCollection) <FLEXMutableCollection>
|
||||
- (void)filterUsingPredicate:(NSPredicate *)predicate;
|
||||
@end
|
||||
|
||||
@interface FLEXCollectionContentSection ()
|
||||
/// Generated from \c collectionFuture or \c collection
|
||||
@property (nonatomic, copy) id<FLEXCollection> cachedCollection;
|
||||
/// A static collection to display
|
||||
@property (nonatomic, readonly) id<FLEXCollection> collection;
|
||||
/// A collection that may change over time and can be called upon for new data
|
||||
@property (nonatomic, readonly) FLEXCollectionContentFuture collectionFuture;
|
||||
@property (nonatomic, readonly) FLEXCollectionType collectionType;
|
||||
@property (nonatomic, readonly) BOOL isMutable;
|
||||
@end
|
||||
|
||||
@implementation FLEXCollectionContentSection
|
||||
@synthesize filterText = _filterText;
|
||||
|
||||
#pragma mark Initialization
|
||||
|
||||
+ (instancetype)forObject:(id)object {
|
||||
return [self forCollection:object];
|
||||
}
|
||||
|
||||
+ (id)forCollection:(id<FLEXCollection>)collection {
|
||||
FLEXCollectionContentSection *section = [self new];
|
||||
section->_collectionType = [self typeForCollection:collection];
|
||||
section->_collection = collection;
|
||||
section.cachedCollection = collection;
|
||||
section->_isMutable = [collection respondsToSelector:@selector(filterUsingPredicate:)];
|
||||
return section;
|
||||
}
|
||||
|
||||
+ (id)forReusableFuture:(FLEXCollectionContentFuture)collectionFuture {
|
||||
FLEXCollectionContentSection *section = [self new];
|
||||
section->_collectionFuture = collectionFuture;
|
||||
section.cachedCollection = (id<FLEXCollection>)collectionFuture(section);
|
||||
section->_collectionType = [self typeForCollection:section.cachedCollection];
|
||||
section->_isMutable = [section->_cachedCollection respondsToSelector:@selector(filterUsingPredicate:)];
|
||||
return section;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Misc
|
||||
|
||||
+ (FLEXCollectionType)typeForCollection:(id<FLEXCollection>)collection {
|
||||
// Order matters here, as NSDictionary is keyed but it responds to allObjects
|
||||
if ([collection respondsToSelector:@selector(objectAtIndex:)]) {
|
||||
return FLEXOrderedCollection;
|
||||
}
|
||||
if ([collection respondsToSelector:@selector(objectForKey:)]) {
|
||||
return FLEXKeyedCollection;
|
||||
}
|
||||
if ([collection respondsToSelector:@selector(allObjects)]) {
|
||||
return FLEXUnorderedCollection;
|
||||
}
|
||||
|
||||
[NSException raise:NSInvalidArgumentException
|
||||
format:@"Given collection does not properly conform to FLEXCollection"];
|
||||
return FLEXUnsupportedCollection;
|
||||
}
|
||||
|
||||
/// Row titles
|
||||
/// - Ordered: the index
|
||||
/// - Unordered: the object
|
||||
/// - Keyed: the key
|
||||
- (NSString *)titleForRow:(NSInteger)row {
|
||||
switch (self.collectionType) {
|
||||
case FLEXOrderedCollection:
|
||||
if (!self.hideOrderIndexes) {
|
||||
return @(row).stringValue;
|
||||
}
|
||||
// Fall-through
|
||||
case FLEXUnorderedCollection:
|
||||
return [self describe:[self objectForRow:row]];
|
||||
case FLEXKeyedCollection:
|
||||
return [self describe:self.cachedCollection.allKeys[row]];
|
||||
|
||||
case FLEXUnsupportedCollection:
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
/// Row subtitles
|
||||
/// - Ordered: the object
|
||||
/// - Unordered: nothing
|
||||
/// - Keyed: the value
|
||||
- (NSString *)subtitleForRow:(NSInteger)row {
|
||||
switch (self.collectionType) {
|
||||
case FLEXOrderedCollection:
|
||||
if (!self.hideOrderIndexes) {
|
||||
nil;
|
||||
}
|
||||
// Fall-through
|
||||
case FLEXKeyedCollection:
|
||||
return [self describe:[self objectForRow:row]];
|
||||
case FLEXUnorderedCollection:
|
||||
return nil;
|
||||
|
||||
case FLEXUnsupportedCollection:
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSString *)describe:(id)object {
|
||||
return [FLEXRuntimeUtility summaryForObject:object];
|
||||
}
|
||||
|
||||
- (id)objectForRow:(NSInteger)row {
|
||||
switch (self.collectionType) {
|
||||
case FLEXOrderedCollection:
|
||||
return self.cachedCollection[row];
|
||||
case FLEXUnorderedCollection:
|
||||
return self.cachedCollection.allObjects[row];
|
||||
case FLEXKeyedCollection:
|
||||
return self.cachedCollection[self.cachedCollection.allKeys[row]];
|
||||
|
||||
case FLEXUnsupportedCollection:
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (UITableViewCellAccessoryType)accessoryTypeForRow:(NSInteger)row {
|
||||
return UITableViewCellAccessoryDisclosureIndicator;
|
||||
// return self.isMutable ? UITableViewCellAccessoryDetailDisclosureButton : UITableViewCellAccessoryDisclosureIndicator;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Overrides
|
||||
|
||||
- (NSString *)title {
|
||||
if (!self.hideSectionTitle) {
|
||||
if (self.customTitle) {
|
||||
return self.customTitle;
|
||||
}
|
||||
|
||||
return FLEXPluralString(self.cachedCollection.count, @"Entries", @"Entry");
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (NSInteger)numberOfRows {
|
||||
return self.cachedCollection.count;
|
||||
}
|
||||
|
||||
- (void)setFilterText:(NSString *)filterText {
|
||||
super.filterText = filterText;
|
||||
|
||||
if (filterText.length) {
|
||||
BOOL (^matcher)(id, id) = self.customFilter ?: ^BOOL(NSString *query, id obj) {
|
||||
return [[self describe:obj] localizedCaseInsensitiveContainsString:query];
|
||||
};
|
||||
|
||||
NSPredicate *filter = [NSPredicate predicateWithBlock:^BOOL(id obj, NSDictionary *bindings) {
|
||||
return matcher(filterText, obj);
|
||||
}];
|
||||
|
||||
id<FLEXMutableCollection> tmp = self.cachedCollection.mutableCopy;
|
||||
[tmp filterUsingPredicate:filter];
|
||||
self.cachedCollection = tmp;
|
||||
} else {
|
||||
self.cachedCollection = self.collection ?: (id<FLEXCollection>)self.collectionFuture(self);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)reloadData {
|
||||
if (self.collectionFuture) {
|
||||
self.cachedCollection = (id<FLEXCollection>)self.collectionFuture(self);
|
||||
} else {
|
||||
self.cachedCollection = self.collection.copy;
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)canSelectRow:(NSInteger)row {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (UIViewController *)viewControllerToPushForRow:(NSInteger)row {
|
||||
return [FLEXObjectExplorerFactory explorerViewControllerForObject:[self objectForRow:row]];
|
||||
}
|
||||
|
||||
- (NSString *)reuseIdentifierForRow:(NSInteger)row {
|
||||
return kFLEXDetailCell;
|
||||
}
|
||||
|
||||
- (void)configureCell:(__kindof FLEXTableViewCell *)cell forRow:(NSInteger)row {
|
||||
cell.titleLabel.text = [self titleForRow:row];
|
||||
cell.subtitleLabel.text = [self subtitleForRow:row];
|
||||
cell.accessoryType = [self accessoryTypeForRow:row];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
#pragma mark - NSMutableDictionary
|
||||
|
||||
@implementation NSMutableDictionary (FLEXMutableCollection)
|
||||
|
||||
- (void)filterUsingPredicate:(NSPredicate *)predicate {
|
||||
id test = ^BOOL(id key, NSUInteger idx, BOOL *stop) {
|
||||
if ([predicate evaluateWithObject:key]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
return ![predicate evaluateWithObject:self[key]];
|
||||
};
|
||||
|
||||
NSArray *keys = self.allKeys;
|
||||
NSIndexSet *remove = [keys indexesOfObjectsPassingTest:test];
|
||||
|
||||
[self removeObjectsForKeys:[keys objectsAtIndexes:remove]];
|
||||
}
|
||||
|
||||
@end
|
@@ -0,0 +1,16 @@
|
||||
//
|
||||
// FLEXColorPreviewSection.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 12/12/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXSingleRowSection.h"
|
||||
#import "FLEXObjectInfoSection.h"
|
||||
|
||||
@interface FLEXColorPreviewSection : FLEXSingleRowSection <FLEXObjectInfoSection>
|
||||
|
||||
+ (instancetype)forObject:(UIColor *)color;
|
||||
|
||||
@end
|
@@ -0,0 +1,30 @@
|
||||
//
|
||||
// FLEXColorPreviewSection.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 12/12/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXColorPreviewSection.h"
|
||||
|
||||
@implementation FLEXColorPreviewSection
|
||||
|
||||
+ (instancetype)forObject:(UIColor *)color {
|
||||
return [self title:@"Color" reuse:nil cell:^(__kindof UITableViewCell *cell) {
|
||||
cell.backgroundColor = color;
|
||||
}];
|
||||
}
|
||||
|
||||
- (BOOL)canSelectRow:(NSInteger)row {
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL (^)(NSString *))filterMatcher {
|
||||
return ^BOOL(NSString *filterText) {
|
||||
// Hide when searching
|
||||
return !filterText.length;
|
||||
};
|
||||
}
|
||||
|
||||
@end
|
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// FLEXDefaultsContentSection.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 8/28/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXCollectionContentSection.h"
|
||||
#import "FLEXObjectInfoSection.h"
|
||||
|
||||
@interface FLEXDefaultsContentSection : FLEXCollectionContentSection <FLEXObjectInfoSection>
|
||||
|
||||
/// Uses \c NSUserDefaults.standardUserDefaults
|
||||
+ (instancetype)standard;
|
||||
+ (instancetype)forDefaults:(NSUserDefaults *)userDefaults;
|
||||
|
||||
/// Whether or not to filter out keys not present in the app's user defaults file.
|
||||
///
|
||||
/// This is useful for filtering out some useless keys that seem to appear
|
||||
/// in every app's defaults but are never actually used or touched by the app.
|
||||
/// Only applies to instances using \c NSUserDefaults.standardUserDefaults.
|
||||
/// This is the default for any instance using \c standardUserDefaults, so
|
||||
/// you must opt-out in those instances if you don't want this behavior.
|
||||
@property (nonatomic) BOOL onlyShowKeysForAppPrefs;
|
||||
|
||||
@end
|
@@ -0,0 +1,117 @@
|
||||
//
|
||||
// FLEXDefaultsContentSection.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 8/28/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXDefaultsContentSection.h"
|
||||
#import "FLEXDefaultEditorViewController.h"
|
||||
#import "FLEXUtility.h"
|
||||
|
||||
@interface FLEXDefaultsContentSection ()
|
||||
@property (nonatomic) NSUserDefaults *defaults;
|
||||
@property (nonatomic) NSArray *keys;
|
||||
@property (nonatomic, readonly) NSDictionary *unexcludedDefaults;
|
||||
@end
|
||||
|
||||
@implementation FLEXDefaultsContentSection
|
||||
@synthesize keys = _keys;
|
||||
|
||||
#pragma mark Initialization
|
||||
|
||||
+ (instancetype)forObject:(id)object {
|
||||
return [self forDefaults:object];
|
||||
}
|
||||
|
||||
+ (instancetype)standard {
|
||||
return [self forDefaults:NSUserDefaults.standardUserDefaults];
|
||||
}
|
||||
|
||||
+ (instancetype)forDefaults:(NSUserDefaults *)userDefaults {
|
||||
FLEXDefaultsContentSection *section = [self forReusableFuture:^id(FLEXDefaultsContentSection *section) {
|
||||
section.defaults = userDefaults;
|
||||
section.onlyShowKeysForAppPrefs = YES;
|
||||
return section.unexcludedDefaults;
|
||||
}];
|
||||
return section;
|
||||
}
|
||||
|
||||
#pragma mark - Overrides
|
||||
|
||||
- (NSString *)title {
|
||||
return @"Defaults";
|
||||
}
|
||||
|
||||
- (void (^)(__kindof UIViewController *))didPressInfoButtonAction:(NSInteger)row {
|
||||
return ^(UIViewController *host) {
|
||||
if ([FLEXDefaultEditorViewController canEditDefaultWithValue:[self objectForRow:row]]) {
|
||||
// We use titleForRow: to get the key because self.keys is not
|
||||
// necessarily in the same order as the keys being displayed
|
||||
FLEXVariableEditorViewController *controller = [FLEXDefaultEditorViewController
|
||||
target:self.defaults key:[self titleForRow:row] commitHandler:^{
|
||||
[self reloadData:YES];
|
||||
}
|
||||
];
|
||||
[host.navigationController pushViewController:controller animated:YES];
|
||||
} else {
|
||||
[FLEXAlert showAlert:@"Oh No…" message:@"We can't edit this entry :(" from:host];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
- (UITableViewCellAccessoryType)accessoryTypeForRow:(NSInteger)row {
|
||||
return UITableViewCellAccessoryDetailDisclosureButton;
|
||||
}
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
- (NSArray *)keys {
|
||||
if (!_keys) {
|
||||
if (self.onlyShowKeysForAppPrefs) {
|
||||
// Read keys from preferences file
|
||||
NSString *bundle = NSBundle.mainBundle.bundleIdentifier;
|
||||
NSString *prefsPath = [NSHomeDirectory() stringByAppendingPathComponent:@"Library/Preferences"];
|
||||
NSString *filePath = [NSString stringWithFormat:@"%@/%@.plist", prefsPath, bundle];
|
||||
self.keys = [NSDictionary dictionaryWithContentsOfFile:filePath].allKeys;
|
||||
} else {
|
||||
self.keys = self.defaults.dictionaryRepresentation.allKeys;
|
||||
}
|
||||
}
|
||||
|
||||
return _keys;
|
||||
}
|
||||
|
||||
- (void)setKeys:(NSArray *)keys {
|
||||
_keys = [keys sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)];
|
||||
}
|
||||
|
||||
- (NSDictionary *)unexcludedDefaults {
|
||||
// Case: no excluding
|
||||
if (!self.onlyShowKeysForAppPrefs) {
|
||||
return self.defaults.dictionaryRepresentation;
|
||||
}
|
||||
|
||||
// Always regenerate key allowlist when this method is called
|
||||
_keys = nil;
|
||||
|
||||
// Generate new dictionary from unexcluded keys
|
||||
NSArray *values = [self.defaults.dictionaryRepresentation
|
||||
objectsForKeys:self.keys notFoundMarker:NSNull.null
|
||||
];
|
||||
return [NSDictionary dictionaryWithObjects:values forKeys:self.keys];
|
||||
}
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
- (void)setOnlyShowKeysForAppPrefs:(BOOL)onlyShowKeysForAppPrefs {
|
||||
if (onlyShowKeysForAppPrefs) {
|
||||
// This property only applies if we're using standardUserDefaults
|
||||
if (self.defaults != NSUserDefaults.standardUserDefaults) return;
|
||||
}
|
||||
|
||||
_onlyShowKeysForAppPrefs = onlyShowKeysForAppPrefs;
|
||||
}
|
||||
|
||||
@end
|
37
Tweaks/FLEX/ObjectExplorers/Sections/FLEXMetadataSection.h
Normal file
37
Tweaks/FLEX/ObjectExplorers/Sections/FLEXMetadataSection.h
Normal file
@@ -0,0 +1,37 @@
|
||||
//
|
||||
// FLEXMetadataSection.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 9/19/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXTableViewSection.h"
|
||||
#import "FLEXObjectExplorer.h"
|
||||
|
||||
typedef NS_ENUM(NSUInteger, FLEXMetadataKind) {
|
||||
FLEXMetadataKindProperties = 1,
|
||||
FLEXMetadataKindClassProperties,
|
||||
FLEXMetadataKindIvars,
|
||||
FLEXMetadataKindMethods,
|
||||
FLEXMetadataKindClassMethods,
|
||||
FLEXMetadataKindClassHierarchy,
|
||||
FLEXMetadataKindProtocols,
|
||||
FLEXMetadataKindOther
|
||||
};
|
||||
|
||||
/// This section is used for displaying ObjC runtime metadata
|
||||
/// about a class or object, such as listing methods, properties, etc.
|
||||
@interface FLEXMetadataSection : FLEXTableViewSection
|
||||
|
||||
+ (instancetype)explorer:(FLEXObjectExplorer *)explorer kind:(FLEXMetadataKind)metadataKind;
|
||||
|
||||
@property (nonatomic, readonly) FLEXMetadataKind metadataKind;
|
||||
|
||||
/// The names of metadata to exclude. Useful if you wish to group specific
|
||||
/// properties or methods together in their own section outside of this one.
|
||||
///
|
||||
/// Setting this property calls \c reloadData on this section.
|
||||
@property (nonatomic) NSSet<NSString *> *excludedMetadata;
|
||||
|
||||
@end
|
233
Tweaks/FLEX/ObjectExplorers/Sections/FLEXMetadataSection.m
Normal file
233
Tweaks/FLEX/ObjectExplorers/Sections/FLEXMetadataSection.m
Normal file
@@ -0,0 +1,233 @@
|
||||
//
|
||||
// FLEXMetadataSection.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 9/19/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXMetadataSection.h"
|
||||
#import "FLEXTableView.h"
|
||||
#import "FLEXTableViewCell.h"
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
#import "FLEXFieldEditorViewController.h"
|
||||
#import "FLEXMethodCallingViewController.h"
|
||||
#import "FLEXIvar.h"
|
||||
#import "NSArray+FLEX.h"
|
||||
#import "FLEXRuntime+UIKitHelpers.h"
|
||||
|
||||
@interface FLEXMetadataSection ()
|
||||
@property (nonatomic, readonly) FLEXObjectExplorer *explorer;
|
||||
/// Filtered
|
||||
@property (nonatomic, copy) NSArray<id<FLEXRuntimeMetadata>> *metadata;
|
||||
/// Unfiltered
|
||||
@property (nonatomic, copy) NSArray<id<FLEXRuntimeMetadata>> *allMetadata;
|
||||
@end
|
||||
|
||||
@implementation FLEXMetadataSection
|
||||
|
||||
#pragma mark - Initialization
|
||||
|
||||
+ (instancetype)explorer:(FLEXObjectExplorer *)explorer kind:(FLEXMetadataKind)metadataKind {
|
||||
return [[self alloc] initWithExplorer:explorer kind:metadataKind];
|
||||
}
|
||||
|
||||
- (id)initWithExplorer:(FLEXObjectExplorer *)explorer kind:(FLEXMetadataKind)metadataKind {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_explorer = explorer;
|
||||
_metadataKind = metadataKind;
|
||||
|
||||
[self reloadData];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
- (NSString *)titleWithBaseName:(NSString *)baseName {
|
||||
unsigned long totalCount = self.allMetadata.count;
|
||||
unsigned long filteredCount = self.metadata.count;
|
||||
|
||||
if (totalCount == filteredCount) {
|
||||
return [baseName stringByAppendingFormat:@" (%lu)", totalCount];
|
||||
} else {
|
||||
return [baseName stringByAppendingFormat:@" (%lu of %lu)", filteredCount, totalCount];
|
||||
}
|
||||
}
|
||||
|
||||
- (UITableViewCellAccessoryType)accessoryTypeForRow:(NSInteger)row {
|
||||
return [self.metadata[row] suggestedAccessoryTypeWithTarget:self.explorer.object];
|
||||
}
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
- (void)setExcludedMetadata:(NSSet<NSString *> *)excludedMetadata {
|
||||
_excludedMetadata = excludedMetadata;
|
||||
[self reloadData];
|
||||
}
|
||||
|
||||
#pragma mark - Overrides
|
||||
|
||||
- (NSString *)titleForRow:(NSInteger)row {
|
||||
return [self.metadata[row] description];
|
||||
}
|
||||
|
||||
- (NSString *)subtitleForRow:(NSInteger)row {
|
||||
return [self.metadata[row] previewWithTarget:self.explorer.object];
|
||||
}
|
||||
|
||||
- (NSString *)title {
|
||||
switch (self.metadataKind) {
|
||||
case FLEXMetadataKindProperties:
|
||||
return [self titleWithBaseName:@"Properties"];
|
||||
case FLEXMetadataKindClassProperties:
|
||||
return [self titleWithBaseName:@"Class Properties"];
|
||||
case FLEXMetadataKindIvars:
|
||||
return [self titleWithBaseName:@"Ivars"];
|
||||
case FLEXMetadataKindMethods:
|
||||
return [self titleWithBaseName:@"Methods"];
|
||||
case FLEXMetadataKindClassMethods:
|
||||
return [self titleWithBaseName:@"Class Methods"];
|
||||
case FLEXMetadataKindClassHierarchy:
|
||||
return [self titleWithBaseName:@"Class Hierarchy"];
|
||||
case FLEXMetadataKindProtocols:
|
||||
return [self titleWithBaseName:@"Protocols"];
|
||||
case FLEXMetadataKindOther:
|
||||
return @"Miscellaneous";
|
||||
}
|
||||
}
|
||||
|
||||
- (NSInteger)numberOfRows {
|
||||
return self.metadata.count;
|
||||
}
|
||||
|
||||
- (void)setFilterText:(NSString *)filterText {
|
||||
super.filterText = filterText;
|
||||
|
||||
if (!self.filterText.length) {
|
||||
self.metadata = self.allMetadata;
|
||||
} else {
|
||||
self.metadata = [self.allMetadata flex_filtered:^BOOL(id<FLEXRuntimeMetadata> obj, NSUInteger idx) {
|
||||
return [obj.description localizedCaseInsensitiveContainsString:self.filterText];
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)reloadData {
|
||||
switch (self.metadataKind) {
|
||||
case FLEXMetadataKindProperties:
|
||||
self.allMetadata = self.explorer.properties;
|
||||
break;
|
||||
case FLEXMetadataKindClassProperties:
|
||||
self.allMetadata = self.explorer.classProperties;
|
||||
break;
|
||||
case FLEXMetadataKindIvars:
|
||||
self.allMetadata = self.explorer.ivars;
|
||||
break;
|
||||
case FLEXMetadataKindMethods:
|
||||
self.allMetadata = self.explorer.methods;
|
||||
break;
|
||||
case FLEXMetadataKindClassMethods:
|
||||
self.allMetadata = self.explorer.classMethods;
|
||||
break;
|
||||
case FLEXMetadataKindProtocols:
|
||||
self.allMetadata = self.explorer.conformedProtocols;
|
||||
break;
|
||||
case FLEXMetadataKindClassHierarchy:
|
||||
self.allMetadata = self.explorer.classHierarchy;
|
||||
break;
|
||||
case FLEXMetadataKindOther:
|
||||
self.allMetadata = @[self.explorer.instanceSize, self.explorer.imageName];
|
||||
break;
|
||||
}
|
||||
|
||||
// Remove excluded metadata
|
||||
if (self.excludedMetadata.count) {
|
||||
id filterBlock = ^BOOL(id<FLEXRuntimeMetadata> obj, NSUInteger idx) {
|
||||
return ![self.excludedMetadata containsObject:obj.name];
|
||||
};
|
||||
|
||||
// Filter exclusions and sort
|
||||
self.allMetadata = [[self.allMetadata flex_filtered:filterBlock]
|
||||
sortedArrayUsingSelector:@selector(compare:)
|
||||
];
|
||||
}
|
||||
|
||||
// Re-filter data
|
||||
self.filterText = self.filterText;
|
||||
}
|
||||
|
||||
- (BOOL)canSelectRow:(NSInteger)row {
|
||||
UITableViewCellAccessoryType accessory = [self accessoryTypeForRow:row];
|
||||
return accessory == UITableViewCellAccessoryDisclosureIndicator ||
|
||||
accessory == UITableViewCellAccessoryDetailDisclosureButton;
|
||||
}
|
||||
|
||||
- (NSString *)reuseIdentifierForRow:(NSInteger)row {
|
||||
return [self.metadata[row] reuseIdentifierWithTarget:self.explorer.object] ?: kFLEXCodeFontCell;
|
||||
}
|
||||
|
||||
- (UIViewController *)viewControllerToPushForRow:(NSInteger)row {
|
||||
return [self.metadata[row] viewerWithTarget:self.explorer.object];
|
||||
}
|
||||
|
||||
- (void (^)(__kindof UIViewController *))didPressInfoButtonAction:(NSInteger)row {
|
||||
return ^(UIViewController *host) {
|
||||
[host.navigationController pushViewController:[self editorForRow:row] animated:YES];
|
||||
};
|
||||
}
|
||||
|
||||
- (UIViewController *)editorForRow:(NSInteger)row {
|
||||
return [self.metadata[row] editorWithTarget:self.explorer.object section:self];
|
||||
}
|
||||
|
||||
- (void)configureCell:(__kindof FLEXTableViewCell *)cell forRow:(NSInteger)row {
|
||||
cell.titleLabel.text = [self titleForRow:row];
|
||||
cell.subtitleLabel.text = [self subtitleForRow:row];
|
||||
cell.accessoryType = [self accessoryTypeForRow:row];
|
||||
}
|
||||
|
||||
- (NSString *)menuSubtitleForRow:(NSInteger)row {
|
||||
return [self.metadata[row] contextualSubtitleWithTarget:self.explorer.object];
|
||||
}
|
||||
|
||||
- (NSArray<UIMenuElement *> *)menuItemsForRow:(NSInteger)row sender:(UIViewController *)sender {
|
||||
NSArray<UIMenuElement *> *existingItems = [super menuItemsForRow:row sender:sender];
|
||||
|
||||
// These two metadata kinds don't any of the additional options below
|
||||
switch (self.metadataKind) {
|
||||
case FLEXMetadataKindClassHierarchy:
|
||||
case FLEXMetadataKindOther:
|
||||
return existingItems;
|
||||
|
||||
default: break;
|
||||
}
|
||||
|
||||
id<FLEXRuntimeMetadata> metadata = self.metadata[row];
|
||||
NSMutableArray<UIMenuElement *> *menuItems = [NSMutableArray new];
|
||||
|
||||
[menuItems addObject:[UIAction
|
||||
actionWithTitle:@"Explore Metadata"
|
||||
image:nil
|
||||
identifier:nil
|
||||
handler:^(__kindof UIAction *action) {
|
||||
[sender.navigationController pushViewController:[FLEXObjectExplorerFactory
|
||||
explorerViewControllerForObject:metadata
|
||||
] animated:YES];
|
||||
}
|
||||
]];
|
||||
[menuItems addObjectsFromArray:[metadata
|
||||
additionalActionsWithTarget:self.explorer.object sender:sender
|
||||
]];
|
||||
[menuItems addObjectsFromArray:existingItems];
|
||||
|
||||
return menuItems.copy;
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)copyMenuItemsForRow:(NSInteger)row {
|
||||
return [self.metadata[row] copiableMetadataWithTarget:self.explorer.object];
|
||||
}
|
||||
|
||||
@end
|
@@ -0,0 +1,58 @@
|
||||
//
|
||||
// FLEXMutableListSection.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 3/9/20.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXCollectionContentSection.h"
|
||||
|
||||
typedef void (^FLEXMutableListCellForElement)(__kindof UITableViewCell *cell, id element, NSInteger row);
|
||||
|
||||
/// A section aimed at meeting the needs of table views with one section
|
||||
/// (or, a section that shouldn't warrant the code duplication that comes
|
||||
/// with creating a new section just for some specific table view)
|
||||
///
|
||||
/// Use this section if you want to display a growing list of rows,
|
||||
/// or even if you want to display a static list of rows.
|
||||
///
|
||||
/// To support editing or inserting, implement the appropriate
|
||||
/// table view delegate methods in your table view delegate class
|
||||
/// and call \c mutate: (or \c setList: ) before updating the table view.
|
||||
///
|
||||
/// By default, no section title is shown. Assign one to \c customTitle
|
||||
///
|
||||
/// By default, \c kFLEXDetailCell is the reuse identifier used. If you need
|
||||
/// to support multiple reuse identifiers in a single section, implement the
|
||||
/// \c cellForRowAtIndexPath: method, dequeue the cell yourself and call
|
||||
/// \c -configureCell: on the appropriate section object, passing in the cell
|
||||
@interface FLEXMutableListSection<__covariant ObjectType> : FLEXCollectionContentSection
|
||||
|
||||
/// Initializes a section with an empty list.
|
||||
+ (instancetype)list:(NSArray<ObjectType> *)list
|
||||
cellConfiguration:(FLEXMutableListCellForElement)configurationBlock
|
||||
filterMatcher:(BOOL(^)(NSString *filterText, id element))filterBlock;
|
||||
|
||||
/// By default, rows are not selectable. If you want rows
|
||||
/// to be selectable, provide a selection handler here.
|
||||
@property (nonatomic, copy) void (^selectionHandler)(__kindof UIViewController *host, ObjectType element);
|
||||
|
||||
/// The objects representing all possible rows in the section.
|
||||
@property (nonatomic) NSArray<ObjectType> *list;
|
||||
/// The objects representing the currently unfiltered rows in the section.
|
||||
@property (nonatomic, readonly) NSArray<ObjectType> *filteredList;
|
||||
|
||||
/// A readwrite version of the same property in \c FLEXTableViewSection.h
|
||||
///
|
||||
/// This property expects one entry. An exception is thrown if more than one
|
||||
/// entry is supplied. If you need more than one reuse identifier within a single
|
||||
/// section, your view probably has more complexity than this class can handle.
|
||||
@property (nonatomic, readwrite) NSDictionary<NSString *, Class> *cellRegistrationMapping;
|
||||
|
||||
/// Call this method to mutate the full, unfiltered list.
|
||||
/// This ensures that \c filteredList is updated after any mutations.
|
||||
- (void)mutate:(void(^)(NSMutableArray *list))block;
|
||||
|
||||
@end
|
||||
|
110
Tweaks/FLEX/ObjectExplorers/Sections/FLEXMutableListSection.m
Normal file
110
Tweaks/FLEX/ObjectExplorers/Sections/FLEXMutableListSection.m
Normal file
@@ -0,0 +1,110 @@
|
||||
//
|
||||
// FLEXMutableListSection.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 3/9/20.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXMutableListSection.h"
|
||||
#import "FLEXMacros.h"
|
||||
|
||||
@interface FLEXMutableListSection ()
|
||||
@property (nonatomic, readonly) FLEXMutableListCellForElement configureCell;
|
||||
@end
|
||||
|
||||
@implementation FLEXMutableListSection
|
||||
@synthesize cellRegistrationMapping = _cellRegistrationMapping;
|
||||
|
||||
#pragma mark - Initialization
|
||||
|
||||
+ (instancetype)list:(NSArray *)list
|
||||
cellConfiguration:(FLEXMutableListCellForElement)cellConfig
|
||||
filterMatcher:(BOOL(^)(NSString *, id))filterBlock {
|
||||
return [[self alloc] initWithList:list configurationBlock:cellConfig filterMatcher:filterBlock];
|
||||
}
|
||||
|
||||
- (id)initWithList:(NSArray *)list
|
||||
configurationBlock:(FLEXMutableListCellForElement)cellConfig
|
||||
filterMatcher:(BOOL(^)(NSString *, id))filterBlock {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_configureCell = cellConfig;
|
||||
|
||||
self.list = list.mutableCopy;
|
||||
self.customFilter = filterBlock;
|
||||
self.hideSectionTitle = YES;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
- (NSArray *)list {
|
||||
return (id)_collection;
|
||||
}
|
||||
|
||||
- (void)setList:(NSMutableArray *)list {
|
||||
NSParameterAssert(list);
|
||||
_collection = (id)list;
|
||||
|
||||
[self reloadData];
|
||||
}
|
||||
|
||||
- (NSArray *)filteredList {
|
||||
return (id)_cachedCollection;
|
||||
}
|
||||
|
||||
- (void)mutate:(void (^)(NSMutableArray *))block {
|
||||
block((NSMutableArray *)_collection);
|
||||
[self reloadData];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Overrides
|
||||
|
||||
- (void)setCustomTitle:(NSString *)customTitle {
|
||||
super.customTitle = customTitle;
|
||||
self.hideSectionTitle = customTitle == nil;
|
||||
}
|
||||
|
||||
- (BOOL)canSelectRow:(NSInteger)row {
|
||||
return self.selectionHandler != nil;
|
||||
}
|
||||
|
||||
- (UIViewController *)viewControllerToPushForRow:(NSInteger)row {
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (void (^)(__kindof UIViewController *))didSelectRowAction:(NSInteger)row {
|
||||
if (self.selectionHandler) { weakify(self)
|
||||
return ^(UIViewController *host) { strongify(self)
|
||||
if (self) {
|
||||
self.selectionHandler(host, self.filteredList[row]);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (void)configureCell:(__kindof UITableViewCell *)cell forRow:(NSInteger)row {
|
||||
self.configureCell(cell, self.filteredList[row], row);
|
||||
}
|
||||
|
||||
- (NSString *)reuseIdentifierForRow:(NSInteger)row {
|
||||
if (self.cellRegistrationMapping.count) {
|
||||
return self.cellRegistrationMapping.allKeys.firstObject;
|
||||
}
|
||||
|
||||
return [super reuseIdentifierForRow:row];
|
||||
}
|
||||
|
||||
- (void)setCellRegistrationMapping:(NSDictionary<NSString *,Class> *)cellRegistrationMapping {
|
||||
NSParameterAssert(cellRegistrationMapping.count <= 1);
|
||||
_cellRegistrationMapping = cellRegistrationMapping;
|
||||
}
|
||||
|
||||
@end
|
19
Tweaks/FLEX/ObjectExplorers/Sections/FLEXObjectInfoSection.h
Normal file
19
Tweaks/FLEX/ObjectExplorers/Sections/FLEXObjectInfoSection.h
Normal file
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// FLEXObjectInfoSection.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 8/28/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
/// \c FLEXTableViewSection itself doesn't know about the object being explored.
|
||||
/// Subclasses might need this info to provide useful information about the object. Instead
|
||||
/// of adding an abstract class to the class hierarchy, subclasses can conform to this protocol
|
||||
/// to indicate that the only info they need to be initialized is the object being explored.
|
||||
@protocol FLEXObjectInfoSection <NSObject>
|
||||
|
||||
+ (instancetype)forObject:(id)object;
|
||||
|
||||
@end
|
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// FLEXBlockShortcuts.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 1/30/20.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXShortcutsSection.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/// Provides a description of the block's signature
|
||||
/// and access to an NSMethodSignature of the block
|
||||
@interface FLEXBlockShortcuts : FLEXShortcutsSection
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@@ -0,0 +1,59 @@
|
||||
//
|
||||
// FLEXBlockShortcuts.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 1/30/20.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXBlockShortcuts.h"
|
||||
#import "FLEXShortcut.h"
|
||||
#import "FLEXBlockDescription.h"
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
|
||||
#pragma mark -
|
||||
@implementation FLEXBlockShortcuts
|
||||
|
||||
#pragma mark Overrides
|
||||
|
||||
+ (instancetype)forObject:(id)block {
|
||||
NSParameterAssert([block isKindOfClass:NSClassFromString(@"NSBlock")]);
|
||||
|
||||
FLEXBlockDescription *blockInfo = [FLEXBlockDescription describing:block];
|
||||
NSMethodSignature *signature = blockInfo.signature;
|
||||
NSArray *blockShortcutRows = @[blockInfo.summary];
|
||||
|
||||
if (signature) {
|
||||
blockShortcutRows = @[
|
||||
blockInfo.summary,
|
||||
blockInfo.sourceDeclaration,
|
||||
signature.debugDescription,
|
||||
[FLEXActionShortcut title:@"View Method Signature"
|
||||
subtitle:^NSString *(id block) {
|
||||
return signature.description ?: @"unsupported signature";
|
||||
}
|
||||
viewer:^UIViewController *(id block) {
|
||||
return [FLEXObjectExplorerFactory explorerViewControllerForObject:signature];
|
||||
}
|
||||
accessoryType:^UITableViewCellAccessoryType(id view) {
|
||||
if (signature) {
|
||||
return UITableViewCellAccessoryDisclosureIndicator;
|
||||
}
|
||||
return UITableViewCellAccessoryNone;
|
||||
}
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
return [self forObject:block additionalRows:blockShortcutRows];
|
||||
}
|
||||
|
||||
- (NSString *)title {
|
||||
return @"Metadata";
|
||||
}
|
||||
|
||||
- (NSInteger)numberOfLines {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@end
|
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// FLEXBundleShortcuts.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 12/12/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXShortcutsSection.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/// Provides a "Browse Bundle Directory" action
|
||||
@interface FLEXBundleShortcuts : FLEXShortcutsSection
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@@ -0,0 +1,114 @@
|
||||
//
|
||||
// FLEXBundleShortcuts.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 12/12/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXBundleShortcuts.h"
|
||||
#import "FLEXShortcut.h"
|
||||
#import "FLEXAlert.h"
|
||||
#import "FLEXMacros.h"
|
||||
#import "FLEXRuntimeExporter.h"
|
||||
#import "FLEXTableListViewController.h"
|
||||
#import "FLEXFileBrowserController.h"
|
||||
|
||||
#pragma mark -
|
||||
@implementation FLEXBundleShortcuts
|
||||
#pragma mark Overrides
|
||||
|
||||
+ (instancetype)forObject:(NSBundle *)bundle { weakify(self)
|
||||
return [self forObject:bundle additionalRows:@[
|
||||
[FLEXActionShortcut
|
||||
title:@"Browse Bundle Directory" subtitle:nil
|
||||
viewer:^UIViewController *(NSBundle *bundle) {
|
||||
return [FLEXFileBrowserController path:bundle.bundlePath];
|
||||
}
|
||||
accessoryType:^UITableViewCellAccessoryType(NSBundle *bundle) {
|
||||
return UITableViewCellAccessoryDisclosureIndicator;
|
||||
}
|
||||
],
|
||||
[FLEXActionShortcut title:@"Browse Bundle as Database…" subtitle:nil
|
||||
selectionHandler:^(UIViewController *host, NSBundle *bundle) { strongify(self)
|
||||
[self promptToExportBundleAsDatabase:bundle host:host];
|
||||
}
|
||||
accessoryType:^UITableViewCellAccessoryType(NSBundle *bundle) {
|
||||
return UITableViewCellAccessoryDisclosureIndicator;
|
||||
}
|
||||
],
|
||||
]];
|
||||
}
|
||||
|
||||
+ (void)promptToExportBundleAsDatabase:(NSBundle *)bundle host:(UIViewController *)host {
|
||||
[FLEXAlert makeAlert:^(FLEXAlert *make) {
|
||||
make.title(@"Save As…").message(
|
||||
@"The database be saved in the Library folder. "
|
||||
"Depending on the number of classes, it may take "
|
||||
"10 minutes or more to finish exporting. 20,000 "
|
||||
"classes takes about 7 minutes."
|
||||
);
|
||||
make.configuredTextField(^(UITextField *field) {
|
||||
field.placeholder = @"FLEXRuntimeExport.objc.db";
|
||||
field.text = [NSString stringWithFormat:
|
||||
@"%@.objc.db", bundle.executablePath.lastPathComponent
|
||||
];
|
||||
});
|
||||
make.button(@"Start").handler(^(NSArray<NSString *> *strings) {
|
||||
[self browseBundleAsDatabase:bundle host:host name:strings[0]];
|
||||
});
|
||||
make.button(@"Cancel").cancelStyle();
|
||||
} showFrom:host];
|
||||
}
|
||||
|
||||
+ (void)browseBundleAsDatabase:(NSBundle *)bundle host:(UIViewController *)host name:(NSString *)name {
|
||||
NSParameterAssert(name.length);
|
||||
|
||||
UIAlertController *progress = [FLEXAlert makeAlert:^(FLEXAlert *make) {
|
||||
make.title(@"Generating Database");
|
||||
// Some iOS version glitch out of there is
|
||||
// no initial message and you add one later
|
||||
make.message(@"…");
|
||||
}];
|
||||
|
||||
[host presentViewController:progress animated:YES completion:^{
|
||||
// Generate path to store db
|
||||
NSString *path = [NSSearchPathForDirectoriesInDomains(
|
||||
NSLibraryDirectory, NSUserDomainMask, YES
|
||||
)[0] stringByAppendingPathComponent:name];
|
||||
|
||||
progress.message = [path stringByAppendingString:@"\n\nCreating database…"];
|
||||
|
||||
// Generate db and show progress
|
||||
[FLEXRuntimeExporter createRuntimeDatabaseAtPath:path
|
||||
forImages:@[bundle.executablePath]
|
||||
progressHandler:^(NSString *status) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
progress.message = [progress.message
|
||||
stringByAppendingFormat:@"\n%@", status
|
||||
];
|
||||
[progress.view setNeedsLayout];
|
||||
[progress.view layoutIfNeeded];
|
||||
});
|
||||
} completion:^(NSString *error) {
|
||||
// Display error if any
|
||||
if (error) {
|
||||
progress.title = @"Error";
|
||||
progress.message = error;
|
||||
[progress addAction:[UIAlertAction
|
||||
actionWithTitle:@"OK" style:UIAlertActionStyleCancel handler:nil]
|
||||
];
|
||||
}
|
||||
// Browse database
|
||||
else {
|
||||
[progress dismissViewControllerAnimated:YES completion:nil];
|
||||
[host.navigationController pushViewController:[
|
||||
[FLEXTableListViewController alloc] initWithPath:path
|
||||
] animated:YES];
|
||||
}
|
||||
}
|
||||
];
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// FLEXClassShortcuts.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 11/22/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXShortcutsSection.h"
|
||||
|
||||
/// Provides handy shortcuts for class objects.
|
||||
/// This is the default section used for all class objects.
|
||||
@interface FLEXClassShortcuts : FLEXShortcutsSection
|
||||
|
||||
+ (instancetype)forObject:(Class)cls;
|
||||
|
||||
@end
|
@@ -0,0 +1,73 @@
|
||||
//
|
||||
// FLEXClassShortcuts.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 11/22/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXClassShortcuts.h"
|
||||
#import "FLEXShortcut.h"
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
#import "FLEXObjectListViewController.h"
|
||||
#import "NSObject+FLEX_Reflection.h"
|
||||
|
||||
@interface FLEXClassShortcuts ()
|
||||
@property (nonatomic, readonly) Class cls;
|
||||
@end
|
||||
|
||||
@implementation FLEXClassShortcuts
|
||||
|
||||
+ (instancetype)forObject:(Class)cls {
|
||||
// These additional rows will appear at the beginning of the shortcuts section.
|
||||
// The methods below are written in such a way that they will not interfere
|
||||
// with properties/etc being registered alongside these
|
||||
return [self forObject:cls additionalRows:@[
|
||||
[FLEXActionShortcut title:@"Find Live Instances" subtitle:nil
|
||||
viewer:^UIViewController *(id obj) {
|
||||
return [FLEXObjectListViewController
|
||||
instancesOfClassWithName:NSStringFromClass(obj)
|
||||
retained:NO
|
||||
];
|
||||
}
|
||||
accessoryType:^UITableViewCellAccessoryType(id obj) {
|
||||
return UITableViewCellAccessoryDisclosureIndicator;
|
||||
}
|
||||
],
|
||||
[FLEXActionShortcut title:@"List Subclasses" subtitle:nil
|
||||
viewer:^UIViewController *(id obj) {
|
||||
NSString *name = NSStringFromClass(obj);
|
||||
return [FLEXObjectListViewController subclassesOfClassWithName:name];
|
||||
}
|
||||
accessoryType:^UITableViewCellAccessoryType(id view) {
|
||||
return UITableViewCellAccessoryDisclosureIndicator;
|
||||
}
|
||||
],
|
||||
[FLEXActionShortcut title:@"Explore Bundle for Class"
|
||||
subtitle:^NSString *(id obj) {
|
||||
return [self shortNameForBundlePath:[NSBundle bundleForClass:obj].executablePath];
|
||||
}
|
||||
viewer:^UIViewController *(id obj) {
|
||||
NSBundle *bundle = [NSBundle bundleForClass:obj];
|
||||
return [FLEXObjectExplorerFactory explorerViewControllerForObject:bundle];
|
||||
}
|
||||
accessoryType:^UITableViewCellAccessoryType(id view) {
|
||||
return UITableViewCellAccessoryDisclosureIndicator;
|
||||
}
|
||||
],
|
||||
]];
|
||||
}
|
||||
|
||||
+ (NSString *)shortNameForBundlePath:(NSString *)imageName {
|
||||
NSArray<NSString *> *components = [imageName componentsSeparatedByString:@"/"];
|
||||
if (components.count >= 2) {
|
||||
return [NSString stringWithFormat:@"%@/%@",
|
||||
components[components.count - 2],
|
||||
components[components.count - 1]
|
||||
];
|
||||
}
|
||||
|
||||
return imageName.lastPathComponent;
|
||||
}
|
||||
|
||||
@end
|
@@ -0,0 +1,16 @@
|
||||
//
|
||||
// FLEXImageShortcuts.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 8/29/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXShortcutsSection.h"
|
||||
|
||||
/// Provides "view image" and "save image" shortcuts for UIImage objects
|
||||
@interface FLEXImageShortcuts : FLEXShortcutsSection
|
||||
|
||||
+ (instancetype)forObject:(UIImage *)image;
|
||||
|
||||
@end
|
@@ -0,0 +1,68 @@
|
||||
//
|
||||
// FLEXImageShortcuts.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 8/29/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXImageShortcuts.h"
|
||||
#import "FLEXImagePreviewViewController.h"
|
||||
#import "FLEXShortcut.h"
|
||||
#import "FLEXAlert.h"
|
||||
#import "FLEXMacros.h"
|
||||
|
||||
@interface UIAlertController (FLEXImageShortcuts)
|
||||
- (void)flex_image:(UIImage *)image disSaveWithError:(NSError *)error :(void *)context;
|
||||
@end
|
||||
|
||||
@implementation FLEXImageShortcuts
|
||||
|
||||
#pragma mark - Overrides
|
||||
|
||||
+ (instancetype)forObject:(UIImage *)image {
|
||||
// These additional rows will appear at the beginning of the shortcuts section.
|
||||
// The methods below are written in such a way that they will not interfere
|
||||
// with properties/etc being registered alongside these
|
||||
return [self forObject:image additionalRows:@[
|
||||
[FLEXActionShortcut title:@"View Image" subtitle:nil
|
||||
viewer:^UIViewController *(id image) {
|
||||
return [FLEXImagePreviewViewController forImage:image];
|
||||
}
|
||||
accessoryType:^UITableViewCellAccessoryType(id image) {
|
||||
return UITableViewCellAccessoryDisclosureIndicator;
|
||||
}
|
||||
],
|
||||
[FLEXActionShortcut title:@"Save Image" subtitle:nil
|
||||
selectionHandler:^(UIViewController *host, id image) {
|
||||
// Present modal alerting user about saving
|
||||
UIAlertController *alert = [FLEXAlert makeAlert:^(FLEXAlert *make) {
|
||||
make.title(@"Saving Image…");
|
||||
}];
|
||||
[host presentViewController:alert animated:YES completion:nil];
|
||||
|
||||
// Save the image
|
||||
UIImageWriteToSavedPhotosAlbum(
|
||||
image, alert, @selector(flex_image:disSaveWithError::), nil
|
||||
);
|
||||
}
|
||||
accessoryType:^UITableViewCellAccessoryType(id image) {
|
||||
return UITableViewCellAccessoryDisclosureIndicator;
|
||||
}
|
||||
]
|
||||
]];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation UIAlertController (FLEXImageShortcuts)
|
||||
|
||||
- (void)flex_image:(UIImage *)image disSaveWithError:(NSError *)error :(void *)context {
|
||||
self.title = @"Image Saved";
|
||||
flex_dispatch_after(1, dispatch_get_main_queue(), ^{
|
||||
[self dismissViewControllerAnimated:YES completion:nil];
|
||||
});
|
||||
}
|
||||
|
||||
@end
|
@@ -0,0 +1,15 @@
|
||||
//
|
||||
// FLEXLayerShortcuts.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 12/12/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXShortcutsSection.h"
|
||||
|
||||
@interface FLEXLayerShortcuts : FLEXShortcutsSection
|
||||
|
||||
+ (instancetype)forObject:(CALayer *)layer;
|
||||
|
||||
@end
|
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// FLEXLayerShortcuts.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 12/12/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXLayerShortcuts.h"
|
||||
#import "FLEXShortcut.h"
|
||||
#import "FLEXImagePreviewViewController.h"
|
||||
|
||||
@implementation FLEXLayerShortcuts
|
||||
|
||||
+ (instancetype)forObject:(CALayer *)layer {
|
||||
return [self forObject:layer additionalRows:@[
|
||||
[FLEXActionShortcut title:@"Preview Image" subtitle:nil
|
||||
viewer:^UIViewController *(CALayer *layer) {
|
||||
return [FLEXImagePreviewViewController previewForLayer:layer];
|
||||
}
|
||||
accessoryType:^UITableViewCellAccessoryType(CALayer *layer) {
|
||||
return CGRectIsEmpty(layer.bounds) ? UITableViewCellAccessoryNone : UITableViewCellAccessoryDisclosureIndicator;
|
||||
}
|
||||
]
|
||||
]];
|
||||
}
|
||||
|
||||
@end
|
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// FLEXNSDataShortcuts.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 3/29/21.
|
||||
//
|
||||
|
||||
#import "FLEXShortcutsSection.h"
|
||||
|
||||
/// Adds a "UTF-8 String" shortcut
|
||||
@interface FLEXNSDataShortcuts : FLEXShortcutsSection
|
||||
|
||||
@end
|
@@ -0,0 +1,48 @@
|
||||
//
|
||||
// FLEXNSDataShortcuts.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 3/29/21.
|
||||
//
|
||||
|
||||
#import "FLEXNSDataShortcuts.h"
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
#import "FLEXShortcut.h"
|
||||
|
||||
@implementation FLEXNSDataShortcuts
|
||||
|
||||
+ (instancetype)forObject:(NSData *)data {
|
||||
NSString *string = [self stringForData:data];
|
||||
|
||||
return [self forObject:data additionalRows:@[
|
||||
[FLEXActionShortcut title:@"UTF-8 String" subtitle:^(NSData *object) {
|
||||
return string.length ? string : (string ?
|
||||
@"Data is not a UTF8 String" : @"Empty string"
|
||||
);
|
||||
} viewer:^UIViewController *(id object) {
|
||||
return [FLEXObjectExplorerFactory explorerViewControllerForObject:string];
|
||||
} accessoryType:^UITableViewCellAccessoryType(NSData *object) {
|
||||
if (string.length) {
|
||||
return UITableViewCellAccessoryDisclosureIndicator;
|
||||
}
|
||||
|
||||
return UITableViewCellAccessoryNone;
|
||||
}]
|
||||
]];
|
||||
}
|
||||
|
||||
+ (NSString *)stringForData:(NSData *)data {
|
||||
return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface NSData (Overrides) @end
|
||||
@implementation NSData (Overrides)
|
||||
|
||||
// This normally crashes
|
||||
- (NSUInteger)length {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@end
|
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// FLEXNSStringShortcuts.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 3/29/21.
|
||||
//
|
||||
|
||||
#import "FLEXShortcutsSection.h"
|
||||
|
||||
/// Adds a "UTF-8 Data" shortcut
|
||||
@interface FLEXNSStringShortcuts : FLEXShortcutsSection
|
||||
|
||||
@end
|
@@ -0,0 +1,29 @@
|
||||
//
|
||||
// FLEXNSStringShortcuts.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 3/29/21.
|
||||
//
|
||||
|
||||
#import "FLEXNSStringShortcuts.h"
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
#import "FLEXShortcut.h"
|
||||
|
||||
@implementation FLEXNSStringShortcuts
|
||||
|
||||
+ (instancetype)forObject:(NSString *)string {
|
||||
NSUInteger length = [string lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
|
||||
NSData *data = [NSData dataWithBytesNoCopy:(void *)string.UTF8String length:length freeWhenDone:NO];
|
||||
|
||||
return [self forObject:string additionalRows:@[
|
||||
[FLEXActionShortcut title:@"UTF-8 Data" subtitle:^NSString *(id _) {
|
||||
return data.description;
|
||||
} viewer:^UIViewController *(id _) {
|
||||
return [FLEXObjectExplorerFactory explorerViewControllerForObject:data];
|
||||
} accessoryType:^UITableViewCellAccessoryType(id _) {
|
||||
return UITableViewCellAccessoryDisclosureIndicator;
|
||||
}]
|
||||
]];
|
||||
}
|
||||
|
||||
@end
|
@@ -0,0 +1,71 @@
|
||||
//
|
||||
// FLEXShortcut.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 12/10/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXObjectExplorer.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/// Represents a row in a shortcut section.
|
||||
///
|
||||
/// The purpsoe of this protocol is to allow delegating a small
|
||||
/// subset of the responsibilities of a \c FLEXShortcutsSection
|
||||
/// to another object, for a single arbitrary row.
|
||||
///
|
||||
/// It is useful to make your own shortcuts to append/prepend
|
||||
/// them to the existing list of shortcuts for a class.
|
||||
@protocol FLEXShortcut <FLEXObjectExplorerItem>
|
||||
|
||||
- (nonnull NSString *)titleWith:(id)object;
|
||||
- (nullable NSString *)subtitleWith:(id)object;
|
||||
- (nullable void (^)(UIViewController *host))didSelectActionWith:(id)object;
|
||||
/// Called when the row is selected
|
||||
- (nullable UIViewController *)viewerWith:(id)object;
|
||||
/// Basically, whether or not to show a detail disclosure indicator
|
||||
- (UITableViewCellAccessoryType)accessoryTypeWith:(id)object;
|
||||
/// If nil is returned, the default reuse identifier is used
|
||||
- (nullable NSString *)customReuseIdentifierWith:(id)object;
|
||||
|
||||
@optional
|
||||
/// Called when the (i) button is pressed if the accessory type includes it
|
||||
- (UIViewController *)editorWith:(id)object forSection:(FLEXTableViewSection *)section;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
/// Provides default behavior for FLEX metadata objects. Also works in a limited way with strings.
|
||||
/// Used internally. If you wish to use this object, only pass in \c FLEX* metadata objects.
|
||||
@interface FLEXShortcut : NSObject <FLEXShortcut>
|
||||
|
||||
/// @param item An \c NSString or \c FLEX* metadata object.
|
||||
/// @note You may also pass a \c FLEXShortcut conforming object,
|
||||
/// and that object will be returned instead.
|
||||
+ (id<FLEXShortcut>)shortcutFor:(id)item;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
/// Provides a quick and dirty implementation of the \c FLEXShortcut protocol,
|
||||
/// allowing you to specify a static title and dynamic atttributes for everything else.
|
||||
/// The object passed into each block is the object passed to each \c FLEXShortcut method.
|
||||
///
|
||||
/// Does not support the \c -editorWith: method.
|
||||
@interface FLEXActionShortcut : NSObject <FLEXShortcut>
|
||||
|
||||
+ (instancetype)title:(NSString *)title
|
||||
subtitle:(nullable NSString *(^)(id object))subtitleFuture
|
||||
viewer:(nullable UIViewController *(^)(id object))viewerFuture
|
||||
accessoryType:(nullable UITableViewCellAccessoryType(^)(id object))accessoryTypeFuture;
|
||||
|
||||
+ (instancetype)title:(NSString *)title
|
||||
subtitle:(nullable NSString *(^)(id object))subtitleFuture
|
||||
selectionHandler:(nullable void (^)(UIViewController *host, id object))tapAction
|
||||
accessoryType:(nullable UITableViewCellAccessoryType(^)(id object))accessoryTypeFuture;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
254
Tweaks/FLEX/ObjectExplorers/Sections/Shortcuts/FLEXShortcut.m
Normal file
254
Tweaks/FLEX/ObjectExplorers/Sections/Shortcuts/FLEXShortcut.m
Normal file
@@ -0,0 +1,254 @@
|
||||
//
|
||||
// FLEXShortcut.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 12/10/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXShortcut.h"
|
||||
#import "FLEXProperty.h"
|
||||
#import "FLEXPropertyAttributes.h"
|
||||
#import "FLEXIvar.h"
|
||||
#import "FLEXMethod.h"
|
||||
#import "FLEXRuntime+UIKitHelpers.h"
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
#import "FLEXFieldEditorViewController.h"
|
||||
#import "FLEXMethodCallingViewController.h"
|
||||
#import "FLEXMetadataSection.h"
|
||||
#import "FLEXTableView.h"
|
||||
|
||||
|
||||
#pragma mark - FLEXShortcut
|
||||
|
||||
@interface FLEXShortcut () {
|
||||
id _item;
|
||||
}
|
||||
|
||||
@property (nonatomic, readonly) FLEXMetadataKind metadataKind;
|
||||
@property (nonatomic, readonly) FLEXProperty *property;
|
||||
@property (nonatomic, readonly) FLEXMethod *method;
|
||||
@property (nonatomic, readonly) FLEXIvar *ivar;
|
||||
@property (nonatomic, readonly) id<FLEXRuntimeMetadata> metadata;
|
||||
@end
|
||||
|
||||
@implementation FLEXShortcut
|
||||
@synthesize defaults = _defaults;
|
||||
|
||||
+ (id<FLEXShortcut>)shortcutFor:(id)item {
|
||||
if ([item conformsToProtocol:@protocol(FLEXShortcut)]) {
|
||||
return item;
|
||||
}
|
||||
|
||||
FLEXShortcut *shortcut = [self new];
|
||||
shortcut->_item = item;
|
||||
|
||||
if ([item isKindOfClass:[FLEXProperty class]]) {
|
||||
if (shortcut.property.isClassProperty) {
|
||||
shortcut->_metadataKind = FLEXMetadataKindClassProperties;
|
||||
} else {
|
||||
shortcut->_metadataKind = FLEXMetadataKindProperties;
|
||||
}
|
||||
}
|
||||
if ([item isKindOfClass:[FLEXIvar class]]) {
|
||||
shortcut->_metadataKind = FLEXMetadataKindIvars;
|
||||
}
|
||||
if ([item isKindOfClass:[FLEXMethod class]]) {
|
||||
// We don't care if it's a class method or not
|
||||
shortcut->_metadataKind = FLEXMetadataKindMethods;
|
||||
}
|
||||
|
||||
return shortcut;
|
||||
}
|
||||
|
||||
- (id)propertyOrIvarValue:(id)object {
|
||||
return [self.metadata currentValueWithTarget:object];
|
||||
}
|
||||
|
||||
- (NSString *)titleWith:(id)object {
|
||||
switch (self.metadataKind) {
|
||||
case FLEXMetadataKindClassProperties:
|
||||
case FLEXMetadataKindProperties:
|
||||
// Since we're outside of the "properties" section, prepend @property for clarity.
|
||||
return [@"@property " stringByAppendingString:[_item description]];
|
||||
|
||||
default:
|
||||
return [_item description];
|
||||
}
|
||||
|
||||
NSAssert(
|
||||
[_item isKindOfClass:[NSString class]],
|
||||
@"Unexpected type: %@", [_item class]
|
||||
);
|
||||
|
||||
return _item;
|
||||
}
|
||||
|
||||
- (NSString *)subtitleWith:(id)object {
|
||||
if (self.metadataKind) {
|
||||
return [self.metadata previewWithTarget:object];
|
||||
}
|
||||
|
||||
// Item is probably a string; must return empty string since
|
||||
// these will be gathered into an array. If the object is a
|
||||
// just a string, it doesn't get a subtitle.
|
||||
return @"";
|
||||
}
|
||||
|
||||
- (void (^)(UIViewController *))didSelectActionWith:(id)object {
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (UIViewController *)viewerWith:(id)object {
|
||||
NSAssert(self.metadataKind, @"Static titles cannot be viewed");
|
||||
return [self.metadata viewerWithTarget:object];
|
||||
}
|
||||
|
||||
- (UIViewController *)editorWith:(id)object forSection:(FLEXTableViewSection *)section {
|
||||
NSAssert(self.metadataKind, @"Static titles cannot be edited");
|
||||
return [self.metadata editorWithTarget:object section:section];
|
||||
}
|
||||
|
||||
- (UITableViewCellAccessoryType)accessoryTypeWith:(id)object {
|
||||
if (self.metadataKind) {
|
||||
return [self.metadata suggestedAccessoryTypeWithTarget:object];
|
||||
}
|
||||
|
||||
return UITableViewCellAccessoryNone;
|
||||
}
|
||||
|
||||
- (NSString *)customReuseIdentifierWith:(id)object {
|
||||
if (self.metadataKind) {
|
||||
return kFLEXCodeFontCell;
|
||||
}
|
||||
|
||||
return kFLEXMultilineCell;
|
||||
}
|
||||
|
||||
#pragma mark FLEXObjectExplorerDefaults
|
||||
|
||||
- (void)setDefaults:(FLEXObjectExplorerDefaults *)defaults {
|
||||
_defaults = defaults;
|
||||
|
||||
if (_metadataKind) {
|
||||
self.metadata.defaults = defaults;
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)isEditable {
|
||||
if (_metadataKind) {
|
||||
return self.metadata.isEditable;
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)isCallable {
|
||||
if (_metadataKind) {
|
||||
return self.metadata.isCallable;
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
#pragma mark - Helpers
|
||||
|
||||
- (FLEXProperty *)property { return _item; }
|
||||
- (FLEXMethodBase *)method { return _item; }
|
||||
- (FLEXIvar *)ivar { return _item; }
|
||||
- (id<FLEXRuntimeMetadata>)metadata { return _item; }
|
||||
|
||||
@end
|
||||
|
||||
|
||||
#pragma mark - FLEXActionShortcut
|
||||
|
||||
@interface FLEXActionShortcut ()
|
||||
@property (nonatomic, readonly) NSString *title;
|
||||
@property (nonatomic, readonly) NSString *(^subtitleFuture)(id);
|
||||
@property (nonatomic, readonly) UIViewController *(^viewerFuture)(id);
|
||||
@property (nonatomic, readonly) void (^selectionHandler)(UIViewController *, id);
|
||||
@property (nonatomic, readonly) UITableViewCellAccessoryType (^accessoryTypeFuture)(id);
|
||||
@end
|
||||
|
||||
@implementation FLEXActionShortcut
|
||||
@synthesize defaults = _defaults;
|
||||
|
||||
+ (instancetype)title:(NSString *)title
|
||||
subtitle:(NSString *(^)(id))subtitle
|
||||
viewer:(UIViewController *(^)(id))viewer
|
||||
accessoryType:(UITableViewCellAccessoryType (^)(id))type {
|
||||
return [[self alloc] initWithTitle:title subtitle:subtitle viewer:viewer selectionHandler:nil accessoryType:type];
|
||||
}
|
||||
|
||||
+ (instancetype)title:(NSString *)title
|
||||
subtitle:(NSString * (^)(id))subtitle
|
||||
selectionHandler:(void (^)(UIViewController *, id))tapAction
|
||||
accessoryType:(UITableViewCellAccessoryType (^)(id))type {
|
||||
return [[self alloc] initWithTitle:title subtitle:subtitle viewer:nil selectionHandler:tapAction accessoryType:type];
|
||||
}
|
||||
|
||||
- (id)initWithTitle:(NSString *)title
|
||||
subtitle:(id)subtitleFuture
|
||||
viewer:(id)viewerFuture
|
||||
selectionHandler:(id)tapAction
|
||||
accessoryType:(id)accessoryTypeFuture {
|
||||
NSParameterAssert(title.length);
|
||||
|
||||
self = [super init];
|
||||
if (self) {
|
||||
id nilBlock = ^id (id obj) { return nil; };
|
||||
|
||||
_title = title;
|
||||
_subtitleFuture = subtitleFuture ?: nilBlock;
|
||||
_viewerFuture = viewerFuture ?: nilBlock;
|
||||
_selectionHandler = tapAction;
|
||||
_accessoryTypeFuture = accessoryTypeFuture ?: nilBlock;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSString *)titleWith:(id)object {
|
||||
return self.title;
|
||||
}
|
||||
|
||||
- (NSString *)subtitleWith:(id)object {
|
||||
if (self.defaults.wantsDynamicPreviews) {
|
||||
return self.subtitleFuture(object);
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (void (^)(UIViewController *))didSelectActionWith:(id)object {
|
||||
if (self.selectionHandler) {
|
||||
return ^(UIViewController *host) {
|
||||
self.selectionHandler(host, object);
|
||||
};
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (UIViewController *)viewerWith:(id)object {
|
||||
return self.viewerFuture(object);
|
||||
}
|
||||
|
||||
- (UITableViewCellAccessoryType)accessoryTypeWith:(id)object {
|
||||
return self.accessoryTypeFuture(object);
|
||||
}
|
||||
|
||||
- (NSString *)customReuseIdentifierWith:(id)object {
|
||||
if (!self.subtitleFuture(object)) {
|
||||
// The text is more centered with this style if there is no subtitle
|
||||
return kFLEXDefaultCell;
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (BOOL)isEditable { return NO; }
|
||||
- (BOOL)isCallable { return NO; }
|
||||
|
||||
@end
|
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// FLEXShortcutsFactory+Defaults.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 8/29/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXShortcutsSection.h"
|
||||
|
||||
@interface FLEXShortcutsFactory (UIApplication) @end
|
||||
|
||||
@interface FLEXShortcutsFactory (Views) @end
|
||||
|
||||
@interface FLEXShortcutsFactory (ViewControllers) @end
|
||||
|
||||
@interface FLEXShortcutsFactory (UIImage) @end
|
||||
|
||||
@interface FLEXShortcutsFactory (NSBundle) @end
|
||||
|
||||
@interface FLEXShortcutsFactory (Classes) @end
|
||||
|
||||
@interface FLEXShortcutsFactory (Activities) @end
|
||||
|
||||
@interface FLEXShortcutsFactory (Blocks) @end
|
||||
|
||||
@interface FLEXShortcutsFactory (Foundation) @end
|
||||
|
||||
@interface FLEXShortcutsFactory (WebKit_Safari) @end
|
||||
|
||||
@interface FLEXShortcutsFactory (Pasteboard) @end
|
||||
|
||||
@interface FLEXShortcutsFactory (FirebaseFirestore) @end
|
@@ -0,0 +1,461 @@
|
||||
//
|
||||
// FLEXShortcutsFactory+Defaults.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 8/29/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXShortcutsFactory+Defaults.h"
|
||||
#import "FLEXShortcut.h"
|
||||
#import "FLEXMacros.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
#import "NSArray+FLEX.h"
|
||||
#import "NSObject+FLEX_Reflection.h"
|
||||
#import "FLEXObjcInternal.h"
|
||||
#import "Cocoa+FLEXShortcuts.h"
|
||||
|
||||
#pragma mark - UIApplication
|
||||
|
||||
@implementation FLEXShortcutsFactory (UIApplication)
|
||||
|
||||
+ (void)load { FLEX_EXIT_IF_NO_CTORS()
|
||||
// sharedApplication class property possibly not added
|
||||
// as a literal class property until iOS 10
|
||||
FLEXRuntimeUtilityTryAddObjectProperty(
|
||||
2, sharedApplication, UIApplication.flex_metaclass, UIApplication, PropertyKey(ReadOnly)
|
||||
);
|
||||
|
||||
self.append.classProperties(@[@"sharedApplication"]).forClass(UIApplication.flex_metaclass);
|
||||
self.append.properties(@[
|
||||
@"delegate", @"keyWindow", @"windows"
|
||||
]).forClass(UIApplication.class);
|
||||
|
||||
if (@available(iOS 13, *)) {
|
||||
self.append.properties(@[
|
||||
@"connectedScenes", @"openSessions", @"supportsMultipleScenes"
|
||||
]).forClass(UIApplication.class);
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark - Views
|
||||
|
||||
@implementation FLEXShortcutsFactory (Views)
|
||||
|
||||
+ (void)load { FLEX_EXIT_IF_NO_CTORS()
|
||||
// A quirk of UIView and some other classes: a lot of the `@property`s are
|
||||
// not actually properties from the perspective of the runtime.
|
||||
//
|
||||
// We add these properties to the class at runtime if they haven't been added yet.
|
||||
// This way, we can use our property editor to access and change them.
|
||||
// The property attributes match the declared attributes in their headers.
|
||||
|
||||
// UIView, public
|
||||
Class UIView_ = UIView.class;
|
||||
FLEXRuntimeUtilityTryAddNonatomicProperty(2, frame, UIView_, CGRect);
|
||||
FLEXRuntimeUtilityTryAddNonatomicProperty(2, alpha, UIView_, CGFloat);
|
||||
FLEXRuntimeUtilityTryAddNonatomicProperty(2, clipsToBounds, UIView_, BOOL);
|
||||
FLEXRuntimeUtilityTryAddNonatomicProperty(2, opaque, UIView_, BOOL, PropertyKeyGetter(isOpaque));
|
||||
FLEXRuntimeUtilityTryAddNonatomicProperty(2, hidden, UIView_, BOOL, PropertyKeyGetter(isHidden));
|
||||
FLEXRuntimeUtilityTryAddObjectProperty(2, backgroundColor, UIView_, UIColor, PropertyKey(Copy));
|
||||
FLEXRuntimeUtilityTryAddObjectProperty(6, constraints, UIView_, NSArray, PropertyKey(ReadOnly));
|
||||
FLEXRuntimeUtilityTryAddObjectProperty(2, subviews, UIView_, NSArray, PropertyKey(ReadOnly));
|
||||
FLEXRuntimeUtilityTryAddObjectProperty(2, superview, UIView_, UIView, PropertyKey(ReadOnly));
|
||||
FLEXRuntimeUtilityTryAddObjectProperty(7, tintColor, UIView_, UIView);
|
||||
|
||||
// UIButton, private
|
||||
FLEXRuntimeUtilityTryAddObjectProperty(2, font, UIButton.class, UIFont, PropertyKey(ReadOnly));
|
||||
|
||||
// Only available since iOS 3.2, but we never supported iOS 3, so who cares
|
||||
NSArray *ivars = @[@"_gestureRecognizers"];
|
||||
NSArray *methods = @[@"sizeToFit", @"setNeedsLayout", @"removeFromSuperview"];
|
||||
|
||||
// UIView
|
||||
self.append.ivars(ivars).methods(methods).properties(@[
|
||||
@"frame", @"bounds", @"center", @"transform",
|
||||
@"backgroundColor", @"alpha", @"opaque", @"hidden",
|
||||
@"clipsToBounds", @"userInteractionEnabled", @"layer",
|
||||
@"superview", @"subviews",
|
||||
@"accessibilityIdentifier", @"accessibilityLabel"
|
||||
]).forClass(UIView.class);
|
||||
|
||||
// UILabel
|
||||
self.append.ivars(ivars).methods(methods).properties(@[
|
||||
@"text", @"attributedText", @"font", @"frame",
|
||||
@"textColor", @"textAlignment", @"numberOfLines",
|
||||
@"lineBreakMode", @"enabled", @"backgroundColor",
|
||||
@"alpha", @"hidden", @"preferredMaxLayoutWidth",
|
||||
@"superview", @"subviews",
|
||||
@"accessibilityIdentifier", @"accessibilityLabel"
|
||||
]).forClass(UILabel.class);
|
||||
|
||||
// UIWindow
|
||||
self.append.ivars(ivars).properties(@[
|
||||
@"rootViewController", @"windowLevel", @"keyWindow",
|
||||
@"frame", @"bounds", @"center", @"transform",
|
||||
@"backgroundColor", @"alpha", @"opaque", @"hidden",
|
||||
@"clipsToBounds", @"userInteractionEnabled", @"layer",
|
||||
@"subviews"
|
||||
]).forClass(UIWindow.class);
|
||||
|
||||
if (@available(iOS 13, *)) {
|
||||
self.append.properties(@[@"windowScene"]).forClass(UIWindow.class);
|
||||
}
|
||||
|
||||
ivars = @[@"_targetActions", @"_gestureRecognizers"];
|
||||
|
||||
// Property was added in iOS 10 but we want it on iOS 9 too
|
||||
FLEXRuntimeUtilityTryAddObjectProperty(9, allTargets, UIControl.class, NSArray, PropertyKey(ReadOnly));
|
||||
|
||||
// UIControl
|
||||
self.append.ivars(ivars).methods(methods).properties(@[
|
||||
@"enabled", @"allTargets", @"frame",
|
||||
@"backgroundColor", @"hidden", @"clipsToBounds",
|
||||
@"userInteractionEnabled", @"superview", @"subviews",
|
||||
@"accessibilityIdentifier", @"accessibilityLabel"
|
||||
]).forClass(UIControl.class);
|
||||
|
||||
// UIButton
|
||||
self.append.ivars(ivars).properties(@[
|
||||
@"titleLabel", @"font", @"imageView", @"tintColor",
|
||||
@"currentTitle", @"currentImage", @"enabled", @"frame",
|
||||
@"superview", @"subviews",
|
||||
@"accessibilityIdentifier", @"accessibilityLabel"
|
||||
]).forClass(UIButton.class);
|
||||
|
||||
// UIImageView
|
||||
self.append.properties(@[
|
||||
@"image", @"animationImages", @"frame", @"bounds", @"center",
|
||||
@"transform", @"alpha", @"hidden", @"clipsToBounds",
|
||||
@"userInteractionEnabled", @"layer", @"superview", @"subviews",
|
||||
@"accessibilityIdentifier", @"accessibilityLabel"
|
||||
]).forClass(UIImageView.class);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
#pragma mark - View Controllers
|
||||
|
||||
@implementation FLEXShortcutsFactory (ViewControllers)
|
||||
|
||||
+ (void)load { FLEX_EXIT_IF_NO_CTORS()
|
||||
// toolbarItems is not really a property, make it one
|
||||
FLEXRuntimeUtilityTryAddObjectProperty(3, toolbarItems, UIViewController.class, NSArray);
|
||||
|
||||
// UIViewController
|
||||
self.append
|
||||
.properties(@[
|
||||
@"viewIfLoaded", @"title", @"navigationItem", @"toolbarItems", @"tabBarItem",
|
||||
@"childViewControllers", @"navigationController", @"tabBarController", @"splitViewController",
|
||||
@"parentViewController", @"presentedViewController", @"presentingViewController",
|
||||
])
|
||||
.methods(@[@"view"])
|
||||
.forClass(UIViewController.class);
|
||||
|
||||
// UIAlertController
|
||||
NSMutableArray *alertControllerProps = @[
|
||||
@"title", @"message", @"actions", @"textFields",
|
||||
@"preferredAction", @"presentingViewController", @"viewIfLoaded",
|
||||
].mutableCopy;
|
||||
if (@available(iOS 14.0, *)) {
|
||||
[alertControllerProps insertObject:@"image" atIndex:4];
|
||||
}
|
||||
self.append
|
||||
.properties(alertControllerProps)
|
||||
.methods(@[@"addAction:"])
|
||||
.forClass(UIAlertController.class);
|
||||
self.append.properties(@[
|
||||
@"title", @"style", @"enabled", @"flex_styleName",
|
||||
@"image", @"keyCommandInput", @"_isPreferred", @"_alertController",
|
||||
]).forClass(UIAlertAction.class);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
#pragma mark - UIImage
|
||||
|
||||
@implementation FLEXShortcutsFactory (UIImage)
|
||||
|
||||
+ (void)load { FLEX_EXIT_IF_NO_CTORS()
|
||||
self.append.methods(@[
|
||||
@"CGImage", @"CIImage"
|
||||
]).properties(@[
|
||||
@"scale", @"size", @"capInsets",
|
||||
@"alignmentRectInsets", @"duration", @"images"
|
||||
]).forClass(UIImage.class);
|
||||
|
||||
if (@available(iOS 13, *)) {
|
||||
self.append.properties(@[@"symbolImage"]).forClass(UIImage.class);
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
#pragma mark - NSBundle
|
||||
|
||||
@implementation FLEXShortcutsFactory (NSBundle)
|
||||
|
||||
+ (void)load { FLEX_EXIT_IF_NO_CTORS()
|
||||
self.append.properties(@[
|
||||
@"bundleIdentifier", @"principalClass",
|
||||
@"infoDictionary", @"bundlePath",
|
||||
@"executablePath", @"loaded"
|
||||
]).forClass(NSBundle.class);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
#pragma mark - Classes
|
||||
|
||||
@implementation FLEXShortcutsFactory (Classes)
|
||||
|
||||
+ (void)load { FLEX_EXIT_IF_NO_CTORS()
|
||||
self.append.classMethods(@[@"new", @"alloc"]).forClass(NSObject.flex_metaclass);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
#pragma mark - Activities
|
||||
|
||||
@implementation FLEXShortcutsFactory (Activities)
|
||||
|
||||
+ (void)load { FLEX_EXIT_IF_NO_CTORS()
|
||||
// Property was added in iOS 10 but we want it on iOS 9 too
|
||||
FLEXRuntimeUtilityTryAddNonatomicProperty(9, item, UIActivityItemProvider.class, id, PropertyKey(ReadOnly));
|
||||
|
||||
self.append.properties(@[
|
||||
@"item", @"placeholderItem", @"activityType"
|
||||
]).forClass(UIActivityItemProvider.class);
|
||||
|
||||
self.append.properties(@[
|
||||
@"activityItems", @"applicationActivities", @"excludedActivityTypes", @"completionHandler"
|
||||
]).forClass(UIActivityViewController.class);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
#pragma mark - Blocks
|
||||
|
||||
@implementation FLEXShortcutsFactory (Blocks)
|
||||
|
||||
+ (void)load { FLEX_EXIT_IF_NO_CTORS()
|
||||
self.append.methods(@[@"invoke"]).forClass(NSClassFromString(@"NSBlock"));
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark - Foundation
|
||||
|
||||
@implementation FLEXShortcutsFactory (Foundation)
|
||||
|
||||
+ (void)load { FLEX_EXIT_IF_NO_CTORS()
|
||||
self.append.properties(@[
|
||||
@"configuration", @"delegate", @"delegateQueue", @"sessionDescription",
|
||||
]).methods(@[
|
||||
@"dataTaskWithURL:", @"finishTasksAndInvalidate", @"invalidateAndCancel",
|
||||
]).forClass(NSURLSession.class);
|
||||
|
||||
self.append.methods(@[
|
||||
@"cachedResponseForRequest:", @"storeCachedResponse:forRequest:",
|
||||
@"storeCachedResponse:forDataTask:", @"removeCachedResponseForRequest:",
|
||||
@"removeCachedResponseForDataTask:", @"removeCachedResponsesSinceDate:",
|
||||
@"removeAllCachedResponses",
|
||||
]).forClass(NSURLCache.class);
|
||||
|
||||
|
||||
self.append.methods(@[
|
||||
@"postNotification:", @"postNotificationName:object:userInfo:",
|
||||
@"addObserver:selector:name:object:", @"removeObserver:",
|
||||
@"removeObserver:name:object:",
|
||||
]).forClass(NSNotificationCenter.class);
|
||||
|
||||
// NSTimeZone class properties aren't real properties
|
||||
FLEXRuntimeUtilityTryAddObjectProperty(2, localTimeZone, NSTimeZone.flex_metaclass, NSTimeZone);
|
||||
FLEXRuntimeUtilityTryAddObjectProperty(2, systemTimeZone, NSTimeZone.flex_metaclass, NSTimeZone);
|
||||
FLEXRuntimeUtilityTryAddObjectProperty(2, defaultTimeZone, NSTimeZone.flex_metaclass, NSTimeZone);
|
||||
FLEXRuntimeUtilityTryAddObjectProperty(2, knownTimeZoneNames, NSTimeZone.flex_metaclass, NSArray);
|
||||
FLEXRuntimeUtilityTryAddObjectProperty(2, abbreviationDictionary, NSTimeZone.flex_metaclass, NSDictionary);
|
||||
|
||||
self.append.classMethods(@[
|
||||
@"timeZoneWithName:", @"timeZoneWithAbbreviation:", @"timeZoneForSecondsFromGMT:",
|
||||
]).forClass(NSTimeZone.flex_metaclass);
|
||||
|
||||
self.append.classProperties(@[
|
||||
@"defaultTimeZone", @"systemTimeZone", @"localTimeZone",
|
||||
]).forClass(NSTimeZone.class);
|
||||
|
||||
// UTF8String is not a real property under the hood
|
||||
FLEXRuntimeUtilityTryAddNonatomicProperty(2, UTF8String, NSString.class, const char *, PropertyKey(ReadOnly));
|
||||
|
||||
self.append.properties(@[@"length"]).methods(@[@"characterAtIndex:"]).forClass(NSString.class);
|
||||
self.append.methods(@[
|
||||
@"writeToFile:atomically:", @"subdataWithRange:", @"isEqualToData:",
|
||||
]).properties(@[
|
||||
@"length", @"bytes",
|
||||
]).forClass(NSData.class);
|
||||
|
||||
self.append.classMethods(@[
|
||||
@"dataWithJSONObject:options:error:",
|
||||
@"JSONObjectWithData:options:error:",
|
||||
@"isValidJSONObject:",
|
||||
]).forClass(NSJSONSerialization.class);
|
||||
|
||||
// NSArray
|
||||
self.append.classMethods(@[
|
||||
@"arrayWithObject:", @"arrayWithContentsOfFile:"
|
||||
]).forClass(NSArray.flex_metaclass);
|
||||
self.append.methods(@[
|
||||
@"valueForKeyPath:", @"subarrayWithRange:",
|
||||
@"arrayByAddingObject:", @"arrayByAddingObjectsFromArray:",
|
||||
@"filteredArrayUsingPredicate:", @"subarrayWithRange:",
|
||||
@"containsObject:", @"objectAtIndex:", @"indexOfObject:",
|
||||
@"makeObjectsPerformSelector:", @"makeObjectsPerformSelector:withObject:",
|
||||
@"sortedArrayUsingSelector:", @"reverseObjectEnumerator",
|
||||
@"isEqualToArray:", @"mutableCopy",
|
||||
]).forClass(NSArray.class);
|
||||
// NSDictionary
|
||||
self.append.methods(@[
|
||||
@"objectForKey:", @"valueForKeyPath:",
|
||||
@"isEqualToDictionary:", @"mutableCopy",
|
||||
]).forClass(NSDictionary.class);
|
||||
// NSSet
|
||||
self.append.classMethods(@[
|
||||
@"setWithObject:", @"setWithArray:"
|
||||
]).forClass(NSSet.flex_metaclass);
|
||||
self.append.methods(@[
|
||||
@"allObjects", @"valueForKeyPath:", @"containsObject:",
|
||||
@"setByAddingObject:", @"setByAddingObjectsFromArray:",
|
||||
@"filteredSetUsingPredicate:", @"isSubsetOfSet:",
|
||||
@"makeObjectsPerformSelector:", @"makeObjectsPerformSelector:withObject:",
|
||||
@"reverseObjectEnumerator", @"isEqualToSet:", @"mutableCopy",
|
||||
]).forClass(NSSet.class);
|
||||
|
||||
// NSMutableArray
|
||||
self.prepend.methods(@[
|
||||
@"addObject:", @"insertObject:atIndex:", @"addObjectsFromArray:",
|
||||
@"removeObject:", @"removeObjectAtIndex:",
|
||||
@"removeObjectsInArray:", @"removeAllObjects",
|
||||
@"removeLastObject", @"filterUsingPredicate:",
|
||||
@"sortUsingSelector:", @"copy",
|
||||
]).forClass(NSMutableArray.class);
|
||||
// NSMutableDictionary
|
||||
self.prepend.methods(@[
|
||||
@"setObject:forKey:", @"removeObjectForKey:",
|
||||
@"removeAllObjects", @"removeObjectsForKeys:", @"copy",
|
||||
]).forClass(NSMutableDictionary.class);
|
||||
// NSMutableSet
|
||||
self.prepend.methods(@[
|
||||
@"addObject:", @"removeObject:", @"filterUsingPredicate:",
|
||||
@"removeAllObjects", @"addObjectsFromArray:",
|
||||
@"unionSet:", @"minusSet:", @"intersectSet:", @"copy"
|
||||
]).forClass(NSMutableSet.class);
|
||||
|
||||
self.append.methods(@[@"nextObject", @"allObjects"]).forClass(NSEnumerator.class);
|
||||
|
||||
self.append.properties(@[@"flex_observers"]).forClass(NSNotificationCenter.class);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark - WebKit / Safari
|
||||
|
||||
@implementation FLEXShortcutsFactory (WebKit_Safari)
|
||||
|
||||
+ (void)load { FLEX_EXIT_IF_NO_CTORS()
|
||||
Class WKWebView = NSClassFromString(@"WKWebView");
|
||||
Class SafariVC = NSClassFromString(@"SFSafariViewController");
|
||||
|
||||
if (WKWebView) {
|
||||
self.append.properties(@[
|
||||
@"configuration", @"scrollView", @"title", @"URL",
|
||||
@"customUserAgent", @"navigationDelegate"
|
||||
]).methods(@[@"reload", @"stopLoading"]).forClass(WKWebView);
|
||||
}
|
||||
|
||||
if (SafariVC) {
|
||||
self.append.properties(@[
|
||||
@"delegate"
|
||||
]).forClass(SafariVC);
|
||||
if (@available(iOS 10.0, *)) {
|
||||
self.append.properties(@[
|
||||
@"preferredBarTintColor", @"preferredControlTintColor"
|
||||
]).forClass(SafariVC);
|
||||
}
|
||||
if (@available(iOS 11.0, *)) {
|
||||
self.append.properties(@[
|
||||
@"configuration", @"dismissButtonStyle"
|
||||
]).forClass(SafariVC);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark - Pasteboard
|
||||
|
||||
@implementation FLEXShortcutsFactory (Pasteboard)
|
||||
|
||||
+ (void)load { FLEX_EXIT_IF_NO_CTORS()
|
||||
self.append.properties(@[
|
||||
@"name", @"numberOfItems", @"items",
|
||||
@"string", @"image", @"color", @"URL",
|
||||
]).forClass(UIPasteboard.class);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface NSNotificationCenter (Observers)
|
||||
@property (readonly) NSArray<NSString *> *flex_observers;
|
||||
@end
|
||||
|
||||
@implementation NSNotificationCenter (Observers)
|
||||
- (id)flex_observers {
|
||||
NSString *debug = self.debugDescription;
|
||||
NSArray<NSString *> *observers = [debug componentsSeparatedByString:@"\n"];
|
||||
NSArray<NSArray<NSString *> *> *splitObservers = [observers flex_mapped:^id(NSString *entry, NSUInteger idx) {
|
||||
return [entry componentsSeparatedByString:@","];
|
||||
}];
|
||||
|
||||
NSArray *names = [splitObservers flex_mapped:^id(NSArray<NSString *> *entry, NSUInteger idx) {
|
||||
return entry[0];
|
||||
}];
|
||||
NSArray *objects = [splitObservers flex_mapped:^id(NSArray<NSString *> *entry, NSUInteger idx) {
|
||||
if (entry.count < 2) return NSNull.null;
|
||||
NSScanner *scanner = [NSScanner scannerWithString:entry[1]];
|
||||
|
||||
unsigned long long objectPointerValue;
|
||||
if ([scanner scanHexLongLong:&objectPointerValue]) {
|
||||
void *objectPointer = (void *)objectPointerValue;
|
||||
if (FLEXPointerIsValidObjcObject(objectPointer))
|
||||
return (__bridge id)(void *)objectPointer;
|
||||
}
|
||||
|
||||
return NSNull.null;
|
||||
}];
|
||||
|
||||
return [NSArray flex_forEachUpTo:names.count map:^id(NSUInteger i) {
|
||||
return @[names[i], objects[i]];
|
||||
}];
|
||||
}
|
||||
@end
|
||||
|
||||
#pragma mark - Firebase Firestore
|
||||
|
||||
@implementation FLEXShortcutsFactory (FirebaseFirestore)
|
||||
|
||||
+ (void)load { FLEX_EXIT_IF_NO_CTORS()
|
||||
Class FIRDocumentSnap = NSClassFromString(@"FIRDocumentSnapshot");
|
||||
if (FIRDocumentSnap) {
|
||||
FLEXRuntimeUtilityTryAddObjectProperty(2, data, FIRDocumentSnap, NSDictionary, PropertyKey(ReadOnly));
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
@@ -0,0 +1,134 @@
|
||||
//
|
||||
// FLEXShortcutsSection.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 8/29/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXTableViewSection.h"
|
||||
#import "FLEXObjectInfoSection.h"
|
||||
@class FLEXProperty, FLEXIvar, FLEXMethod;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/// An abstract base class for custom object "shortcuts" where every
|
||||
/// row can possibly have some action. The section title is "Shortcuts".
|
||||
///
|
||||
/// You should only subclass this class if you need simple shortcuts
|
||||
/// with plain titles and/or subtitles. This class will automatically
|
||||
/// configure each cell appropriately. Since this is intended as a
|
||||
/// static section, subclasses should only need to implement the
|
||||
/// \c viewControllerToPushForRow: and/or \c didSelectRowAction: methods.
|
||||
///
|
||||
/// If you create the section using \c forObject:rows:numberOfLines:
|
||||
/// then it will provide a view controller from \c viewControllerToPushForRow:
|
||||
/// automatically for rows that are a property/ivar/method.
|
||||
@interface FLEXShortcutsSection : FLEXTableViewSection <FLEXObjectInfoSection>
|
||||
|
||||
/// Uses \c kFLEXDefaultCell
|
||||
+ (instancetype)forObject:(id)objectOrClass rowTitles:(nullable NSArray<NSString *> *)titles;
|
||||
/// Uses \c kFLEXDetailCell for non-empty subtitles, otherwise uses \c kFLEXDefaultCell
|
||||
+ (instancetype)forObject:(id)objectOrClass
|
||||
rowTitles:(nullable NSArray<NSString *> *)titles
|
||||
rowSubtitles:(nullable NSArray<NSString *> *)subtitles;
|
||||
|
||||
/// Uses \c kFLEXDefaultCell for rows that are given a title, otherwise
|
||||
/// this uses \c kFLEXDetailCell for any other allowed object.
|
||||
///
|
||||
/// The section provide a view controller from \c viewControllerToPushForRow:
|
||||
/// automatically for rows that are a property/ivar/method.
|
||||
///
|
||||
/// @param rows A mixed array containing any of the following:
|
||||
/// - any \c FLEXShortcut conforming object
|
||||
/// - an \c NSString
|
||||
/// - a \c FLEXProperty
|
||||
/// - a \c FLEXIvar
|
||||
/// - a \c FLEXMethodBase (includes \c FLEXMethod of course)
|
||||
/// Passing one of the latter 3 will provide a shortcut to that property/ivar/method.
|
||||
+ (instancetype)forObject:(id)objectOrClass rows:(nullable NSArray *)rows;
|
||||
|
||||
/// Same as \c forObject:rows: but the given rows are prepended
|
||||
/// to the shortcuts already registered for the object's class.
|
||||
/// \c forObject:rows: does not use the registered shortcuts at all.
|
||||
+ (instancetype)forObject:(id)objectOrClass additionalRows:(nullable NSArray *)rows;
|
||||
|
||||
/// Calls into \c forObject:rows: using the registered shortcuts for the object's class.
|
||||
/// @return An empty section if the object has no shortcuts registered at all.
|
||||
+ (instancetype)forObject:(id)objectOrClass;
|
||||
|
||||
/// Subclasses \e may override this to hide the disclosure indicator
|
||||
/// for some rows. It is shown for all rows by default, unless
|
||||
/// you initialize it with \c forObject:rowTitles:rowSubtitles:
|
||||
///
|
||||
/// When you hide the disclosure indicator, the row is not selectable.
|
||||
- (UITableViewCellAccessoryType)accessoryTypeForRow:(NSInteger)row;
|
||||
|
||||
/// The number of lines for the title and subtitle labels. Defaults to 1.
|
||||
@property (nonatomic, readonly) NSInteger numberOfLines;
|
||||
/// The object used to initialize this section.
|
||||
@property (nonatomic, readonly) id object;
|
||||
|
||||
/// Whether dynamic subtitles should always be computed as a cell is configured.
|
||||
/// Defaults to NO. Has no effect on static subtitles that are passed explicitly.
|
||||
@property (nonatomic) BOOL cacheSubtitles;
|
||||
|
||||
/// Whether this shortcut section overrides the default section or not.
|
||||
/// Subclasses should not override this method. To provide a second
|
||||
/// section alongside the default shortcuts section, use \c forObject:rows:
|
||||
/// @return \c NO if initialized with \c forObject: or \c forObject:additionalRows:
|
||||
@property (nonatomic, readonly) BOOL isNewSection;
|
||||
|
||||
@end
|
||||
|
||||
@class FLEXShortcutsFactory;
|
||||
typedef FLEXShortcutsFactory *_Nonnull(^FLEXShortcutsFactoryNames)(NSArray *names);
|
||||
typedef void (^FLEXShortcutsFactoryTarget)(Class targetClass);
|
||||
|
||||
/// The block properties below are to be used like SnapKit or Masonry.
|
||||
/// \c FLEXShortcutsSection.append.properties(@[@"frame",@"bounds"]).forClass(UIView.class);
|
||||
///
|
||||
/// To safely register your own classes at launch, subclass this class,
|
||||
/// override \c +load, and call the appropriate methods on \c self
|
||||
@interface FLEXShortcutsFactory : NSObject
|
||||
|
||||
/// Returns the list of all registered shortcuts for the given object in this order:
|
||||
/// Properties, ivars, methods.
|
||||
///
|
||||
/// This method traverses up the object's class hierarchy until it finds
|
||||
/// something registered. This allows you to show different shortcuts for
|
||||
/// the same object in different parts of the class hierarchy.
|
||||
///
|
||||
/// As an example, UIView may have a -layer shortcut registered. But if
|
||||
/// you're inspecting a UIControl, you may not care about the layer or other
|
||||
/// UIView-specific things; you might rather see the target-actions registered
|
||||
/// for this control, and so you would register that property or ivar to UIControl,
|
||||
/// And you would still be able to see the UIView-registered shorcuts by clicking
|
||||
/// on the UIView "lens" at the top the explorer view controller screen.
|
||||
+ (NSArray *)shortcutsForObjectOrClass:(id)objectOrClass;
|
||||
|
||||
@property (nonatomic, readonly, class) FLEXShortcutsFactory *append;
|
||||
@property (nonatomic, readonly, class) FLEXShortcutsFactory *prepend;
|
||||
@property (nonatomic, readonly, class) FLEXShortcutsFactory *replace;
|
||||
|
||||
@property (nonatomic, readonly) FLEXShortcutsFactoryNames properties;
|
||||
/// Do not try to set \c classProperties at the same time as \c ivars or other instance things.
|
||||
@property (nonatomic, readonly) FLEXShortcutsFactoryNames classProperties;
|
||||
@property (nonatomic, readonly) FLEXShortcutsFactoryNames ivars;
|
||||
@property (nonatomic, readonly) FLEXShortcutsFactoryNames methods;
|
||||
/// Do not try to set \c classMethods at the same time as \c ivars or other instance things.
|
||||
@property (nonatomic, readonly) FLEXShortcutsFactoryNames classMethods;
|
||||
|
||||
/// Accepts the target class. If you pass a regular class object,
|
||||
/// shortcuts will appear on instances. If you pass a metaclass object,
|
||||
/// shortcuts will appear when exploring a class object.
|
||||
///
|
||||
/// For example, some class method shortcuts are added to the NSObject meta
|
||||
/// class by default so that you can see +alloc and +new when exploring
|
||||
/// a class object. If you wanted these to show up when exploring
|
||||
/// instances you would pass them to the classMethods method above.
|
||||
@property (nonatomic, readonly) FLEXShortcutsFactoryTarget forClass;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
@@ -0,0 +1,482 @@
|
||||
//
|
||||
// FLEXShortcutsSection.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 8/29/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXShortcutsSection.h"
|
||||
#import "FLEXTableView.h"
|
||||
#import "FLEXTableViewCell.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXShortcut.h"
|
||||
#import "FLEXProperty.h"
|
||||
#import "FLEXPropertyAttributes.h"
|
||||
#import "FLEXIvar.h"
|
||||
#import "FLEXMethod.h"
|
||||
#import "FLEXRuntime+UIKitHelpers.h"
|
||||
#import "FLEXObjectExplorer.h"
|
||||
|
||||
#pragma mark Private
|
||||
|
||||
@interface FLEXShortcutsSection ()
|
||||
@property (nonatomic, copy) NSArray<NSString *> *titles;
|
||||
@property (nonatomic, copy) NSArray<NSString *> *subtitles;
|
||||
|
||||
@property (nonatomic, copy) NSArray<NSString *> *allTitles;
|
||||
@property (nonatomic, copy) NSArray<NSString *> *allSubtitles;
|
||||
|
||||
// Shortcuts are not used if initialized with static titles and subtitles
|
||||
@property (nonatomic, copy) NSArray<id<FLEXShortcut>> *shortcuts;
|
||||
@property (nonatomic, readonly) NSArray<id<FLEXShortcut>> *allShortcuts;
|
||||
@end
|
||||
|
||||
@implementation FLEXShortcutsSection
|
||||
@synthesize isNewSection = _isNewSection;
|
||||
|
||||
#pragma mark Initialization
|
||||
|
||||
+ (instancetype)forObject:(id)objectOrClass rowTitles:(NSArray<NSString *> *)titles {
|
||||
return [self forObject:objectOrClass rowTitles:titles rowSubtitles:nil];
|
||||
}
|
||||
|
||||
+ (instancetype)forObject:(id)objectOrClass
|
||||
rowTitles:(NSArray<NSString *> *)titles
|
||||
rowSubtitles:(NSArray<NSString *> *)subtitles {
|
||||
return [[self alloc] initWithObject:objectOrClass titles:titles subtitles:subtitles];
|
||||
}
|
||||
|
||||
+ (instancetype)forObject:(id)objectOrClass rows:(NSArray *)rows {
|
||||
return [[self alloc] initWithObject:objectOrClass rows:rows isNewSection:YES];
|
||||
}
|
||||
|
||||
+ (instancetype)forObject:(id)objectOrClass additionalRows:(NSArray *)toPrepend {
|
||||
NSArray *rows = [FLEXShortcutsFactory shortcutsForObjectOrClass:objectOrClass];
|
||||
NSArray *allRows = [toPrepend arrayByAddingObjectsFromArray:rows] ?: rows;
|
||||
return [[self alloc] initWithObject:objectOrClass rows:allRows isNewSection:NO];
|
||||
}
|
||||
|
||||
+ (instancetype)forObject:(id)objectOrClass {
|
||||
return [self forObject:objectOrClass additionalRows:nil];
|
||||
}
|
||||
|
||||
- (id)initWithObject:(id)object
|
||||
titles:(NSArray<NSString *> *)titles
|
||||
subtitles:(NSArray<NSString *> *)subtitles {
|
||||
|
||||
NSParameterAssert(titles.count == subtitles.count || !subtitles);
|
||||
NSParameterAssert(titles.count);
|
||||
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_object = object;
|
||||
_allTitles = titles.copy;
|
||||
_allSubtitles = subtitles.copy;
|
||||
_isNewSection = YES;
|
||||
_numberOfLines = 1;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (id)initWithObject:object rows:(NSArray *)rows isNewSection:(BOOL)newSection {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_object = object;
|
||||
_isNewSection = newSection;
|
||||
|
||||
_allShortcuts = [rows flex_mapped:^id(id obj, NSUInteger idx) {
|
||||
return [FLEXShortcut shortcutFor:obj];
|
||||
}];
|
||||
_numberOfLines = 1;
|
||||
|
||||
// Populate titles and subtitles
|
||||
[self reloadData];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
- (void)setCacheSubtitles:(BOOL)cacheSubtitles {
|
||||
if (_cacheSubtitles == cacheSubtitles) return;
|
||||
|
||||
// cacheSubtitles only applies if we have shortcut objects
|
||||
if (self.allShortcuts) {
|
||||
_cacheSubtitles = cacheSubtitles;
|
||||
[self reloadData];
|
||||
} else {
|
||||
NSLog(@"Warning: setting 'cacheSubtitles' on a shortcut section with static subtitles");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Overrides
|
||||
|
||||
- (UITableViewCellAccessoryType)accessoryTypeForRow:(NSInteger)row {
|
||||
if (_allShortcuts) {
|
||||
return [self.shortcuts[row] accessoryTypeWith:self.object];
|
||||
}
|
||||
|
||||
return UITableViewCellAccessoryNone;
|
||||
}
|
||||
|
||||
- (void)setFilterText:(NSString *)filterText {
|
||||
super.filterText = filterText;
|
||||
|
||||
NSAssert(
|
||||
self.allTitles.count == self.allSubtitles.count,
|
||||
@"Each title needs a (possibly empty) subtitle"
|
||||
);
|
||||
|
||||
if (filterText.length) {
|
||||
// Tally up indexes of titles and subtitles matching the filter
|
||||
NSMutableIndexSet *filterMatches = [NSMutableIndexSet new];
|
||||
id filterBlock = ^BOOL(NSString *obj, NSUInteger idx) {
|
||||
if ([obj localizedCaseInsensitiveContainsString:filterText]) {
|
||||
[filterMatches addIndex:idx];
|
||||
return YES;
|
||||
}
|
||||
|
||||
return NO;
|
||||
};
|
||||
|
||||
// Get all matching indexes, including subtitles
|
||||
[self.allTitles flex_forEach:filterBlock];
|
||||
[self.allSubtitles flex_forEach:filterBlock];
|
||||
// Filter to matching indexes only
|
||||
self.titles = [self.allTitles objectsAtIndexes:filterMatches];
|
||||
self.subtitles = [self.allSubtitles objectsAtIndexes:filterMatches];
|
||||
self.shortcuts = [self.allShortcuts objectsAtIndexes:filterMatches];
|
||||
} else {
|
||||
self.shortcuts = self.allShortcuts;
|
||||
self.titles = self.allTitles;
|
||||
self.subtitles = [self.allSubtitles flex_filtered:^BOOL(NSString *sub, NSUInteger idx) {
|
||||
return sub.length > 0;
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)reloadData {
|
||||
[FLEXObjectExplorer configureDefaultsForItems:self.allShortcuts];
|
||||
|
||||
// Generate all (sub)titles from shortcuts
|
||||
if (self.allShortcuts) {
|
||||
self.allTitles = [self.allShortcuts flex_mapped:^id(id<FLEXShortcut> s, NSUInteger idx) {
|
||||
return [s titleWith:self.object];
|
||||
}];
|
||||
self.allSubtitles = [self.allShortcuts flex_mapped:^id(id<FLEXShortcut> s, NSUInteger idx) {
|
||||
return [s subtitleWith:self.object] ?: @"";
|
||||
}];
|
||||
}
|
||||
|
||||
// Re-generate filtered (sub)titles and shortcuts
|
||||
self.filterText = self.filterText;
|
||||
}
|
||||
|
||||
- (NSString *)title {
|
||||
return @"Shortcuts";
|
||||
}
|
||||
|
||||
- (NSInteger)numberOfRows {
|
||||
return self.titles.count;
|
||||
}
|
||||
|
||||
- (BOOL)canSelectRow:(NSInteger)row {
|
||||
UITableViewCellAccessoryType type = [self.shortcuts[row] accessoryTypeWith:self.object];
|
||||
BOOL hasDisclosure = NO;
|
||||
hasDisclosure |= type == UITableViewCellAccessoryDisclosureIndicator;
|
||||
hasDisclosure |= type == UITableViewCellAccessoryDetailDisclosureButton;
|
||||
return hasDisclosure;
|
||||
}
|
||||
|
||||
- (void (^)(__kindof UIViewController *))didSelectRowAction:(NSInteger)row {
|
||||
return [self.shortcuts[row] didSelectActionWith:self.object];
|
||||
}
|
||||
|
||||
- (UIViewController *)viewControllerToPushForRow:(NSInteger)row {
|
||||
/// Nil if shortcuts is nil, i.e. if initialized with forObject:rowTitles:rowSubtitles:
|
||||
return [self.shortcuts[row] viewerWith:self.object];
|
||||
}
|
||||
|
||||
- (void (^)(__kindof UIViewController *))didPressInfoButtonAction:(NSInteger)row {
|
||||
id<FLEXShortcut> shortcut = self.shortcuts[row];
|
||||
if ([shortcut respondsToSelector:@selector(editorWith:forSection:)]) {
|
||||
id object = self.object;
|
||||
return ^(UIViewController *host) {
|
||||
UIViewController *editor = [shortcut editorWith:object forSection:self];
|
||||
[host.navigationController pushViewController:editor animated:YES];
|
||||
};
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (NSString *)reuseIdentifierForRow:(NSInteger)row {
|
||||
FLEXTableViewCellReuseIdentifier defaultReuse = kFLEXDetailCell;
|
||||
if (@available(iOS 11, *)) {
|
||||
defaultReuse = kFLEXMultilineDetailCell;
|
||||
}
|
||||
|
||||
return [self.shortcuts[row] customReuseIdentifierWith:self.object] ?: defaultReuse;
|
||||
}
|
||||
|
||||
- (void)configureCell:(__kindof FLEXTableViewCell *)cell forRow:(NSInteger)row {
|
||||
cell.titleLabel.text = [self titleForRow:row];
|
||||
cell.titleLabel.numberOfLines = self.numberOfLines;
|
||||
cell.subtitleLabel.text = [self subtitleForRow:row];
|
||||
cell.subtitleLabel.numberOfLines = self.numberOfLines;
|
||||
cell.accessoryType = [self accessoryTypeForRow:row];
|
||||
}
|
||||
|
||||
- (NSString *)titleForRow:(NSInteger)row {
|
||||
return self.titles[row];
|
||||
}
|
||||
|
||||
- (NSString *)subtitleForRow:(NSInteger)row {
|
||||
// Case: dynamic, uncached subtitles
|
||||
if (!self.cacheSubtitles) {
|
||||
NSString *subtitle = [self.shortcuts[row] subtitleWith:self.object];
|
||||
return subtitle.length ? subtitle : nil;
|
||||
}
|
||||
|
||||
// Case: static subtitles, or cached subtitles
|
||||
return self.subtitles[row];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
#pragma mark - Global shortcut registration
|
||||
|
||||
@interface FLEXShortcutsFactory () {
|
||||
BOOL _append, _prepend, _replace, _notInstance;
|
||||
NSArray<NSString *> *_properties, *_ivars, *_methods;
|
||||
}
|
||||
@end
|
||||
|
||||
#define NewAndSet(ivar) ({ FLEXShortcutsFactory *r = [self sharedFactory]; r->ivar = YES; r; })
|
||||
#define SetIvar(ivar) ({ self->ivar = YES; self; })
|
||||
#define SetParamBlock(ivar) ^(NSArray *p) { self->ivar = p; return self; }
|
||||
|
||||
typedef NSMutableDictionary<Class, NSMutableArray<id<FLEXRuntimeMetadata>> *> RegistrationBuckets;
|
||||
|
||||
@implementation FLEXShortcutsFactory {
|
||||
// Class buckets
|
||||
RegistrationBuckets *cProperties;
|
||||
RegistrationBuckets *cIvars;
|
||||
RegistrationBuckets *cMethods;
|
||||
// Metaclass buckets
|
||||
RegistrationBuckets *mProperties;
|
||||
RegistrationBuckets *mMethods;
|
||||
}
|
||||
|
||||
+ (instancetype)sharedFactory {
|
||||
static FLEXShortcutsFactory *shared = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
shared = [self new];
|
||||
});
|
||||
|
||||
return shared;
|
||||
}
|
||||
|
||||
- (id)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
cProperties = [NSMutableDictionary new];
|
||||
cIvars = [NSMutableDictionary new];
|
||||
cMethods = [NSMutableDictionary new];
|
||||
|
||||
mProperties = [NSMutableDictionary new];
|
||||
mMethods = [NSMutableDictionary new];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (NSArray<id<FLEXRuntimeMetadata>> *)shortcutsForObjectOrClass:(id)objectOrClass {
|
||||
return [[self sharedFactory] shortcutsForObjectOrClass:objectOrClass];
|
||||
}
|
||||
|
||||
- (NSArray<id<FLEXRuntimeMetadata>> *)shortcutsForObjectOrClass:(id)objectOrClass {
|
||||
NSParameterAssert(objectOrClass);
|
||||
|
||||
NSMutableArray<id<FLEXRuntimeMetadata>> *shortcuts = [NSMutableArray new];
|
||||
BOOL isClass = object_isClass(objectOrClass);
|
||||
// The -class does not give you a metaclass, and we want a metaclass
|
||||
// if a class is passed in, or a class if an object is passed in
|
||||
Class classKey = object_getClass(objectOrClass);
|
||||
|
||||
RegistrationBuckets *propertyBucket = isClass ? mProperties : cProperties;
|
||||
RegistrationBuckets *methodBucket = isClass ? mMethods : cMethods;
|
||||
RegistrationBuckets *ivarBucket = isClass ? nil : cIvars;
|
||||
|
||||
BOOL stop = NO;
|
||||
while (!stop && classKey) {
|
||||
NSArray *properties = propertyBucket[classKey];
|
||||
NSArray *ivars = ivarBucket[classKey];
|
||||
NSArray *methods = methodBucket[classKey];
|
||||
|
||||
// Stop if we found anything
|
||||
stop = properties || ivars || methods;
|
||||
if (stop) {
|
||||
// Add things we found to the list
|
||||
[shortcuts addObjectsFromArray:properties];
|
||||
[shortcuts addObjectsFromArray:ivars];
|
||||
[shortcuts addObjectsFromArray:methods];
|
||||
} else {
|
||||
classKey = class_getSuperclass(classKey);
|
||||
}
|
||||
}
|
||||
|
||||
[FLEXObjectExplorer configureDefaultsForItems:shortcuts];
|
||||
return shortcuts;
|
||||
}
|
||||
|
||||
+ (FLEXShortcutsFactory *)append {
|
||||
return NewAndSet(_append);
|
||||
}
|
||||
|
||||
+ (FLEXShortcutsFactory *)prepend {
|
||||
return NewAndSet(_prepend);
|
||||
}
|
||||
|
||||
+ (FLEXShortcutsFactory *)replace {
|
||||
return NewAndSet(_replace);
|
||||
}
|
||||
|
||||
- (void)_register:(NSArray<id<FLEXRuntimeMetadata>> *)items to:(RegistrationBuckets *)global class:(Class)key {
|
||||
@synchronized (self) {
|
||||
// Get (or initialize) the bucket for this class
|
||||
NSMutableArray *bucket = ({
|
||||
id bucket = global[key];
|
||||
if (!bucket) {
|
||||
bucket = [NSMutableArray new];
|
||||
global[(id)key] = bucket;
|
||||
}
|
||||
bucket;
|
||||
});
|
||||
|
||||
if (self->_append) { [bucket addObjectsFromArray:items]; }
|
||||
if (self->_replace) { [bucket setArray:items]; }
|
||||
if (self->_prepend) {
|
||||
if (bucket.count) {
|
||||
// Set new items as array, add old items behind them
|
||||
id copy = bucket.copy;
|
||||
[bucket setArray:items];
|
||||
[bucket addObjectsFromArray:copy];
|
||||
} else {
|
||||
[bucket addObjectsFromArray:items];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)reset {
|
||||
_append = NO;
|
||||
_prepend = NO;
|
||||
_replace = NO;
|
||||
_notInstance = NO;
|
||||
|
||||
_properties = nil;
|
||||
_ivars = nil;
|
||||
_methods = nil;
|
||||
}
|
||||
|
||||
- (FLEXShortcutsFactory *)class {
|
||||
return SetIvar(_notInstance);
|
||||
}
|
||||
|
||||
- (FLEXShortcutsFactoryNames)properties {
|
||||
NSAssert(!_notInstance, @"Do not try to set properties+classProperties at the same time");
|
||||
return SetParamBlock(_properties);
|
||||
}
|
||||
|
||||
- (FLEXShortcutsFactoryNames)classProperties {
|
||||
_notInstance = YES;
|
||||
return SetParamBlock(_properties);
|
||||
}
|
||||
|
||||
- (FLEXShortcutsFactoryNames)ivars {
|
||||
return SetParamBlock(_ivars);
|
||||
}
|
||||
|
||||
- (FLEXShortcutsFactoryNames)methods {
|
||||
NSAssert(!_notInstance, @"Do not try to set methods+classMethods at the same time");
|
||||
return SetParamBlock(_methods);
|
||||
}
|
||||
|
||||
- (FLEXShortcutsFactoryNames)classMethods {
|
||||
_notInstance = YES;
|
||||
return SetParamBlock(_methods);
|
||||
}
|
||||
|
||||
- (FLEXShortcutsFactoryTarget)forClass {
|
||||
return ^(Class cls) {
|
||||
NSAssert(
|
||||
( self->_append && !self->_prepend && !self->_replace) ||
|
||||
(!self->_append && self->_prepend && !self->_replace) ||
|
||||
(!self->_append && !self->_prepend && self->_replace),
|
||||
@"You can only do one of [append, prepend, replace]"
|
||||
);
|
||||
|
||||
|
||||
/// Whether the metadata we're about to add is instance or
|
||||
/// class metadata, i.e. class properties vs instance properties
|
||||
BOOL instanceMetadata = !self->_notInstance;
|
||||
/// Whether the given class is a metaclass or not; we need to switch to
|
||||
/// the metaclass to add class metadata if we are given the normal class object
|
||||
BOOL isMeta = class_isMetaClass(cls);
|
||||
/// Whether the shortcuts we're about to add should appear for classes or instances
|
||||
BOOL instanceShortcut = !isMeta;
|
||||
|
||||
if (instanceMetadata) {
|
||||
NSAssert(!isMeta,
|
||||
@"Instance metadata can only be added as an instance shortcut"
|
||||
);
|
||||
}
|
||||
|
||||
Class metaclass = isMeta ? cls : object_getClass(cls);
|
||||
Class clsForMetadata = instanceMetadata ? cls : metaclass;
|
||||
|
||||
// The factory is a singleton so we don't need to worry about "leaking" it
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wimplicit-retain-self"
|
||||
|
||||
RegistrationBuckets *propertyBucket = instanceShortcut ? cProperties : mProperties;
|
||||
RegistrationBuckets *methodBucket = instanceShortcut ? cMethods : mMethods;
|
||||
RegistrationBuckets *ivarBucket = instanceShortcut ? cIvars : nil;
|
||||
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
if (self->_properties) {
|
||||
NSArray *items = [self->_properties flex_mapped:^id(NSString *name, NSUInteger idx) {
|
||||
return [FLEXProperty named:name onClass:clsForMetadata];
|
||||
}];
|
||||
[self _register:items to:propertyBucket class:cls];
|
||||
}
|
||||
|
||||
if (self->_methods) {
|
||||
NSArray *items = [self->_methods flex_mapped:^id(NSString *name, NSUInteger idx) {
|
||||
return [FLEXMethod selector:NSSelectorFromString(name) class:clsForMetadata];
|
||||
}];
|
||||
[self _register:items to:methodBucket class:cls];
|
||||
}
|
||||
|
||||
if (self->_ivars) {
|
||||
NSAssert(instanceMetadata, @"Instance metadata can only be added as an instance shortcut (%@)", cls);
|
||||
NSArray *items = [self->_ivars flex_mapped:^id(NSString *name, NSUInteger idx) {
|
||||
return [FLEXIvar named:name onClass:clsForMetadata];
|
||||
}];
|
||||
[self _register:items to:ivarBucket class:cls];
|
||||
}
|
||||
|
||||
[self reset];
|
||||
};
|
||||
}
|
||||
|
||||
@end
|
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// FLEXUIAppShortcuts.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 5/25/20.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXShortcutsSection.h"
|
||||
|
||||
@interface FLEXUIAppShortcuts : FLEXShortcutsSection
|
||||
|
||||
@end
|
@@ -0,0 +1,77 @@
|
||||
//
|
||||
// FLEXUIAppShortcuts.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 5/25/20.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXUIAppShortcuts.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
#import "FLEXShortcut.h"
|
||||
#import "FLEXAlert.h"
|
||||
|
||||
@implementation FLEXUIAppShortcuts
|
||||
|
||||
#pragma mark - Overrides
|
||||
|
||||
+ (instancetype)forObject:(UIApplication *)application {
|
||||
return [self forObject:application additionalRows:@[
|
||||
[FLEXActionShortcut title:@"Open URL…"
|
||||
subtitle:^NSString *(UIViewController *controller) {
|
||||
return nil;
|
||||
}
|
||||
selectionHandler:^void(UIViewController *host, UIApplication *app) {
|
||||
[FLEXAlert makeAlert:^(FLEXAlert *make) {
|
||||
make.title(@"Open URL");
|
||||
make.message(
|
||||
@"This will call openURL: or openURL:options:completion: "
|
||||
"with the string below. 'Open if Universal' will only open "
|
||||
"the URL if it is a registered Universal Link."
|
||||
);
|
||||
|
||||
make.textField(@"twitter://user?id=12345");
|
||||
make.button(@"Open").handler(^(NSArray<NSString *> *strings) {
|
||||
[self openURL:strings[0] inApp:app onlyIfUniveral:NO host:host];
|
||||
});
|
||||
make.button(@"Open if Universal").handler(^(NSArray<NSString *> *strings) {
|
||||
[self openURL:strings[0] inApp:app onlyIfUniveral:YES host:host];
|
||||
});
|
||||
make.button(@"Cancel").cancelStyle();
|
||||
} showFrom:host];
|
||||
}
|
||||
accessoryType:^UITableViewCellAccessoryType(UIViewController *controller) {
|
||||
return UITableViewCellAccessoryDisclosureIndicator;
|
||||
}
|
||||
]
|
||||
]];
|
||||
}
|
||||
|
||||
+ (void)openURL:(NSString *)urlString
|
||||
inApp:(UIApplication *)app
|
||||
onlyIfUniveral:(BOOL)universalOnly
|
||||
host:(UIViewController *)host {
|
||||
NSURL *url = [NSURL URLWithString:urlString];
|
||||
|
||||
if (url) {
|
||||
if (@available(iOS 10, *)) {
|
||||
[app openURL:url options:@{
|
||||
UIApplicationOpenURLOptionUniversalLinksOnly: @(universalOnly)
|
||||
} completionHandler:^(BOOL success) {
|
||||
if (!success) {
|
||||
[FLEXAlert showAlert:@"No Universal Link Handler"
|
||||
message:@"No installed application is registered to handle this link."
|
||||
from:host
|
||||
];
|
||||
}
|
||||
}];
|
||||
} else {
|
||||
[app openURL:url];
|
||||
}
|
||||
} else {
|
||||
[FLEXAlert showAlert:@"Error" message:@"Invalid URL" from:host];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
@@ -0,0 +1,15 @@
|
||||
//
|
||||
// FLEXViewControllerShortcuts.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 12/12/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXShortcutsSection.h"
|
||||
|
||||
@interface FLEXViewControllerShortcuts : FLEXShortcutsSection
|
||||
|
||||
+ (instancetype)forObject:(UIViewController *)viewController;
|
||||
|
||||
@end
|
@@ -0,0 +1,58 @@
|
||||
//
|
||||
// FLEXViewControllerShortcuts.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 12/12/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXViewControllerShortcuts.h"
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
#import "FLEXShortcut.h"
|
||||
#import "FLEXAlert.h"
|
||||
|
||||
@interface FLEXViewControllerShortcuts ()
|
||||
@end
|
||||
|
||||
@implementation FLEXViewControllerShortcuts
|
||||
|
||||
#pragma mark - Overrides
|
||||
|
||||
+ (instancetype)forObject:(UIViewController *)viewController {
|
||||
BOOL (^vcIsInuse)(UIViewController *) = ^BOOL(UIViewController *controller) {
|
||||
if (controller.viewIfLoaded.window) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
return controller.navigationController != nil;
|
||||
};
|
||||
|
||||
return [self forObject:viewController additionalRows:@[
|
||||
[FLEXActionShortcut title:@"Push View Controller"
|
||||
subtitle:^NSString *(UIViewController *controller) {
|
||||
return vcIsInuse(controller) ? @"In use, cannot push" : nil;
|
||||
}
|
||||
selectionHandler:^void(UIViewController *host, UIViewController *controller) {
|
||||
if (!vcIsInuse(controller)) {
|
||||
[host.navigationController pushViewController:controller animated:YES];
|
||||
} else {
|
||||
[FLEXAlert
|
||||
showAlert:@"Cannot Push View Controller"
|
||||
message:@"This view controller's view is currently in use."
|
||||
from:host
|
||||
];
|
||||
}
|
||||
}
|
||||
accessoryType:^UITableViewCellAccessoryType(UIViewController *controller) {
|
||||
if (!vcIsInuse(controller)) {
|
||||
return UITableViewCellAccessoryDisclosureIndicator;
|
||||
} else {
|
||||
return UITableViewCellAccessoryNone;
|
||||
}
|
||||
}
|
||||
]
|
||||
]];
|
||||
}
|
||||
|
||||
@end
|
@@ -0,0 +1,14 @@
|
||||
//
|
||||
// FLEXViewShortcuts.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 12/11/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXShortcutsSection.h"
|
||||
|
||||
/// Adds "Nearest View Controller" and "Preview Image" shortcuts to all views
|
||||
@interface FLEXViewShortcuts : FLEXShortcutsSection
|
||||
|
||||
@end
|
@@ -0,0 +1,90 @@
|
||||
//
|
||||
// FLEXViewShortcuts.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 12/11/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXViewShortcuts.h"
|
||||
#import "FLEXShortcut.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
#import "FLEXImagePreviewViewController.h"
|
||||
|
||||
@interface FLEXViewShortcuts ()
|
||||
@property (nonatomic, readonly) UIView *view;
|
||||
@end
|
||||
|
||||
@implementation FLEXViewShortcuts
|
||||
|
||||
#pragma mark - Internal
|
||||
|
||||
- (UIView *)view {
|
||||
return self.object;
|
||||
}
|
||||
|
||||
+ (UIViewController *)viewControllerForView:(UIView *)view {
|
||||
NSString *viewDelegate = @"viewDelegate";
|
||||
if ([view respondsToSelector:NSSelectorFromString(viewDelegate)]) {
|
||||
return [view valueForKey:viewDelegate];
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
+ (UIViewController *)viewControllerForAncestralView:(UIView *)view {
|
||||
NSString *_viewControllerForAncestor = @"_viewControllerForAncestor";
|
||||
if ([view respondsToSelector:NSSelectorFromString(_viewControllerForAncestor)]) {
|
||||
return [view valueForKey:_viewControllerForAncestor];
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
+ (UIViewController *)nearestViewControllerForView:(UIView *)view {
|
||||
return [self viewControllerForView:view] ?: [self viewControllerForAncestralView:view];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Overrides
|
||||
|
||||
+ (instancetype)forObject:(UIView *)view {
|
||||
// In the past, FLEX would not hold a strong reference to something like this.
|
||||
// After using FLEX for so long, I am certain it is more useful to eagerly
|
||||
// reference something as useful as a view controller so that the reference
|
||||
// is not lost and swept out from under you before you can access it.
|
||||
//
|
||||
// The alternative here is to use a future in place of `controller` which would
|
||||
// dynamically grab a reference to the view controller. 99% of the time, however,
|
||||
// it is not all that useful. If you need it to refresh, you can simply go back
|
||||
// and go forward again and it will show if the view controller is nil or changed.
|
||||
UIViewController *controller = [FLEXViewShortcuts nearestViewControllerForView:view];
|
||||
|
||||
return [self forObject:view additionalRows:@[
|
||||
[FLEXActionShortcut title:@"Nearest View Controller"
|
||||
subtitle:^NSString *(id view) {
|
||||
return [FLEXRuntimeUtility safeDescriptionForObject:controller];
|
||||
}
|
||||
viewer:^UIViewController *(id view) {
|
||||
return [FLEXObjectExplorerFactory explorerViewControllerForObject:controller];
|
||||
}
|
||||
accessoryType:^UITableViewCellAccessoryType(id view) {
|
||||
return controller ? UITableViewCellAccessoryDisclosureIndicator : UITableViewCellAccessoryNone;
|
||||
}
|
||||
],
|
||||
[FLEXActionShortcut title:@"Preview Image" subtitle:^NSString *(UIView *view) {
|
||||
return !CGRectIsEmpty(view.bounds) ? @"" : @"Unavailable with empty bounds";
|
||||
}
|
||||
viewer:^UIViewController *(UIView *view) {
|
||||
return [FLEXImagePreviewViewController previewForView:view];
|
||||
}
|
||||
accessoryType:^UITableViewCellAccessoryType(UIView *view) {
|
||||
// Disable preview if bounds are CGRectZero
|
||||
return !CGRectIsEmpty(view.bounds) ? UITableViewCellAccessoryDisclosureIndicator : UITableViewCellAccessoryNone;
|
||||
}
|
||||
]
|
||||
]];
|
||||
}
|
||||
|
||||
@end
|
Reference in New Issue
Block a user