Compare commits

..

1 Commits

Author SHA1 Message Date
Tanner Bennett 4f8b6c05cb Bump version, close #465 #466 2020-10-22 17:57:25 -05:00
133 changed files with 737 additions and 1904 deletions
-3
View File
@@ -1,3 +0,0 @@
# These are supported funding model platforms
github: [NSExceptional]
-27
View File
@@ -1,27 +0,0 @@
---
name: Bug report
about: Report a bug in FLEX
title: ''
labels: bug
assignees: ''
---
### Environment
- Platform+version: **iOS 14** <!--- Change to match your platform and version -->
- FLEX version: **9.9.9** <!--- Change to the version of FLEX you're using -->
<!--- FLEXing / libFLEX users: please include FLEXing and libFLEX versions separately -->
### Bug Report
Here, you can provide a description of the bug. Some tips:
- Please do not paste an entire crash log. Upload the crash log to something like [ghostbin.co](https://ghostbin.co/) or another paste service. Alternatively, you can cut out the relevant stack trace and paste that inside a ` ```code block``` `
- If the bug is more complex than "this button is broken" or a crash, consider including a sample project. For example, if your app's requests aren't showing up in the network history page.
- Providing steps to reproduce is always helpful!
- If you want to include a screenshot or GIF, consider modifying the default markdown for uploaded images to use this code to make the image smaller on desktop:
```
<img width="50%" src=your-image-url >
```
This template is a suggestion. You may format your issue however you want, but generally you should at least include your iOS version and FLEX version.
-10
View File
@@ -1,10 +0,0 @@
---
name: Feature request
about: Suggest a new feature for FLEX
title: ''
labels: enhancement
assignees: ''
---
@@ -9,7 +9,6 @@
#import "FLEXFilteringTableViewController.h"
#import "FLEXTableViewSection.h"
#import "NSArray+FLEX.h"
#import "FLEXMacros.h"
@interface FLEXFilteringTableViewController ()
@@ -188,6 +187,8 @@
[self.filterDelegate.sections[indexPath.section] didPressInfoButtonAction:indexPath.row](self);
}
#if FLEX_AT_LEAST_IOS13_SDK
- (UIContextMenuConfiguration *)tableView:(UITableView *)tableView contextMenuConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath point:(CGPoint)point __IOS_AVAILABLE(13.0) {
FLEXTableViewSection *section = self.filterDelegate.sections[indexPath.section];
NSString *title = [section menuTitleForRow:indexPath.row];
@@ -206,4 +207,6 @@
return nil;
}
#endif
@end
@@ -20,7 +20,6 @@
@interface FLEXNavigationController ()
@property (nonatomic, readonly) BOOL toolbarWasHidden;
@property (nonatomic) BOOL waitingToAddTab;
@property (nonatomic, readonly) BOOL canShowToolbar;
@property (nonatomic) BOOL didSetupPendingDismissButtons;
@property (nonatomic) UISwipeGestureRecognizer *navigationBarSwipeGesture;
@end
@@ -37,13 +36,10 @@
self.waitingToAddTab = YES;
// Add gesture to reveal toolbar if hidden
UITapGestureRecognizer *navbarTapGesture = [[UITapGestureRecognizer alloc]
self.navigationBar.userInteractionEnabled = YES;
[self.navigationBar addGestureRecognizer:[[UITapGestureRecognizer alloc]
initWithTarget:self action:@selector(handleNavigationBarTap:)
];
// Don't cancel touches to work around bug on versions of iOS prior to 13
navbarTapGesture.cancelsTouchesInView = NO;
[self.navigationBar addGestureRecognizer:navbarTapGesture];
]];
// Add gesture to dismiss if not presented with a sheet style
if (@available(iOS 13, *)) {
@@ -100,10 +96,6 @@
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
}
- (BOOL)canShowToolbar {
return self.topViewController.toolbarItems.count;
}
- (void)addNavigationBarItemsToViewController:(UINavigationItem *)navigationItem {
if (!self.presentingViewController) {
return;
@@ -153,15 +145,8 @@
}
- (void)handleNavigationBarTap:(UIGestureRecognizer *)sender {
// Don't reveal the toolbar if we were just tapping a button
CGPoint location = [sender locationInView:self.navigationBar];
UIView *hitView = [self.navigationBar hitTest:location withEvent:nil];
if ([hitView isKindOfClass:[UIControl class]]) {
return;
}
if (sender.state == UIGestureRecognizerStateRecognized) {
if (self.toolbarHidden && self.canShowToolbar) {
if (self.toolbarHidden) {
[self setToolbarHidden:NO animated:YES];
}
}
@@ -177,7 +162,7 @@
- (void)_gestureRecognizedInteractiveHide:(UIPanGestureRecognizer *)sender {
if (sender.state == UIGestureRecognizerStateRecognized) {
BOOL show = self.canShowToolbar;
BOOL show = self.topViewController.toolbarItems.count;
CGFloat yTranslation = [sender translationInView:self.view].y;
CGFloat yVelocity = [sender velocityInView:self.view].y;
if (yVelocity > 2000) {
@@ -67,10 +67,6 @@ extern CGFloat const kFLEXDebounceForExpensiveIO;
/// Setting this to YES will make the search bar appear whenever the view appears.
/// Otherwise, iOS will only show the search bar when you scroll up.
@property (nonatomic) BOOL showSearchBarInitially;
/// Defaults to NO.
///
/// Setting this to YES will make the search bar activate whenever the view appears.
@property (nonatomic) BOOL activatesSearchBarAutomatically;
/// nil unless showsSearchBar is set to YES.
///
@@ -50,12 +50,15 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
#pragma mark - Initialization
- (id)init {
#if FLEX_AT_LEAST_IOS13_SDK
if (@available(iOS 13.0, *)) {
self = [self initWithStyle:UITableViewStyleInsetGrouped];
} else {
self = [self initWithStyle:UITableViewStyleGrouped];
}
#else
self = [self initWithStyle:UITableViewStyleGrouped];
#endif
return self;
}
@@ -103,9 +106,11 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
self.automaticallyShowsSearchBarCancelButton = YES;
#if FLEX_AT_LEAST_IOS13_SDK
if (@available(iOS 13, *)) {
self.searchController.automaticallyShowsScopeBar = NO;
}
#endif
[self addSearchController:self.searchController];
} else {
@@ -119,15 +124,18 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
_showsCarousel = showsCarousel;
if (showsCarousel) {
_carousel = ({ weakify(self)
_carousel = ({
__weak __typeof(self) weakSelf = self;
FLEXScopeCarousel *carousel = [FLEXScopeCarousel new];
carousel.selectedIndexChangedAction = ^(NSInteger idx) { strongify(self);
carousel.selectedIndexChangedAction = ^(NSInteger idx) {
__typeof(self) self = weakSelf;
[self.searchDelegate updateSearchResults:self.searchText];
};
// UITableView won't update the header size unless you reset the header view
[carousel registerBlockForDynamicTypeChanges:^(FLEXScopeCarousel *_) { strongify(self);
[carousel registerBlockForDynamicTypeChanges:^(FLEXScopeCarousel *carousel) {
__typeof(self) self = weakSelf;
[self layoutTableHeaderIfNeeded];
}];
@@ -165,17 +173,21 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
}
- (BOOL)automaticallyShowsSearchBarCancelButton {
#if FLEX_AT_LEAST_IOS13_SDK
if (@available(iOS 13, *)) {
return self.searchController.automaticallyShowsCancelButton;
}
#endif
return _automaticallyShowsSearchBarCancelButton;
}
- (void)setAutomaticallyShowsSearchBarCancelButton:(BOOL)value {
#if FLEX_AT_LEAST_IOS13_SDK
if (@available(iOS 13, *)) {
self.searchController.automaticallyShowsCancelButton = value;
}
#endif
_automaticallyShowsSearchBarCancelButton = value;
}
@@ -229,7 +241,7 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
self.tableView.keyboardDismissMode = UIScrollViewKeyboardDismissModeOnDrag;
// Toolbar
self.navigationController.toolbarHidden = self.toolbarItems.count > 0;
self.navigationController.toolbarHidden = NO;
self.navigationController.hidesBarsOnSwipe = YES;
// On iOS 13, the root view controller shows it's search bar no matter what.
@@ -247,17 +259,12 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
// When going back, make the search bar reappear instead of hiding
if (@available(iOS 11.0, *)) {
// When going back, make the search bar reappear instead of hiding
if ((self.pinSearchBar || self.showSearchBarInitially) && !self.didInitiallyRevealSearchBar) {
self.navigationItem.hidesSearchBarWhenScrolling = NO;
}
}
// Make the keyboard seem to appear faster
if (self.activatesSearchBarAutomatically) {
[self makeKeyboardAppearNow];
}
[self setupToolbarItems];
}
@@ -279,17 +286,6 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
}];
}
}
if (self.activatesSearchBarAutomatically) {
// Keyboard has appeared, now we call this as we soon present our search bar
[self removeDummyTextField];
// Activate the search bar
dispatch_async(dispatch_get_main_queue(), ^{
// This doesn't work unless it's wrapped in this dispatch_async call
[self.searchController.searchBar becomeFirstResponder];
});
}
// We only want to reveal the search bar when the view controller first appears.
self.didInitiallyRevealSearchBar = YES;
@@ -529,30 +525,6 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
#pragma mark - Search Bar
#pragma mark Faster keyboard
static UITextField *kDummyTextField = nil;
/// Make the keyboard appear instantly. We use this to make the
/// keyboard appear faster when the search bar is set to appear initially.
/// You must call \c -removeDummyTextField before your search bar is to appear.
- (void)makeKeyboardAppearNow {
if (!kDummyTextField) {
kDummyTextField = [UITextField new];
kDummyTextField.autocorrectionType = UITextAutocorrectionTypeNo;
}
kDummyTextField.inputAccessoryView = self.searchController.searchBar.inputAccessoryView;
[UIApplication.sharedApplication.keyWindow addSubview:kDummyTextField];
[kDummyTextField becomeFirstResponder];
}
- (void)removeDummyTextField {
if (kDummyTextField.superview) {
[kDummyTextField removeFromSuperview];
}
}
#pragma mark UISearchResultsUpdating
- (void)updateSearchResultsForSearchController:(UISearchController *)searchController {
+3
View File
@@ -7,6 +7,7 @@
//
#import <UIKit/UIKit.h>
#import "FLEXMacros.h"
#import "NSArray+FLEX.h"
@class FLEXTableView;
@@ -100,6 +101,7 @@ NS_ASSUME_NONNULL_BEGIN
- (nullable void(^)(__kindof UIViewController *host))didPressInfoButtonAction:(NSInteger)row;
#pragma mark - Context Menus
#if FLEX_AT_LEAST_IOS13_SDK
/// By default, this is the title of the row.
/// @return The title of the context menu, if any.
@@ -119,6 +121,7 @@ NS_ASSUME_NONNULL_BEGIN
/// should be a description of what will be copied, and the values should be
/// the strings to copy. Return an empty string as a value to show a disabled action.
- (nullable NSArray<NSString *> *)copyMenuItemsForRow:(NSInteger)row API_AVAILABLE(ios(13.0));
#endif
#pragma mark - Cell Configuration
+4
View File
@@ -64,6 +64,8 @@
return kFLEXDefaultCell;
}
#if FLEX_AT_LEAST_IOS13_SDK
- (NSString *)menuTitleForRow:(NSInteger)row {
NSString *title = [self titleForRow:row];
NSString *subtitle = [self menuSubtitleForRow:row];
@@ -125,6 +127,8 @@
return @[];
}
#endif
- (NSArray<NSString *> *)copyMenuItemsForRow:(NSInteger)row {
return nil;
}
@@ -77,7 +77,7 @@
self.selectionIndicatorStripe.translatesAutoresizingMaskIntoConstraints = NO;
UIView *superview = self.contentView;
[self.titleLabel flex_pinEdgesToSuperviewWithInsets:UIEdgeInsetsMake(10, 15, 8 + stripeHeight, 15)];
[self.titleLabel pinEdgesToSuperviewWithInsets:UIEdgeInsetsMake(10, 15, 8 + stripeHeight, 15)];
[self.selectionIndicatorStripe.leadingAnchor constraintEqualToAnchor:superview.leadingAnchor].active = YES;
[self.selectionIndicatorStripe.bottomAnchor constraintEqualToAnchor:superview.bottomAnchor].active = YES;
@@ -9,7 +9,6 @@
#import "FLEXScopeCarousel.h"
#import "FLEXCarouselCell.h"
#import "FLEXColor.h"
#import "FLEXMacros.h"
#import "UIView+FLEX_Layout.h"
const CGFloat kCarouselItemSpacing = 0;
@@ -73,14 +72,15 @@ NSString * const kCarouselCellReuseIdentifier = @"kCarouselCellReuseIdentifier";
self.sizingCell.title = @"NSObject";
// Dynamic type
weakify(self);
__weak __typeof(self) weakSelf = self;
_dynamicTypeObserver = [NSNotificationCenter.defaultCenter
addObserverForName:UIContentSizeCategoryDidChangeNotification
object:nil queue:nil usingBlock:^(NSNotification *note) { strongify(self)
object:nil queue:nil usingBlock:^(NSNotification *note) {
[self.collectionView setNeedsLayout];
[self setNeedsUpdateConstraints];
// Notify observers
__typeof(self) self = weakSelf;
for (void (^block)(FLEXScopeCarousel *) in self.dynamicTypeHandlers) {
block(self);
}
@@ -118,7 +118,7 @@ NSString * const kCarouselCellReuseIdentifier = @"kCarouselCellReuseIdentifier";
- (void)updateConstraints {
if (!self.constraintsInstalled) {
self.collectionView.translatesAutoresizingMaskIntoConstraints = NO;
[self.collectionView flex_pinEdgesToSuperview];
[self.collectionView pinEdgesToSuperview];
self.constraintsInstalled = YES;
}
+8
View File
@@ -30,21 +30,29 @@ FLEXTableViewCellReuseIdentifier const kFLEXCodeFontCell = @"kFLEXCodeFontCell";
@implementation FLEXTableView
+ (instancetype)flexDefaultTableView {
#if FLEX_AT_LEAST_IOS13_SDK
if (@available(iOS 13.0, *)) {
return [[self alloc] initWithFrame:CGRectZero style:UITableViewStyleInsetGrouped];
} else {
return [[self alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];
}
#else
return [[self alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];
#endif
}
#pragma mark - Initialization
+ (id)groupedTableView {
#if FLEX_AT_LEAST_IOS13_SDK
if (@available(iOS 13.0, *)) {
return [[self alloc] initWithFrame:CGRectZero style:UITableViewStyleInsetGrouped];
} else {
return [[self alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];
}
#else
return [[self alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];
#endif
}
+ (id)plainTableView {
@@ -33,7 +33,6 @@
self.inputTextView.delegate = self;
self.inputTextView.inputAccessoryView = [self createToolBar];
if (@available(iOS 11, *)) {
self.inputTextView.smartQuotesType = UITextSmartQuotesTypeNo;
[self.inputTextView.layer setValue:@YES forKey:@"continuousCorners"];
} else {
self.inputTextView.layer.borderWidth = 1.f;
@@ -21,21 +21,11 @@
- (BOOL)shouldReceiveTouchAtWindowPoint:(CGPoint)pointInWindowCoordinates;
/// @brief Used to present (or dismiss) a modal view controller ("tool"),
/// typically triggered by pressing a button in the toolbar.
/// @brief Used to present (or dismiss) a modal view controller ("tool"), typically triggered by pressing a button in the toolbar.
///
/// If a tool is already presented, this method simply dismisses it and calls the completion block.
/// If no tool is presented, @code future() @endcode is presented and the completion block is called.
- (void)toggleToolWithViewControllerProvider:(UINavigationController *(^)(void))future
completion:(void (^)(void))completion;
/// @brief Used to present (or dismiss) a modal view controller ("tool"),
/// typically triggered by pressing a button in the toolbar.
///
/// If a tool is already presented, this method dismisses it and presents the given tool.
/// The completion block is called once the tool has been presented.
- (void)presentTool:(UINavigationController *(^)(void))future
completion:(void (^)(void))completion;
- (void)toggleToolWithViewControllerProvider:(UINavigationController *(^)(void))future completion:(void(^)(void))completion;
// Keyboard shortcut helpers
@@ -45,9 +45,6 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
/// Only valid while a toolbar drag pan gesture is in progress.
@property (nonatomic) CGRect toolbarFrameBeforeDragging;
/// Only valid while a selected view pan gesture is in progress.
@property (nonatomic) CGFloat selectedViewLastPanX;
/// Borders of all the visible views in the hierarchy at the selection point.
/// The keys are NSValues with the corresponding view (nonretained).
@property (nonatomic) NSDictionary<NSValue *, UIView *> *outlineViewsForVisibleViews;
@@ -61,9 +58,6 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
/// A colored transparent overlay to indicate that the view is selected.
@property (nonatomic) UIView *selectedViewOverlay;
/// Used to actuate changes in view selection on iOS 10+
@property (nonatomic, readonly) UISelectionFeedbackGenerator *selectionFBG API_AVAILABLE(ios(10.0));
/// self.view.window as a \c FLEXWindow
@property (nonatomic, readonly) FLEXWindow *window;
@@ -124,11 +118,6 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
self.movePanGR = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleMovePan:)];
self.movePanGR.enabled = self.currentMode == FLEXExplorerModeMove;
[self.view addGestureRecognizer:self.movePanGR];
// Feedback
if (@available(iOS 10.0, *)) {
_selectionFBG = [UISelectionFeedbackGenerator new];
}
}
- (void)viewWillAppear:(BOOL)animated {
@@ -461,16 +450,16 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
];
[toolbar.selectedViewDescriptionContainer addGestureRecognizer:self.detailsTapGR];
// Swipe gestures for selecting deeper / higher views at a point
UIPanGestureRecognizer *leftSwipe = [[UIPanGestureRecognizer alloc]
UISwipeGestureRecognizer *leftSwipe = [[UISwipeGestureRecognizer alloc]
initWithTarget:self action:@selector(handleChangeViewAtPointGesture:)
];
// UIPanGestureRecognizer *rightSwipe = [[UIPanGestureRecognizer alloc]
// initWithTarget:self action:@selector(handleChangeViewAtPointGesture:)
// ];
// leftSwipe.direction = UISwipeGestureRecognizerDirectionLeft;
// rightSwipe.direction = UISwipeGestureRecognizerDirectionRight;
UISwipeGestureRecognizer *rightSwipe = [[UISwipeGestureRecognizer alloc]
initWithTarget:self action:@selector(handleChangeViewAtPointGesture:)
];
leftSwipe.direction = UISwipeGestureRecognizerDirectionLeft;
rightSwipe.direction = UISwipeGestureRecognizerDirectionRight;
[toolbar.selectedViewDescriptionContainer addGestureRecognizer:leftSwipe];
// [toolbar.selectedViewDescriptionContainer addGestureRecognizer:rightSwipe];
[toolbar.selectedViewDescriptionContainer addGestureRecognizer:rightSwipe];
// Long press gesture to present tabs manager
[toolbar.globalsItem addGestureRecognizer:[[UILongPressGestureRecognizer alloc]
@@ -609,54 +598,19 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
}
}
- (void)handleChangeViewAtPointGesture:(UIPanGestureRecognizer *)sender {
- (void)handleChangeViewAtPointGesture:(UISwipeGestureRecognizer *)sender {
NSInteger max = self.viewsAtTapPoint.count - 1;
NSInteger currentIdx = [self.viewsAtTapPoint indexOfObject:self.selectedView];
CGFloat locationX = [sender locationInView:self.view].x;
// Track the pan gesture: every N points we move along the X axis,
// actuate some haptic feedback and move up or down the hierarchy.
// We only store the "last" location when we've met the threshold.
// We only change the view and actuate feedback if the view selection
// changes; that is, as long as we don't go outside or under the array.
switch (sender.state) {
case UIGestureRecognizerStateBegan: {
self.selectedViewLastPanX = locationX;
switch (sender.direction) {
case UISwipeGestureRecognizerDirectionLeft:
self.selectedView = self.viewsAtTapPoint[MIN(max, currentIdx + 1)];
break;
}
case UIGestureRecognizerStateChanged: {
static CGFloat kNextLevelThreshold = 20.f;
CGFloat lastX = self.selectedViewLastPanX;
NSInteger newSelection = currentIdx;
// Left, go down the hierarchy
if (locationX < lastX && (lastX - locationX) >= kNextLevelThreshold) {
// Choose a new view index up to the max index
newSelection = MIN(max, currentIdx + 1);
self.selectedViewLastPanX = locationX;
}
// Right, go up the hierarchy
else if (lastX < locationX && (locationX - lastX) >= kNextLevelThreshold) {
// Choose a new view index down to the min index
newSelection = MAX(0, currentIdx - 1);
self.selectedViewLastPanX = locationX;
}
if (currentIdx != newSelection) {
self.selectedView = self.viewsAtTapPoint[newSelection];
[self actuateSelectionChangedFeedback];
}
case UISwipeGestureRecognizerDirectionRight:
self.selectedView = self.viewsAtTapPoint[MAX(0, currentIdx - 1)];
break;
}
default: break;
}
}
- (void)actuateSelectionChangedFeedback {
if (@available(iOS 10.0, *)) {
[self.selectionFBG selectionChanged];
default:
break;
}
}
@@ -918,33 +872,20 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
[super dismissViewControllerAnimated:animated completion:completion];
}
- (BOOL)wantsWindowToBecomeKey {
- (BOOL)wantsWindowToBecomeKey
{
return self.window.previousKeyWindow != nil;
}
- (void)toggleToolWithViewControllerProvider:(UINavigationController *(^)(void))future
completion:(void (^)(void))completion {
completion:(void(^)(void))completion {
if (self.presentedViewController) {
// We do NOT want to present the future; this is
// a convenience method for toggling the SAME TOOL
[self dismissViewControllerAnimated:YES completion:completion];
} else if (future) {
[self presentViewController:future() animated:YES completion:completion];
}
}
- (void)presentTool:(UINavigationController *(^)(void))future
completion:(void (^)(void))completion {
if (self.presentedViewController) {
// If a tool is already presented, dismiss it first
[self dismissViewControllerAnimated:YES completion:^{
[self presentViewController:future() animated:YES completion:completion];
}];
} else if (future) {
[self presentViewController:future() animated:YES completion:completion];
}
}
- (FLEXWindow *)window {
return (id)self.view.window;
}
@@ -983,7 +924,11 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
} else {
return [FLEXHierarchyViewController delegate:self];
}
} completion:completion];
} completion:^{
if (completion) {
completion();
}
}];
}
- (void)toggleMenuTool {
+5
View File
@@ -12,11 +12,16 @@
#import <FLEX/CALayer+FLEX.h>
#import <FLEX/UIFont+FLEX.h>
#import <FLEX/UIGestureRecognizer+Blocks.h>
#import <FLEX/UIView+FLEX_Layout.h>
#import <FLEX/UIPasteboard+FLEX.h>
#import <FLEX/UIMenu+FLEX.h>
#import <FLEX/UITextField+Range.h>
#import <FLEX/NSObject+FLEX_Reflection.h>
#import <FLEX/NSArray+FLEX.h>
#import <FLEX/NSDictionary+ObjcRuntime.h>
#import <FLEX/NSString+ObjcRuntime.h>
#import <FLEX/NSString+FLEX.h>
#import <FLEX/NSUserDefaults+FLEX.h>
#import <FLEX/NSMapTable+FLEX_Subscripting.h>
#import <FLEX/NSTimer+FLEX.h>
+1
View File
@@ -20,5 +20,6 @@
#import <FLEX/FLEX-Categories.h>
#import <FLEX/FLEX-ObjectExploring.h>
#import <FLEX/FLEXMacros.h>
#import <FLEX/FLEXAlert.h>
#import <FLEX/FLEXResources.h>
@@ -8,21 +8,12 @@
#import <UIKit/UIKit.h>
@class FLEXDBQueryRowCell;
extern NSString * const kFLEXDBQueryRowCellReuse;
@protocol FLEXDBQueryRowCellLayoutSource <NSObject>
- (CGFloat)dbQueryRowCell:(FLEXDBQueryRowCell *)dbQueryRowCell minXForColumn:(NSUInteger)column;
- (CGFloat)dbQueryRowCell:(FLEXDBQueryRowCell *)dbQueryRowCell widthForColumn:(NSUInteger)column;
@end
@interface FLEXDBQueryRowCell : UITableViewCell
/// An array of NSString, NSNumber, or NSData objects
@property (nonatomic) NSArray *data;
@property (nonatomic, weak) id<FLEXDBQueryRowCellLayoutSource> layoutSource;
@end
@@ -63,12 +63,11 @@ NSString * const kFLEXDBQueryRowCellReuse = @"kFLEXDBQueryRowCellReuse";
- (void)layoutSubviews {
[super layoutSubviews];
CGFloat width = self.contentView.frame.size.width / self.labels.count;
CGFloat height = self.contentView.frame.size.height;
[self.labels flex_forEach:^(UILabel *label, NSUInteger i) {
CGFloat width = [self.layoutSource dbQueryRowCell:self widthForColumn:i];
CGFloat minX = [self.layoutSource dbQueryRowCell:self minXForColumn:i];
label.frame = CGRectMake(minX + 5, 0, (width - 10), height);
label.frame = CGRectMake(width * i + 5, 0, (width - 10), height);
}];
}
@@ -29,7 +29,6 @@
@optional
- (NSArray<NSString *> *)queryRowIDsInTable:(NSString *)tableName;
- (FLEXSQLResult *)executeStatement:(NSString *)SQLStatement;
@end
@@ -29,7 +29,7 @@
- (NSString *)rowTitle:(NSInteger)row;
- (NSArray<NSString *> *)contentForRow:(NSInteger)row;
- (CGFloat)multiColumnTableView:(FLEXMultiColumnTableView *)tableView minWidthForContentCellInColumn:(NSInteger)column;
- (CGFloat)multiColumnTableView:(FLEXMultiColumnTableView *)tableView widthForContentCellInColumn:(NSInteger)column;
- (CGFloat)multiColumnTableView:(FLEXMultiColumnTableView *)tableView heightForContentCellInRow:(NSInteger)row;
- (CGFloat)heightForTopHeaderInTableView:(FLEXMultiColumnTableView *)tableView;
- (CGFloat)widthForLeftHeaderInTableView:(FLEXMultiColumnTableView *)tableView;
@@ -9,12 +9,10 @@
#import "FLEXMultiColumnTableView.h"
#import "FLEXDBQueryRowCell.h"
#import "FLEXTableLeftCell.h"
#import "NSArray+FLEX.h"
#import "FLEXColor.h"
@interface FLEXMultiColumnTableView () <
UITableViewDataSource, UITableViewDelegate,
UIScrollViewDelegate, FLEXDBQueryRowCellLayoutSource
UITableViewDataSource, UITableViewDelegate, UIScrollViewDelegate
>
@property (nonatomic) UIScrollView *contentScrollView;
@@ -23,12 +21,12 @@
@property (nonatomic) UITableView *contentTableView;
@property (nonatomic) UIView *leftHeader;
@property (nonatomic) NSArray<UIView *> *headerViews;
/// \c NSNotFound if no column selected
@property (nonatomic) NSInteger sortColumn;
@property (nonatomic) FLEXTableColumnHeaderSortType sortType;
@property (nonatomic) NSArray *rowData;
@property (nonatomic, readonly) NSInteger numberOfColumns;
@property (nonatomic, readonly) NSInteger numberOfRows;
@property (nonatomic, readonly) CGFloat topHeaderHeight;
@@ -73,9 +71,9 @@ static const CGFloat kColumnMargin = 1;
}
CGFloat contentWidth = 0.0;
NSInteger columnsCount = self.numberOfColumns;
for (int i = 0; i < columnsCount; i++) {
contentWidth += CGRectGetWidth(self.headerViews[i].bounds);
NSInteger rowsCount = self.numberOfColumns;
for (int i = 0; i < rowsCount; i++) {
contentWidth += [self contentWidthForColumn:i];
}
CGFloat contentHeight = height - topheaderHeight - topInsets;
@@ -149,30 +147,26 @@ static const CGFloat kColumnMargin = 1;
#pragma mark - Data
- (void)reloadData {
[self loadHeaderData];
[self loadLeftViewData];
[self loadContentData];
[self loadHeaderData];
}
- (void)loadHeaderData {
// Remove existing headers, if any
for (UIView *subview in self.headerViews) {
for (UIView *subview in self.headerScrollView.subviews) {
[subview removeFromSuperview];
}
__block CGFloat xOffset = 0;
self.headerViews = [NSArray flex_forEachUpTo:self.numberOfColumns map:^id(NSUInteger column) {
FLEXTableColumnHeader *header = [FLEXTableColumnHeader new];
CGFloat xOffset = 0.0;
for (NSInteger column = 0; column < self.numberOfColumns; column++) {
CGFloat width = [self contentWidthForColumn:column] + self.columnMargin;
FLEXTableColumnHeader *header = [[FLEXTableColumnHeader alloc]
initWithFrame:CGRectMake(xOffset, 0, width, self.topHeaderHeight - 1)
];
header.titleLabel.text = [self columnTitle:column];
CGSize fittingSize = CGSizeMake(CGFLOAT_MAX, self.topHeaderHeight - 1);
CGFloat width = self.columnMargin + MAX(
[self minContentWidthForColumn:column],
[header sizeThatFits:fittingSize].width
);
header.frame = CGRectMake(xOffset, 0, width, self.topHeaderHeight - 1);
if (column == self.sortColumn) {
header.sortType = self.sortType;
}
@@ -184,22 +178,21 @@ static const CGFloat kColumnMargin = 1;
[header addGestureRecognizer:gesture];
header.userInteractionEnabled = YES;
xOffset += width;
[self.headerScrollView addSubview:header];
return header;
}];
xOffset += width;
}
}
- (void)contentHeaderTap:(UIGestureRecognizer *)gesture {
NSInteger newSortColumn = [self.headerViews indexOfObject:gesture.view];
NSInteger newSortColumn = [self.headerScrollView.subviews indexOfObject:gesture.view];
FLEXTableColumnHeaderSortType newType = FLEXNextTableColumnHeaderSortType(self.sortType);
// Reset old header
FLEXTableColumnHeader *oldHeader = (id)self.headerViews[self.sortColumn];
FLEXTableColumnHeader *oldHeader = (id)self.headerScrollView.subviews[self.sortColumn];
oldHeader.sortType = FLEXTableColumnHeaderSortTypeNone;
// Update new header
FLEXTableColumnHeader *newHeader = (id)self.headerViews[newSortColumn];
FLEXTableColumnHeader *newHeader = (id)self.headerScrollView.subviews[newSortColumn];
newHeader.sortType = newType;
// Update self
@@ -234,13 +227,13 @@ static const CGFloat kColumnMargin = 1;
}
// Right side table view for data
else {
self.rowData = [self.dataSource contentForRow:indexPath.row];
FLEXDBQueryRowCell *cell = [tableView
dequeueReusableCellWithIdentifier:kFLEXDBQueryRowCellReuse forIndexPath:indexPath
];
cell.contentView.backgroundColor = backgroundColor;
cell.data = [self.dataSource contentForRow:indexPath.row];
cell.layoutSource = self;
NSAssert(cell.data.count == self.numberOfColumns, @"Count of data provided was incorrect");
return cell;
}
@@ -287,17 +280,6 @@ static const CGFloat kColumnMargin = 1;
}
#pragma mark FLEXDBQueryRowCellLayoutSource
- (CGFloat)dbQueryRowCell:(FLEXDBQueryRowCell *)dbQueryRowCell minXForColumn:(NSUInteger)column {
return CGRectGetMinX(self.headerViews[column].frame);
}
- (CGFloat)dbQueryRowCell:(FLEXDBQueryRowCell *)dbQueryRowCell widthForColumn:(NSUInteger)column {
return CGRectGetWidth(self.headerViews[column].bounds);
}
#pragma mark DataSource Accessor
- (NSInteger)numberOfRows {
@@ -316,8 +298,8 @@ static const CGFloat kColumnMargin = 1;
return [self.dataSource rowTitle:row];
}
- (CGFloat)minContentWidthForColumn:(NSInteger)column {
return [self.dataSource multiColumnTableView:self minWidthForContentCellInColumn:column];
- (CGFloat)contentWidthForColumn:(NSInteger)column {
return [self.dataSource multiColumnTableView:self widthForContentCellInColumn:column];
}
- (CGFloat)contentHeightForRow:(NSInteger)row {
@@ -30,7 +30,7 @@ static NSString * const QUERY_TABLENAMES = @"SELECT name FROM sqlite_master WHER
- (instancetype)initWithPath:(NSString *)path {
self = [super init];
if (self) {
self.path = path;
self.path = path;;
}
return self;
@@ -114,17 +114,9 @@ static NSString * const QUERY_TABLENAMES = @"SELECT name FROM sqlite_master WHER
}
- (NSArray<NSArray *> *)queryAllDataInTable:(NSString *)tableName {
NSString *command = [NSString stringWithFormat:@"SELECT * FROM \"%@\"", tableName];
return [self executeStatement:command].rows ?: @[];
}
- (NSArray<NSString *> *)queryRowIDsInTable:(NSString *)tableName {
NSString *command = [NSString stringWithFormat:@"SELECT rowid FROM \"%@\"", tableName];
NSArray<NSArray<NSString *> *> *data = [self executeStatement:command].rows ?: @[];
return [data flex_mapped:^id(NSArray<NSString *> *obj, NSUInteger idx) {
return obj.firstObject;
}];
return [self executeStatement:[@"SELECT * FROM "
stringByAppendingString:tableName
]].rows ?: @[];
}
- (FLEXSQLResult *)executeStatement:(NSString *)sql {
@@ -265,7 +257,7 @@ static NSString * const QUERY_TABLENAMES = @"SELECT name FROM sqlite_master WHER
- (FLEXSQLResult *)errorResult:(NSString *)description {
const char *error = sqlite3_errmsg(_db);
NSString *message = error ? @(error) : [NSString
stringWithFormat:@"(%@: empty error)", description
stringWithFormat:@"(%@: empty error", description
];
return [FLEXSQLResult error:message];
@@ -11,9 +11,6 @@
#import "UIFont+FLEX.h"
#import "FLEXUtility.h"
static const CGFloat kMargin = 5;
static const CGFloat kArrowWidth = 20;
@interface FLEXTableColumnHeader ()
@property (nonatomic, readonly) UILabel *arrowLabel;
@property (nonatomic, readonly) UIView *lineView;
@@ -63,16 +60,9 @@ static const CGFloat kArrowWidth = 20;
CGSize size = self.frame.size;
self.titleLabel.frame = CGRectMake(kMargin, 0, size.width - kArrowWidth - kMargin, size.height);
self.arrowLabel.frame = CGRectMake(size.width - kArrowWidth, 0, kArrowWidth, size.height);
self.titleLabel.frame = CGRectMake(5, 0, size.width - 25, size.height);
self.arrowLabel.frame = CGRectMake(size.width - 20, 0, 20, size.height);
self.lineView.frame = CGRectMake(size.width - 1, 2, FLEXPointsToPixels(1), size.height - 4);
}
- (CGSize)sizeThatFits:(CGSize)size {
CGFloat margins = kArrowWidth - 2 * kMargin;
size = CGSizeMake(size.width - margins, size.height);
CGFloat width = [_titleLabel sizeThatFits:size].width + margins;
return CGSizeMake(width, size.height);
}
@end
@@ -7,20 +7,10 @@
//
#import <UIKit/UIKit.h>
#import "FLEXDatabaseManager.h"
NS_ASSUME_NONNULL_BEGIN
@interface FLEXTableContentViewController : UIViewController
/// Display a table with the given columns, rows, and name.
/// @param databaseManager an optional manager to allow modifying the table.
+ (instancetype)columns:(NSArray<NSString *> *)columnNames
rows:(NSArray<NSArray<NSString *> *> *)rowData
rowIDs:(nullable NSArray<NSString *> *)rowIds
tableName:(NSString *)tableName
database:(nullable id<FLEXDatabaseManager>)databaseManager;
rows:(NSArray<NSArray<NSString *> *> *)rowData;
@end
NS_ASSUME_NONNULL_END
@@ -10,16 +10,12 @@
#import "FLEXMultiColumnTableView.h"
#import "FLEXWebViewController.h"
#import "FLEXUtility.h"
#import "UIBarButtonItem+FLEX.h"
@interface FLEXTableContentViewController () <
FLEXMultiColumnTableViewDataSource, FLEXMultiColumnTableViewDelegate
>
@property (nonatomic, readonly) NSArray<NSString *> *columns;
@property (nonatomic) NSMutableArray<NSArray *> *rows;
@property (nonatomic, readonly) NSString *tableName;
@property (nonatomic, nullable) NSMutableArray<NSString *> *rowIDs;
@property (nonatomic, readonly, nullable) id<FLEXDatabaseManager> databaseManager;
@property (nonatomic, copy) NSArray<NSArray *> *rows;
@property (nonatomic) FLEXMultiColumnTableView *multiColumnView;
@end
@@ -27,16 +23,10 @@
@implementation FLEXTableContentViewController
+ (instancetype)columns:(NSArray<NSString *> *)columnNames
rows:(NSArray<NSArray<NSString *> *> *)rowData
rowIDs:(nullable NSArray<NSString *> *)rowIDs
tableName:(NSString *)tableName
database:(nullable id<FLEXDatabaseManager>)databaseManager {
rows:(NSArray<NSArray<NSString *> *> *)rowData {
FLEXTableContentViewController *controller = [self new];
controller->_columns = columnNames.copy;
controller->_rows = rowData.mutableCopy;
controller->_rowIDs = rowIDs.mutableCopy;
controller->_tableName = tableName.copy;
controller->_databaseManager = databaseManager;
controller->_columns = columnNames;
controller->_rows = rowData;
return controller;
}
@@ -48,10 +38,9 @@
- (void)viewDidLoad {
[super viewDidLoad];
self.title = self.tableName;
self.edgesForExtendedLayout = UIRectEdgeNone;
[self.multiColumnView reloadData];
[self setupToolbarItems];
}
- (FLEXMultiColumnTableView *)multiColumnView {
@@ -95,8 +84,8 @@
}
- (CGFloat)multiColumnTableView:(FLEXMultiColumnTableView *)tableView
minWidthForContentCellInColumn:(NSInteger)column {
return 100;
widthForContentCellInColumn:(NSInteger)column {
return 120;
}
- (CGFloat)heightForTopHeaderInTableView:(FLEXMultiColumnTableView *)tableView {
@@ -129,27 +118,6 @@
make.button(@"Copy").handler(^(NSArray<NSString *> *strings) {
UIPasteboard.generalPasteboard.string = message;
});
// Option to delete row
BOOL hasRowID = self.rows.count && row < self.rows.count;
if (hasRowID) {
make.button(@"Delete").destructiveStyle().handler(^(NSArray<NSString *> *strings) {
NSString *deleteRow = [NSString stringWithFormat:
@"DELETE FROM %@ WHERE rowid = %@",
self.tableName, self.rowIDs[row]
];
[self executeStatementAndShowResult:deleteRow completion:^(BOOL success) {
// Remove deleted row and reload view
if (success) {
[self.rowIDs removeObjectAtIndex:row];
[self.rows removeObjectAtIndex:row];
[self.multiColumnView reloadData];
}
}];
});
}
make.button(@"Dismiss").cancelStyle();
} showFrom:self];
}
@@ -159,8 +127,7 @@
sortType:(FLEXTableColumnHeaderSortType)sortType {
NSArray<NSArray *> *sortContentData = [self.rows
sortedArrayWithOptions:NSSortStable
usingComparator:^NSComparisonResult(NSArray *obj1, NSArray *obj2) {
sortedArrayUsingComparator:^NSComparisonResult(NSArray *obj1, NSArray *obj2) {
id a = obj1[column], b = obj2[column];
if (a == NSNull.null) {
return NSOrderedAscending;
@@ -168,11 +135,6 @@
if (b == NSNull.null) {
return NSOrderedDescending;
}
if ([a respondsToSelector:@selector(compare:options:)] &&
[b respondsToSelector:@selector(compare:options:)]) {
return [a compare:b options:NSNumericSearch];
}
if ([a respondsToSelector:@selector(compare:)] && [b respondsToSelector:@selector(compare:)]) {
return [a compare:b];
@@ -186,11 +148,12 @@
sortContentData = sortContentData.reverseObjectEnumerator.allObjects.copy;
}
self.rows = sortContentData.mutableCopy;
self.rows = sortContentData;
[self.multiColumnView reloadData];
}
#pragma mark - About Transition
#pragma mark -
#pragma mark About Transition
- (void)willTransitionToTraitCollection:(UITraitCollection *)newCollection
withTransitionCoordinator:(id <UIViewControllerTransitionCoordinator>)coordinator {
@@ -208,57 +171,4 @@
} completion:nil];
}
#pragma mark - Toolbar
- (void)setupToolbarItems {
// We do not support modifying realm databases
if (![self.databaseManager respondsToSelector:@selector(executeStatement:)]) {
return;
}
UIBarButtonItem *trashButton = [FLEXBarButtonItemSystem(Trash, self, @selector(trashPressed))
flex_withTintColor:UIColor.redColor
];
trashButton.enabled = self.databaseManager && self.rows.count;
self.toolbarItems = @[UIBarButtonItem.flex_flexibleSpace, trashButton];
}
- (void)trashPressed {
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(@"Delete All Rows");
make.message(@"All rows in this table will be permanently deleted.\nDo you want to proceed?");
make.button(@"Yes, I'm sure").destructiveStyle().handler(^(NSArray<NSString *> *strings) {
NSString *deleteAll = [NSString stringWithFormat:@"DELETE FROM %@", self.tableName];
[self executeStatementAndShowResult:deleteAll completion:^(BOOL success) {
// Only dismiss on success
if (success) {
[self.navigationController popViewControllerAnimated:YES];
}
}];
});
make.button(@"Cancel").cancelStyle();
} showFrom:self];
}
#pragma mark - Helpers
- (void)executeStatementAndShowResult:(NSString *)statement completion:(void (^_Nullable)(BOOL success))completion {
FLEXSQLResult *result = [self.databaseManager executeStatement:statement];
[FLEXAlert makeAlert:^(FLEXAlert *make) {
if (result.isError) {
make.title(@"Error");
}
make.message(result.message ?: @"<no output>");
make.button(@"Dismiss").cancelStyle().handler(^(NSArray<NSString *> *_) {
if (completion) {
completion(!result.isError);
}
});
} showFrom:self];
}
@end
@@ -14,7 +14,6 @@
#import "FLEXMutableListSection.h"
#import "NSArray+FLEX.h"
#import "FLEXAlert.h"
#import "FLEXMacros.h"
@interface FLEXTableListViewController ()
@property (nonatomic, readonly) id<FLEXDatabaseManager> dbm;
@@ -71,10 +70,9 @@
self.tables.selectionHandler = ^(FLEXTableListViewController *host, NSString *tableName) {
NSArray *rows = [host.dbm queryAllDataInTable:tableName];
NSArray *columns = [host.dbm queryAllColumnsOfTable:tableName];
NSArray *rowIDs = [host.dbm queryRowIDsInTable:tableName];
UIViewController *resultsScreen = [FLEXTableContentViewController
columns:columns rows:rows rowIDs:rowIDs tableName:tableName database:host.dbm
];
UIViewController *resultsScreen = [FLEXTableContentViewController columns:columns rows:rows];
resultsScreen.title = tableName;
[host.navigationController pushViewController:resultsScreen animated:YES];
};
@@ -102,7 +100,7 @@
[FLEXAlert showAlert:@"Message" message:result.message from:self];
} else {
UIViewController *resultsScreen = [FLEXTableContentViewController
columns:result.columns rows:result.rows rowIDs:nil tableName:@"" database:nil
columns:result.columns rows:result.rows
];
[self.navigationController pushViewController:resultsScreen animated:YES];
@@ -35,7 +35,6 @@ static const NSInteger kFLEXLiveObjectsSortBySizeIndex = 2;
self.showsSearchBar = YES;
self.showSearchBarInitially = YES;
self.activatesSearchBarAutomatically = YES;
self.searchBarDebounceInterval = kFLEXDebounceInstant;
self.showsCarousel = YES;
self.carousel.items = @[@"A→Z", @"Count", @"Size"];
@@ -20,25 +20,11 @@
#import <malloc/malloc.h>
typedef NS_ENUM(NSUInteger, FLEXObjectReferenceSection) {
FLEXObjectReferenceSectionMain,
FLEXObjectReferenceSectionAutoLayout,
FLEXObjectReferenceSectionKVO,
FLEXObjectReferenceSectionFLEX,
FLEXObjectReferenceSectionCount
};
@interface FLEXObjectListViewController ()
@property (nonatomic, readonly, class) NSArray<NSPredicate *> *defaultPredicates;
@property (nonatomic, readonly, class) NSArray<NSString *> *defaultSectionTitles;
@property (nonatomic, copy) NSArray<FLEXMutableListSection *> *sections;
@property (nonatomic, copy) NSArray<FLEXMutableListSection *> *allSections;
@property (nonatomic, readonly, nullable) NSArray<FLEXObjectRef *> *references;
@property (nonatomic, readonly) NSArray<FLEXObjectRef *> *references;
@property (nonatomic, readonly) NSArray<NSPredicate *> *predicates;
@property (nonatomic, readonly) NSArray<NSString *> *sectionTitles;
@@ -52,7 +38,7 @@ typedef NS_ENUM(NSUInteger, FLEXObjectReferenceSection) {
+ (NSPredicate *)defaultPredicateForSection:(NSInteger)section {
// These are the types of references that we typically don't care about.
// We want this list of "object-ivar pairs" split into two sections.
BOOL(^isKVORelated)(FLEXObjectRef *, NSDictionary *) = ^BOOL(FLEXObjectRef *ref, NSDictionary *bindings) {
BOOL(^isObserver)(FLEXObjectRef *, NSDictionary *) = ^BOOL(FLEXObjectRef *ref, NSDictionary *bindings) {
NSString *row = ref.reference;
return [row isEqualToString:@"__NSObserver object"] ||
[row isEqualToString:@"_CFXNotificationObjcObserverRegistration _object"];
@@ -79,50 +65,34 @@ typedef NS_ENUM(NSUInteger, FLEXObjectReferenceSection) {
([row hasPrefix:@"_NSAutoresizingMask"] && [row hasSuffix:@" _referenceItem"]) ||
[ignored containsObject:row];
};
/// These are FLEX classes and usually you aren't looking for FLEX references inside FLEX itself
BOOL(^isFLEXClass)(FLEXObjectRef *, NSDictionary *) = ^BOOL(FLEXObjectRef *ref, NSDictionary *bindings) {
return [ref.reference hasPrefix:@"FLEX"];
};
BOOL(^isEssential)(FLEXObjectRef *, NSDictionary *) = ^BOOL(FLEXObjectRef *ref, NSDictionary *bindings) {
return !(
isKVORelated(ref, bindings) ||
isConstraintRelated(ref, bindings) ||
isFLEXClass(ref, bindings)
);
return !(isObserver(ref, bindings) || isConstraintRelated(ref, bindings));
};
switch (section) {
case FLEXObjectReferenceSectionMain:
return [NSPredicate predicateWithBlock:isEssential];
case FLEXObjectReferenceSectionAutoLayout:
return [NSPredicate predicateWithBlock:isConstraintRelated];
case FLEXObjectReferenceSectionKVO:
return [NSPredicate predicateWithBlock:isKVORelated];
case FLEXObjectReferenceSectionFLEX:
return [NSPredicate predicateWithBlock:isFLEXClass];
case 0: return [NSPredicate predicateWithBlock:isEssential];
case 1: return [NSPredicate predicateWithBlock:isConstraintRelated];
case 2: return [NSPredicate predicateWithBlock:isObserver];
default: return nil;
}
}
+ (NSArray<NSPredicate *> *)defaultPredicates {
return [NSArray flex_forEachUpTo:FLEXObjectReferenceSectionCount map:^id(NSUInteger i) {
return [self defaultPredicateForSection:i];
}];
return @[[self defaultPredicateForSection:0],
[self defaultPredicateForSection:1],
[self defaultPredicateForSection:2]];
}
+ (NSArray<NSString *> *)defaultSectionTitles {
return @[
@"", @"AutoLayout", @"Key-Value Observing", @"FLEX"
];
return @[@"", @"AutoLayout", @"Trivial"];
}
#pragma mark - Initialization
- (id)initWithReferences:(nullable NSArray<FLEXObjectRef *> *)references {
- (id)initWithReferences:(NSArray<FLEXObjectRef *> *)references {
return [self initWithReferences:references predicates:nil sectionTitles:nil];
}
@@ -214,18 +184,19 @@ typedef NS_ENUM(NSUInteger, FLEXObjectReferenceSection) {
}
}
free(ivars);
tryClass = class_getSuperclass(tryClass);
}
}];
NSArray<NSPredicate *> *predicates = [self defaultPredicates];
NSArray<NSString *> *sectionTitles = [self defaultSectionTitles];
FLEXObjectListViewController *viewController = [[self alloc]
initWithReferences:instances
predicates:self.defaultPredicates
sectionTitles:self.defaultSectionTitles
predicates:predicates
sectionTitles:sectionTitles
];
viewController.title = [NSString stringWithFormat:@"Referencing %@ %p",
[FLEXRuntimeUtility safeClassNameForObject:object], object
NSStringFromClass(object_getClass(object)), object
];
return viewController;
}
@@ -260,7 +231,7 @@ typedef NS_ENUM(NSUInteger, FLEXObjectReferenceSection) {
}];
}
- (FLEXMutableListSection *)makeSection:(NSArray *)rows title:(NSString *)title { weakify(self)
- (FLEXMutableListSection *)makeSection:(NSArray *)rows title:(NSString *)title {
FLEXMutableListSection *section = [FLEXMutableListSection list:rows
cellConfiguration:^(FLEXTableViewCell *cell, FLEXObjectRef *ref, NSInteger row) {
cell.textLabel.text = ref.reference;
@@ -275,10 +246,14 @@ typedef NS_ENUM(NSUInteger, FLEXObjectReferenceSection) {
}
];
section.selectionHandler = ^(UIViewController *host, FLEXObjectRef *ref) { strongify(self)
[self.navigationController pushViewController:[
FLEXObjectExplorerFactory explorerViewControllerForObject:ref.object
] animated:YES];
__weak __typeof(self) weakSelf = self;
section.selectionHandler = ^(__kindof UIViewController *host, FLEXObjectRef *ref) {
__strong __typeof(self) strongSelf = weakSelf;
if (strongSelf) {
[strongSelf.navigationController pushViewController:[
FLEXObjectExplorerFactory explorerViewControllerForObject:ref.object
] animated:YES];
}
};
section.customTitle = title;
+1 -1
View File
@@ -47,7 +47,7 @@
_object = object;
_wantsSummary = showSummary;
NSString *class = [FLEXRuntimeUtility safeClassNameForObject:object];
NSString *class = NSStringFromClass(object_getClass(object));
if (ivar) {
_reference = [NSString stringWithFormat:@"%@ %@", class, ivar];
} else if (showSummary) {
@@ -25,7 +25,7 @@
WKWebViewConfiguration *configuration = [WKWebViewConfiguration new];
if (@available(iOS 10.0, *)) {
configuration.dataDetectorTypes = WKDataDetectorTypeLink;
configuration.dataDetectorTypes = UIDataDetectorTypeLink;
}
self.webView = [[WKWebView alloc] initWithFrame:CGRectZero configuration:configuration];
@@ -54,8 +54,9 @@ typedef NS_ENUM(NSUInteger, FLEXFileBrowserSortAttribute) {
self.title = [path lastPathComponent];
self.operationQueue = [NSOperationQueue new];
// Compute path size
weakify(self)
//computing path size
FLEXFileBrowserController *__weak weakSelf = self;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSFileManager *fileManager = NSFileManager.defaultManager;
NSDictionary<NSString *, id> *attributes = [fileManager attributesOfItemAtPath:path error:NULL];
@@ -65,15 +66,16 @@ typedef NS_ENUM(NSUInteger, FLEXFileBrowserSortAttribute) {
attributes = [fileManager attributesOfItemAtPath:[path stringByAppendingPathComponent:fileName] error:NULL];
totalSize += [attributes fileSize];
// Bail if the interested view controller has gone away
if (!self) {
// Bail if the interested view controller has gone away.
if (!weakSelf) {
return;
}
}
dispatch_async(dispatch_get_main_queue(), ^{ strongify(self)
self.recursiveSize = @(totalSize);
[self.tableView reloadData];
dispatch_async(dispatch_get_main_queue(), ^{
FLEXFileBrowserController *__strong strongSelf = weakSelf;
strongSelf.recursiveSize = @(totalSize);
[strongSelf.tableView reloadData];
});
});
@@ -356,43 +358,44 @@ typedef NS_ENUM(NSUInteger, FLEXFileBrowserSortAttribute) {
// Since our actions are outside of that protocol, we need to manually handle the action forwarding from the cells.
}
- (UIContextMenuConfiguration *)tableView:(UITableView *)tableView
contextMenuConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath
point:(CGPoint)point __IOS_AVAILABLE(13.0) {
weakify(self)
return [UIContextMenuConfiguration configurationWithIdentifier:nil previewProvider:nil
actionProvider:^UIMenu *(NSArray<UIMenuElement *> *suggestedActions) {
UITableViewCell * const cell = [tableView cellForRowAtIndexPath:indexPath];
UIAction *rename = [UIAction actionWithTitle:@"Rename" image:nil identifier:@"Rename"
handler:^(UIAction *action) { strongify(self)
[self fileBrowserRename:cell];
}
];
UIAction *delete = [UIAction actionWithTitle:@"Delete" image:nil identifier:@"Delete"
handler:^(UIAction *action) { strongify(self)
[self fileBrowserDelete:cell];
}
];
UIAction *copyPath = [UIAction actionWithTitle:@"Copy Path" image:nil identifier:@"Copy Path"
handler:^(UIAction *action) { strongify(self)
[self fileBrowserCopyPath:cell];
}
];
UIAction *share = [UIAction actionWithTitle:@"Share" image:nil identifier:@"Share"
handler:^(UIAction *action) { strongify(self)
[self fileBrowserShare:cell];
}
];
return [UIMenu menuWithTitle:@"Manage File" image:nil
identifier:@"Manage File"
options:UIMenuOptionsDisplayInline
children:@[rename, delete, copyPath, share]
];
}
];
#if FLEX_AT_LEAST_IOS13_SDK
- (UIContextMenuConfiguration *)tableView:(UITableView *)tableView contextMenuConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath point:(CGPoint)point __IOS_AVAILABLE(13.0) {
__weak __typeof__(self) weakSelf = self;
return [UIContextMenuConfiguration configurationWithIdentifier:nil
previewProvider:nil
actionProvider:^UIMenu * _Nullable(NSArray<UIMenuElement *> * _Nonnull suggestedActions) {
UITableViewCell * const cell = [tableView cellForRowAtIndexPath:indexPath];
UIAction *rename = [UIAction actionWithTitle:@"Rename"
image:nil
identifier:@"Rename"
handler:^(__kindof UIAction * _Nonnull action) {
[weakSelf fileBrowserRename:cell];
}];
UIAction *delete = [UIAction actionWithTitle:@"Delete"
image:nil
identifier:@"Delete"
handler:^(__kindof UIAction * _Nonnull action) {
[weakSelf fileBrowserDelete:cell];
}];
UIAction *copyPath = [UIAction actionWithTitle:@"Copy Path"
image:nil
identifier:@"Copy Path"
handler:^(__kindof UIAction * _Nonnull action) {
[weakSelf fileBrowserCopyPath:cell];
}];
UIAction *share = [UIAction actionWithTitle:@"Share"
image:nil
identifier:@"Share"
handler:^(__kindof UIAction * _Nonnull action) {
[weakSelf fileBrowserShare:cell];
}];
return [UIMenu menuWithTitle:@"Manage File" image:nil identifier:@"Manage File" options:UIMenuOptionsDisplayInline children:@[rename, delete, copyPath, share]];
}];
}
#endif
- (void)openFileController:(NSString *)fullPath {
UIDocumentInteractionController *controller = [UIDocumentInteractionController new];
controller.URL = [NSURL fileURLWithPath:fullPath];
@@ -470,9 +473,6 @@ contextMenuConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath
} else {
// Share sheet for files
UIActivityViewController *shareSheet = [[UIActivityViewController alloc] initWithActivityItems:@[filePath] applicationActivities:nil];
if (UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad) {
shareSheet.popoverPresentationController.sourceView = sender;
}
[self presentViewController:shareSheet animated:true completion:nil];
}
}
@@ -34,9 +34,6 @@ extern NSString *const kFLEXKeychainClassKey;
/// Item description.
extern NSString *const kFLEXKeychainDescriptionKey;
/// Item group.
extern NSString *const kFLEXKeychainGroupKey;
/// Item label.
extern NSString *const kFLEXKeychainLabelKey;
@@ -15,7 +15,6 @@ NSString * const kFLEXKeychainAccountKey = @"acct";
NSString * const kFLEXKeychainCreatedAtKey = @"cdat";
NSString * const kFLEXKeychainClassKey = @"labl";
NSString * const kFLEXKeychainDescriptionKey = @"desc";
NSString * const kFLEXKeychainGroupKey = @"agrp";
NSString * const kFLEXKeychainLabelKey = @"labl";
NSString * const kFLEXKeychainLastModifiedKey = @"mdat";
NSString * const kFLEXKeychainWhereKey = @"svce";
@@ -170,7 +170,7 @@
- (NSString *)password {
if (self.passwordData.length) {
return [[NSString alloc] initWithData:self.passwordData encoding:NSUTF8StringEncoding];
return [NSString stringWithCString:self.passwordData.bytes encoding:NSUTF8StringEncoding];
}
return nil;
@@ -30,10 +30,10 @@
- (void)viewDidLoad {
[super viewDidLoad];
[self addToolbarItems:@[
FLEXBarButtonItemSystem(Add, self, @selector(addPressed)),
[FLEXBarButtonItemSystem(Trash, self, @selector(trashPressed:)) flex_withTintColor:UIColor.redColor],
]];
self.navigationItem.rightBarButtonItems = @[
[UIBarButtonItem flex_systemItem:UIBarButtonSystemItemTrash target:self action:@selector(trashPressed:)],
[UIBarButtonItem flex_systemItem:UIBarButtonSystemItemAdd target:self action:@selector(addPressed)],
];
[self reloadData];
}
@@ -43,15 +43,14 @@
cellConfiguration:^(__kindof FLEXTableViewCell *cell, NSDictionary *item, NSInteger row) {
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
id service = item[kFLEXKeychainWhereKey];
if ([service isKindOfClass:[NSString class]]) {
cell.textLabel.text = service;
cell.detailTextLabel.text = [item[kFLEXKeychainAccountKey] description];
id account = item[kFLEXKeychainAccountKey];
if ([account isKindOfClass:[NSString class]]) {
cell.textLabel.text = account;
} else {
cell.textLabel.text = [NSString stringWithFormat:
@"[%@]\n\n%@",
NSStringFromClass([service class]),
[service description]
NSStringFromClass([account class]),
[account description]
];
}
} filterMatcher:^BOOL(NSString *filterText, NSDictionary *item) {
@@ -99,9 +98,8 @@
NSDictionary *item = self.section.filteredList[idx];
FLEXKeychainQuery *query = [FLEXKeychainQuery new];
query.service = [item[kFLEXKeychainWhereKey] description];
query.account = [item[kFLEXKeychainAccountKey] description];
query.accessGroup = [item[kFLEXKeychainGroupKey] description];
query.service = item[kFLEXKeychainWhereKey];
query.account = item[kFLEXKeychainAccountKey];
[query fetch:nil];
return query;
@@ -131,18 +129,6 @@
make.title(@"Clear Keychain");
make.message(@"This will remove all keychain items for this app.\n");
make.message(@"This action cannot be undone. Are you sure?");
make.button(@"Yes, clear the keychain").destructiveStyle().handler(^(NSArray *strings) {
[self confirmClearKeychain];
});
make.button(@"Cancel").cancelStyle();
} showFrom:self source:sender];
}
- (void)confirmClearKeychain {
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(@"ARE YOU SURE?");
make.message(@"This action CANNOT BE UNDONE.\nAre you sure you want to continue?\n");
make.message(@"If you're sure, scroll to confirm.");
make.button(@"Yes, clear the keychain").destructiveStyle().handler(^(NSArray *strings) {
for (id account in self.section.list) {
[self deleteItem:account];
@@ -150,12 +136,8 @@
[self reloadData];
});
make.button(@"Cancel"); make.button(@"Cancel"); make.button(@"Cancel"); make.button(@"Cancel");
make.button(@"Cancel"); make.button(@"Cancel"); make.button(@"Cancel"); make.button(@"Cancel");
make.button(@"Cancel"); make.button(@"Cancel"); make.button(@"Cancel"); make.button(@"Cancel");
make.button(@"Cancel"); make.button(@"Cancel"); make.button(@"Cancel"); make.button(@"Cancel");
make.button(@"Cancel").cancelStyle();
} showFrom:self];
} showFrom:self source:sender];
}
- (void)addPressed {
@@ -233,7 +215,6 @@
make.message(@"Service: ").message(query.service);
make.message(@"\nAccount: ").message(query.account);
make.message(@"\nPassword: ").message(query.password);
make.message(@"\nGroup: ").message(query.accessGroup);
make.button(@"Copy Service").handler(^(NSArray<NSString *> *strings) {
[UIPasteboard.generalPasteboard flex_copy:query.service];
@@ -303,14 +303,14 @@ static inline NSString * TBWildcardMap(NSString *token, NSString *candidate, TBW
if (options == TBWildcardOptionsAny) {
return [[bundles flex_flatmapped:^NSArray *(NSString *bundlePath, NSUInteger idx) {
return [self classNamesInImageAtPath:bundlePath];
}] flex_sortedUsingSelector:@selector(caseInsensitiveCompare:)];
}] sortedUsingSelector:@selector(caseInsensitiveCompare:)];
}
return [[bundles flex_flatmapped:^NSArray *(NSString *bundlePath, NSUInteger idx) {
return [[self classNamesInImageAtPath:bundlePath] flex_mapped:^id(NSString *className, NSUInteger idx) {
return TBWildcardMap(query, className, options);
}];
}] flex_sortedUsingSelector:@selector(caseInsensitiveCompare:)];
}] sortedUsingSelector:@selector(caseInsensitiveCompare:)];
}
}
@@ -51,7 +51,7 @@
[self sizeToFit];
if (@available(iOS 13, *)) {
self.appearance = UIKeyboardAppearanceDefault;
self.appearance = UIKeyboardTypeDefault;
} else {
self.appearance = UIKeyboardAppearanceLight;
}
@@ -84,6 +84,7 @@
switch (_appearance) {
default:
case UIKeyboardAppearanceDefault:
#if FLEX_AT_LEAST_IOS13_SDK
if (@available(iOS 13, *)) {
titleColor = UIColor.labelColor;
@@ -96,6 +97,7 @@
}
break;
}
#endif
case UIKeyboardAppearanceLight:
titleColor = UIColor.blackColor;
backgroundColor = lightColor;
@@ -61,7 +61,6 @@
searchBar.keyboardType = UIKeyboardTypeWebSearch;
searchBar.autocorrectionType = UITextAutocorrectionTypeNo;
if (@available(iOS 11, *)) {
searchBar.smartQuotesType = UITextSmartQuotesTypeNo;
searchBar.smartInsertDeleteType = UITextSmartInsertDeleteTypeNo;
}
@@ -102,7 +101,7 @@
// Change "Bundle.fooba" to "Bundle.foobar."
NSString *orig = self.delegate.searchController.searchBar.text;
NSString *keyPath = [orig flex_stringByReplacingLastKeyPathComponent:text];
NSString *keyPath = [orig stringByReplacingLastKeyPathComponent:text];
self.delegate.searchController.searchBar.text = keyPath;
self.keyPath = [FLEXRuntimeKeyPathTokenizer tokenizeString:keyPath];
@@ -131,7 +130,7 @@
// Available since at least iOS 9, still present in iOS 13
UITextField *field = [searchBar valueForKey:@"_searchBarTextField"];
if ([self searchBar:searchBar shouldChangeTextInRange:field.flex_selectedRange replacementText:text]) {
if ([self searchBar:searchBar shouldChangeTextInRange:field.selectedRange replacementText:text]) {
[field replaceRange:field.selectedTextRange withText:text];
}
}
@@ -267,7 +266,7 @@
self.filteredClasses = nil;
}
self.timer = [NSTimer flex_fireSecondsFromNow:0.15 block:^{
self.timer = [NSTimer fireSecondsFromNow:0.15 block:^{
[self updateTable];
}];
}
@@ -40,7 +40,7 @@
self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
if (@available(iOS 13, *)) {
self.appearance = UIKeyboardAppearanceDefault;
self.appearance = UIKeyboardTypeDefault;
} else {
self.appearance = UIKeyboardAppearanceLight;
}
@@ -106,6 +106,7 @@
switch (_appearance) {
case UIKeyboardAppearanceDefault:
#if FLEX_AT_LEAST_IOS13_SDK
if (@available(iOS 13, *)) {
borderColor = UIColor.systemBackgroundColor;
@@ -118,6 +119,7 @@
}
break;
}
#endif
case UIKeyboardAppearanceLight: {
borderColor = UIColor.clearColor;
backgroundColor = lightColor;
@@ -13,7 +13,6 @@
#import "FLEXTableView.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXAlert.h"
#import "FLEXRuntimeClient.h"
@interface FLEXObjcRuntimeViewController () <FLEXKeyPathSearchControllerDelegate>
@@ -29,24 +28,9 @@
- (void)viewDidLoad {
[super viewDidLoad];
// Long press on navigation bar to initialize webkit legacy
//
// We call initializeWebKitLegacy automatically before you search
// all bundles just to be safe (since touching some classes before
// WebKit is initialized will initialize it on a thread other than
// the main thread), but sometimes you can encounter this crash
// without searching through all bundles, of course.
[self.navigationController.navigationBar addGestureRecognizer:[
[UILongPressGestureRecognizer alloc]
initWithTarget:[FLEXRuntimeClient class]
action:@selector(initializeWebKitLegacy)
]
];
// Search bar stuff, must be first because this creates self.searchController
self.showsSearchBar = YES;
self.showSearchBarInitially = YES;
self.activatesSearchBarAutomatically = YES;
// Using pinSearchBar on this screen causes a weird visual
// thing on the next view controller that gets pushed.
//
@@ -73,6 +57,15 @@
[self.tableView deselectRowAtIndexPath:self.tableView.indexPathForSelectedRow animated:YES];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
dispatch_async(dispatch_get_main_queue(), ^{
// This doesn't work unless it's wrapped in this dispatch_async call
[self.searchController.searchBar becomeFirstResponder];
});
}
#pragma mark Delegate stuff
@@ -100,9 +100,10 @@ static BOOL my_os_log_shim_enabled(void *addr) {
self.showsSearchBar = YES;
self.showSearchBarInitially = NO;
weakify(self)
id logHandler = ^(NSArray<FLEXSystemLogMessage *> *newMessages) { strongify(self)
[self handleUpdateWithNewMessages:newMessages];
__weak __typeof(self) weakSelf = self;
id logHandler = ^(NSArray<FLEXSystemLogMessage *> *newMessages) {
__strong __typeof(weakSelf) strongSelf = weakSelf;
[strongSelf handleUpdateWithNewMessages:newMessages];
};
if (FLEXOSLogAvailable() && !FLEXNSLogHookWorks) {
@@ -136,18 +137,20 @@ static BOOL my_os_log_shim_enabled(void *addr) {
[self.logController startMonitoring];
}
- (NSArray<FLEXTableViewSection *> *)makeSections { weakify(self)
- (NSArray<FLEXTableViewSection *> *)makeSections {
__weak __typeof(self) weakSelf = self;
_logMessages = [FLEXMutableListSection list:@[]
cellConfiguration:^(FLEXSystemLogCell *cell, FLEXSystemLogMessage *message, NSInteger row) {
strongify(self)
cell.logMessage = message;
cell.highlightedText = self.filterText;
__strong __typeof(self) strongSelf = weakSelf;
if (strongSelf) {
cell.logMessage = message;
cell.highlightedText = strongSelf.filterText;
if (row % 2 == 0) {
cell.backgroundColor = FLEXColor.primaryBackgroundColor;
} else {
cell.backgroundColor = FLEXColor.secondaryBackgroundColor;
if (row % 2 == 0) {
cell.backgroundColor = FLEXColor.primaryBackgroundColor;
} else {
cell.backgroundColor = FLEXColor.secondaryBackgroundColor;
}
}
} filterMatcher:^BOOL(NSString *filterText, FLEXSystemLogMessage *message) {
NSString *displayedText = [FLEXSystemLogCell displayedTextForLogMessage:message];
@@ -262,26 +265,8 @@ static BOOL my_os_log_shim_enabled(void *addr) {
- (void)tableView:(UITableView *)tableView performAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender {
if (action == @selector(copy:)) {
// We usually only want to copy the log message itself, not any metadata associated with it.
UIPasteboard.generalPasteboard.string = self.logMessages.filteredList[indexPath.row].messageText ?: @"";
UIPasteboard.generalPasteboard.string = self.logMessages.filteredList[indexPath.row].messageText;
}
}
- (UIContextMenuConfiguration *)tableView:(UITableView *)tableView
contextMenuConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath
point:(CGPoint)point __IOS_AVAILABLE(13.0) {
weakify(self)
return [UIContextMenuConfiguration configurationWithIdentifier:nil previewProvider:nil
actionProvider:^UIMenu *(NSArray<UIMenuElement *> *suggestedActions) {
UIAction *copy = [UIAction actionWithTitle:@"Copy"
image:nil
identifier:@"Copy"
handler:^(UIAction *action) { strongify(self)
// We usually only want to copy the log message itself, not any metadata associated with it.
UIPasteboard.generalPasteboard.string = self.logMessages.filteredList[indexPath.row].messageText ?: @"";
}];
return [UIMenu menuWithTitle:@"" image:nil identifier:nil options:UIMenuOptionsDisplayInline children:@[copy]];
}
];
}
@end
+3 -17
View File
@@ -7,7 +7,6 @@
//
#import "FLEXManager.h"
#import "FLEXGlobalsEntry.h"
NS_ASSUME_NONNULL_BEGIN
@@ -15,7 +14,7 @@ NS_ASSUME_NONNULL_BEGIN
#pragma mark - Globals Screen Entries
/// Adds an entry at the top of the list of Global State items.
/// Adds an entry at the bottom of the list of Global State items.
/// Call this method before this view controller is displayed.
/// @param entryName The string to be displayed in the cell.
/// @param objectFutureBlock When you tap on the row, information about the object returned
@@ -27,7 +26,7 @@ NS_ASSUME_NONNULL_BEGIN
/// you may want to use __weak references.
- (void)registerGlobalEntryWithName:(NSString *)entryName objectFutureBlock:(id (^)(void))objectFutureBlock;
/// Adds an entry at the top of the list of Global State items.
/// Adds an entry at the bottom of the list of Global State items.
/// Call this method before this view controller is displayed.
/// @param entryName The string to be displayed in the cell.
/// @param viewControllerFutureBlock When you tap on the row, view controller returned
@@ -35,23 +34,10 @@ NS_ASSUME_NONNULL_BEGIN
/// @note This method must be called from the main thread.
/// The viewControllerFutureBlock will be invoked from the main thread and may not return nil.
/// @note The passed block will be copied and retain for the duration of the application,
/// you may want to use __weak references as needed.
/// you may want to use __weak references.
- (void)registerGlobalEntryWithName:(NSString *)entryName
viewControllerFutureBlock:(UIViewController * (^)(void))viewControllerFutureBlock;
/// Adds an entry at the top of the list of Global State items.
/// @param entryName The string to be displayed in the cell.
/// @param rowSelectedAction When you tap on the row, this block will be invoked
/// with the host table view view controller. Use it to deselect the row or present an alert.
/// @note This method must be called from the main thread.
/// The rowSelectedAction will be invoked from the main thread.
/// @note The passed block will be copied and retain for the duration of the application,
/// you may want to use __weak references as needed.
- (void)registerGlobalEntryWithName:(NSString *)entryName action:(FLEXGlobalsEntryRowAction)rowSelectedAction;
/// Removes all registered global entries.
- (void)clearGlobalEntries;
#pragma mark - Simulator Shortcuts
/// Simulator keyboard shortcuts are enabled by default.
+25 -30
View File
@@ -9,6 +9,7 @@
#import "FLEXManager+Extensibility.h"
#import "FLEXManager+Private.h"
#import "FLEXNavigationController.h"
#import "FLEXGlobalsEntry.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXKeyboardShortcutManager.h"
#import "FLEXExplorerViewController.h"
@@ -57,23 +58,6 @@
[self.userGlobalEntries addObject:entry];
}
- (void)registerGlobalEntryWithName:(NSString *)entryName action:(FLEXGlobalsEntryRowAction)rowSelectedAction {
NSParameterAssert(entryName);
NSParameterAssert(rowSelectedAction);
NSAssert(NSThread.isMainThread, @"This method must be called from the main thread.");
entryName = entryName.copy;
FLEXGlobalsEntry *entry = [FLEXGlobalsEntry entryWithNameFuture:^NSString * _Nonnull{
return entryName;
} action:rowSelectedAction];
[self.userGlobalEntries addObject:entry];
}
- (void)clearGlobalEntries {
[self.userGlobalEntries removeAllObjects];
}
#pragma mark - Simulator Shortcuts
@@ -111,49 +95,60 @@
[self registerDefaultSimulatorShortcutWithKey:@"f" modifiers:0 action:^{
[self toggleExplorer];
} description:@"Toggle FLEX toolbar"];
[self registerDefaultSimulatorShortcutWithKey:@"g" modifiers:0 action:^{
[self showExplorerIfNeeded];
[self.explorerViewController toggleMenuTool];
} description:@"Toggle FLEX globals menu"];
[self registerDefaultSimulatorShortcutWithKey:@"v" modifiers:0 action:^{
[self showExplorerIfNeeded];
[self.explorerViewController toggleViewsTool];
} description:@"Toggle view hierarchy menu"];
[self registerDefaultSimulatorShortcutWithKey:@"s" modifiers:0 action:^{
[self showExplorerIfNeeded];
[self.explorerViewController toggleSelectTool];
} description:@"Toggle select tool"];
[self registerDefaultSimulatorShortcutWithKey:@"m" modifiers:0 action:^{
[self showExplorerIfNeeded];
[self.explorerViewController toggleMoveTool];
} description:@"Toggle move tool"];
[self registerDefaultSimulatorShortcutWithKey:@"n" modifiers:0 action:^{
[self toggleTopViewControllerOfClass:[FLEXNetworkMITMViewController class]];
} description:@"Toggle network history view"];
// 't' is for testing: quickly present an object explorer for debugging
[self registerDefaultSimulatorShortcutWithKey:@"t" modifiers:0 action:^{
[self showExplorerIfNeeded];
[self.explorerViewController toggleToolWithViewControllerProvider:^UINavigationController *{
return [FLEXNavigationController withRootViewController:[FLEXObjectExplorerFactory
explorerViewControllerForObject:NSBundle.mainBundle
]];
} completion:nil];
} description:@"Present an object explorer for debugging"];
[self registerDefaultSimulatorShortcutWithKey:UIKeyInputDownArrow modifiers:0 action:^{
if (self.isHidden || ![self.explorerViewController handleDownArrowKeyPressed]) {
[self tryScrollDown];
}
} description:@"Cycle view selection\n\t\tMove view down\n\t\tScroll down"];
[self registerDefaultSimulatorShortcutWithKey:UIKeyInputUpArrow modifiers:0 action:^{
if (self.isHidden || ![self.explorerViewController handleUpArrowKeyPressed]) {
[self tryScrollUp];
}
} description:@"Cycle view selection\n\t\tMove view up\n\t\tScroll up"];
[self registerDefaultSimulatorShortcutWithKey:UIKeyInputRightArrow modifiers:0 action:^{
if (!self.isHidden) {
[self.explorerViewController handleRightArrowKeyPressed];
}
} description:@"Move selected view right"];
[self registerDefaultSimulatorShortcutWithKey:UIKeyInputLeftArrow modifiers:0 action:^{
if (self.isHidden) {
[self tryGoBack];
@@ -161,15 +156,15 @@
[self.explorerViewController handleLeftArrowKeyPressed];
}
} description:@"Move selected view left"];
[self registerDefaultSimulatorShortcutWithKey:@"?" modifiers:0 action:^{
[self toggleTopViewControllerOfClass:[FLEXKeyboardHelpViewController class]];
} description:@"Toggle (this) help menu"];
[self registerDefaultSimulatorShortcutWithKey:UIKeyInputEscape modifiers:0 action:^{
[[self.topViewController presentingViewController] dismissViewControllerAnimated:YES completion:nil];
} description:@"End editing text\n\t\tDismiss top view controller"];
[self registerDefaultSimulatorShortcutWithKey:@"o" modifiers:UIKeyModifierCommand|UIKeyModifierShift action:^{
[self toggleTopViewControllerOfClass:[FLEXFileBrowserController class]];
} description:@"Toggle file browser menu"];
@@ -188,7 +183,7 @@
if (@available(iOS 11, *)) {
return scrollView.adjustedContentInset;
}
return scrollView.contentInset;
}
+2 -2
View File
@@ -21,10 +21,10 @@ NS_ASSUME_NONNULL_BEGIN
/// The response cache uses an NSCache, so it may purge prior to hitting the limit when the app is under memory pressure.
@property (nonatomic) NSUInteger networkResponseCacheByteLimit;
/// Requests whose host ends with one of the excluded entries in this array will be not be recorded (eg. google.com).
/// Requests whose host ends with one of the blacklisted entries in this array will be not be recorded (eg. google.com).
/// Wildcard or subdomain entries are not required (eg. google.com will match any subdomain under google.com).
/// Useful to remove requests that are typically noisy, such as analytics requests that you aren't interested in tracking.
@property (nonatomic) NSMutableArray<NSString *> *networkRequestHostDenylist;
@property (nonatomic) NSMutableArray<NSString *> *networkRequestHostBlacklist;
/// Sets custom viewer for specific content type.
/// @param contentType Mime type like application/json
+4 -4
View File
@@ -48,12 +48,12 @@
FLEXNetworkRecorder.defaultRecorder.responseCacheByteLimit = networkResponseCacheByteLimit;
}
- (NSMutableArray<NSString *> *)networkRequestHostDenylist {
return FLEXNetworkRecorder.defaultRecorder.hostDenylist;
- (NSMutableArray<NSString *> *)networkRequestHostBlacklist {
return FLEXNetworkRecorder.defaultRecorder.hostBlacklist;
}
- (void)setNetworkRequestHostDenylist:(NSMutableArray<NSString *> *)networkRequestHostDenylist {
FLEXNetworkRecorder.defaultRecorder.hostDenylist = networkRequestHostDenylist;
- (void)setNetworkRequestHostBlacklist:(NSMutableArray<NSString *> *)networkRequestHostBlacklist {
FLEXNetworkRecorder.defaultRecorder.hostBlacklist = networkRequestHostBlacklist;
}
- (void)setCustomViewerForContentType:(NSString *)contentType
+4 -8
View File
@@ -8,6 +8,10 @@
#import "FLEXExplorerToolbar.h"
#if !FLEX_AT_LEAST_IOS13_SDK
@class UIWindowScene;
#endif
NS_ASSUME_NONNULL_BEGIN
@interface FLEXManager : NSObject
@@ -21,14 +25,6 @@ NS_ASSUME_NONNULL_BEGIN
- (void)hideExplorer;
- (void)toggleExplorer;
/// Programmatically dismiss anything presented by FLEX, leaving only the toolbar visible.
- (void)dismissAnyPresentedTools:(void (^_Nullable)(void))completion;
/// Programmatically present something on top of the FLEX toolbar.
/// This method will automatically dismiss any currently presented tool,
/// so you do not need to call \c dismissAnyPresentedTools: yourself.
- (void)presentTool:(UINavigationController *(^)(void))viewControllerFuture
completion:(void (^_Nullable)(void))completion;
/// Use this to present the explorer in a specific scene when the one
/// it chooses by default is not the one you wish to display it in.
- (void)showExplorerFromScene:(UIWindowScene *)scene API_AVAILABLE(ios(13.0));
+7 -20
View File
@@ -49,7 +49,7 @@
NSAssert(NSThread.isMainThread, @"You must use %@ from the main thread only.", NSStringFromClass([self class]));
if (!_explorerWindow) {
_explorerWindow = [[FLEXWindow alloc] initWithFrame:FLEXUtility.appKeyWindow.bounds];
_explorerWindow = [[FLEXWindow alloc] initWithFrame:UIScreen.mainScreen.bounds];
_explorerWindow.eventDelegate = self;
_explorerWindow.rootViewController = self.explorerViewController;
}
@@ -69,12 +69,14 @@
- (void)showExplorer {
UIWindow *flex = self.explorerWindow;
flex.hidden = NO;
#if FLEX_AT_LEAST_IOS13_SDK
if (@available(iOS 13.0, *)) {
// Only look for a new scene if we don't have one
if (!flex.windowScene) {
flex.windowScene = FLEXUtility.appKeyWindow.windowScene;
flex.windowScene = FLEXUtility.activeScene;
}
}
#endif
}
- (void)hideExplorer {
@@ -83,33 +85,18 @@
- (void)toggleExplorer {
if (self.explorerWindow.isHidden) {
if (@available(iOS 13.0, *)) {
[self showExplorerFromScene:FLEXUtility.appKeyWindow.windowScene];
} else {
[self showExplorer];
}
[self showExplorer];
} else {
[self hideExplorer];
}
}
- (void)dismissAnyPresentedTools:(void (^)(void))completion {
if (self.explorerViewController.presentedViewController) {
[self.explorerViewController dismissViewControllerAnimated:YES completion:completion];
} else if (completion) {
completion();
}
}
- (void)presentTool:(UINavigationController * _Nonnull (^)(void))future completion:(void (^)(void))completion {
[self showExplorer];
[self.explorerViewController presentTool:future completion:completion];
}
- (void)showExplorerFromScene:(UIWindowScene *)scene {
#if FLEX_AT_LEAST_IOS13_SDK
if (@available(iOS 13.0, *)) {
self.explorerWindow.windowScene = scene;
}
#endif
self.explorerWindow.hidden = NO;
}
+1 -2
View File
@@ -28,8 +28,7 @@
}
if (request.HTTPBody) {
NSString *body = [[NSString alloc] initWithData:request.HTTPBody encoding:NSUTF8StringEncoding];
[curlCommandString appendFormat:@"-d \'%@\'", body];
[curlCommandString appendFormat:@"-d \'%@\'", [NSString stringWithCString:request.HTTPBody.bytes encoding:NSUTF8StringEncoding]];
}
return curlCommandString;
@@ -390,6 +390,8 @@
}
}
#if FLEX_AT_LEAST_IOS13_SDK
- (UIContextMenuConfiguration *)tableView:(UITableView *)tableView contextMenuConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath point:(CGPoint)point __IOS_AVAILABLE(13.0) {
NSURLRequest *request = [self transactionAtIndexPath:indexPath].request;
return [UIContextMenuConfiguration
@@ -404,27 +406,29 @@
UIPasteboard.generalPasteboard.string = request.URL.absoluteString ?: @"";
}
];
UIAction *denylist = [UIAction
actionWithTitle:[NSString stringWithFormat:@"Exclude '%@'", request.URL.host]
UIAction *blacklist = [UIAction
actionWithTitle:[NSString stringWithFormat:@"Blacklist '%@'", request.URL.host]
image:nil
identifier:nil
handler:^(__kindof UIAction *action) {
NSMutableArray *denylist = FLEXNetworkRecorder.defaultRecorder.hostDenylist;
[denylist addObject:request.URL.host];
[FLEXNetworkRecorder.defaultRecorder clearExcludedTransactions];
[FLEXNetworkRecorder.defaultRecorder synchronizeDenylist];
NSMutableArray *blacklist = FLEXNetworkRecorder.defaultRecorder.hostBlacklist;
[blacklist addObject:request.URL.host];
[FLEXNetworkRecorder.defaultRecorder clearBlacklistedTransactions];
[FLEXNetworkRecorder.defaultRecorder synchronizeBlacklist];
[self tryUpdateTransactions];
}
];
return [UIMenu
menuWithTitle:@"" image:nil identifier:nil
options:UIMenuOptionsDisplayInline
children:@[copy, denylist]
children:@[copy, blacklist]
];
}
];
}
#endif
- (FLEXNetworkTransaction *)transactionAtIndexPath:(NSIndexPath *)indexPath {
return self.searchController.isActive ? self.filteredNetworkTransactions[indexPath.row] : self.networkTransactions[indexPath.row];
}
+5 -5
View File
@@ -28,13 +28,13 @@ extern NSString *const kFLEXNetworkRecorderTransactionsClearedNotification;
/// with an "image", "video", or "audio" prefix.
@property (nonatomic) BOOL shouldCacheMediaResponses;
@property (nonatomic) NSMutableArray<NSString *> *hostDenylist;
@property (nonatomic) NSMutableArray<NSString *> *hostBlacklist;
/// Call this after adding to or setting the \c hostDenylist to remove excluded transactions
- (void)clearExcludedTransactions;
/// Call this after adding to or setting the \c hostBlacklist to remove blacklisted transactions
- (void)clearBlacklistedTransactions;
/// Call this to save the denylist to the disk to be loaded next time
- (void)synchronizeDenylist;
/// Call this to save the blacklist to the disk to be loaded next time
- (void)synchronizeBlacklist;
// Accessing recorded network activity
+7 -7
View File
@@ -45,7 +45,7 @@ NSString *const kFLEXNetworkRecorderResponseCacheLimitDefaultsKey = @"com.flex.r
self.orderedTransactions = [NSMutableArray new];
self.requestIDsToTransactions = [NSMutableDictionary new];
self.hostDenylist = NSUserDefaults.standardUserDefaults.flex_networkHostDenylist.mutableCopy;
self.hostBlacklist = NSUserDefaults.standardUserDefaults.flex_networkHostBlacklist.mutableCopy;
// Serial queue used because we use mutable objects that are not thread safe
self.queue = dispatch_queue_create("com.flex.FLEXNetworkRecorder", DISPATCH_QUEUE_SERIAL);
@@ -100,13 +100,13 @@ NSString *const kFLEXNetworkRecorderResponseCacheLimitDefaultsKey = @"com.flex.r
});
}
- (void)clearExcludedTransactions {
- (void)clearBlacklistedTransactions {
dispatch_sync(self.queue, ^{
self.orderedTransactions = ({
[self.orderedTransactions flex_filtered:^BOOL(FLEXNetworkTransaction *ta, NSUInteger idx) {
NSString *host = ta.request.URL.host;
for (NSString *excluded in self.hostDenylist) {
if ([host hasSuffix:excluded]) {
for (NSString *blacklisted in self.hostBlacklist) {
if ([host hasSuffix:blacklisted]) {
return NO;
}
}
@@ -117,8 +117,8 @@ NSString *const kFLEXNetworkRecorderResponseCacheLimitDefaultsKey = @"com.flex.r
});
}
- (void)synchronizeDenylist {
NSUserDefaults.standardUserDefaults.flex_networkHostDenylist = self.hostDenylist;
- (void)synchronizeBlacklist {
NSUserDefaults.standardUserDefaults.flex_networkHostBlacklist = self.hostBlacklist;
}
#pragma mark - Network Events
@@ -126,7 +126,7 @@ NSString *const kFLEXNetworkRecorderResponseCacheLimitDefaultsKey = @"com.flex.r
- (void)recordRequestWillBeSentWithRequestID:(NSString *)requestID
request:(NSURLRequest *)request
redirectResponse:(NSURLResponse *)redirectResponse {
for (NSString *host in self.hostDenylist) {
for (NSString *host in self.hostBlacklist) {
if ([request.URL.host hasSuffix:host]) {
return;
}
+17 -17
View File
@@ -23,7 +23,7 @@
@property (nonatomic, readonly) UISlider *cacheLimitSlider;
@property (nonatomic) UILabel *cacheLimitLabel;
@property (nonatomic) NSMutableArray<NSString *> *hostDenylist;
@property (nonatomic) NSMutableArray<NSString *> *hostBlacklist;
@end
@implementation FLEXNetworkSettingsController
@@ -32,7 +32,7 @@
[super viewDidLoad];
[self disableToolbar];
self.hostDenylist = FLEXNetworkRecorder.defaultRecorder.hostDenylist.mutableCopy;
self.hostBlacklist = FLEXNetworkRecorder.defaultRecorder.hostBlacklist.mutableCopy;
NSUserDefaults *defaults = NSUserDefaults.standardUserDefaults;
@@ -107,13 +107,13 @@
#pragma mark - Table View Data Source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return self.hostDenylist.count ? 2 : 1;
return self.hostBlacklist.count ? 2 : 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
switch (section) {
case 0: return 5;
case 1: return self.hostDenylist.count;
case 1: return self.hostBlacklist.count;
default: return 0;
}
}
@@ -121,7 +121,7 @@
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
switch (section) {
case 0: return @"General";
case 1: return @"Host Denylist";
case 1: return @"Host Blacklist";
default: return nil;
}
}
@@ -162,7 +162,7 @@
cell.accessoryView = self.jsonViewerSwitch;
break;
case 3:
cell.textLabel.text = @"Reset Host Denylist";
cell.textLabel.text = @"Reset Host Blacklist";
cell.textLabel.textColor = tableView.tintColor;
break;
case 4:
@@ -195,9 +195,9 @@
break;
}
// Denylist entries
// Blacklist entries
case 1: {
cell.textLabel.text = self.hostDenylist[indexPath.row];
cell.textLabel.text = self.hostBlacklist[indexPath.row];
break;
}
@@ -212,7 +212,7 @@
#pragma mark - Table View Delegate
- (BOOL)tableView:(UITableView *)tableView shouldHighlightRowAtIndexPath:(NSIndexPath *)ip {
// Can only select the "Reset Host Denylist" row
// Can only select the "Reset Host Blacklist" row
return ip.section == 0 && ip.row == 2;
}
@@ -220,12 +220,12 @@
[tableView deselectRowAtIndexPath:indexPath animated:YES];
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(@"Reset Host Denylist");
make.title(@"Reset Host Blacklist");
make.message(@"You cannot undo this action. Are you sure?");
make.button(@"Reset").destructiveStyle().handler(^(NSArray<NSString *> *strings) {
self.hostDenylist = nil;
[FLEXNetworkRecorder.defaultRecorder.hostDenylist removeAllObjects];
[FLEXNetworkRecorder.defaultRecorder synchronizeDenylist];
self.hostBlacklist = nil;
[FLEXNetworkRecorder.defaultRecorder.hostBlacklist removeAllObjects];
[FLEXNetworkRecorder.defaultRecorder synchronizeBlacklist];
[self.tableView deleteSections:
[NSIndexSet indexSetWithIndex:1]
withRowAnimation:UITableViewRowAnimationAutomatic];
@@ -242,10 +242,10 @@
forRowAtIndexPath:(NSIndexPath *)indexPath {
NSParameterAssert(style == UITableViewCellEditingStyleDelete);
NSString *host = self.hostDenylist[indexPath.row];
[self.hostDenylist removeObjectAtIndex:indexPath.row];
[FLEXNetworkRecorder.defaultRecorder.hostDenylist removeObject:host];
[FLEXNetworkRecorder.defaultRecorder synchronizeDenylist];
NSString *host = self.hostBlacklist[indexPath.row];
[self.hostBlacklist removeObjectAtIndex:indexPath.row];
[FLEXNetworkRecorder.defaultRecorder.hostBlacklist removeObject:host];
[FLEXNetworkRecorder.defaultRecorder synchronizeBlacklist];
[tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
}
@@ -211,6 +211,8 @@ typedef UIViewController *(^FLEXNetworkDetailRowSelectionFuture)(void);
}
}
#if FLEX_AT_LEAST_IOS13_SDK
- (UIContextMenuConfiguration *)tableView:(UITableView *)tableView contextMenuConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath point:(CGPoint)point __IOS_AVAILABLE(13.0) {
return [UIContextMenuConfiguration
configurationWithIdentifier:nil
@@ -234,6 +236,8 @@ typedef UIViewController *(^FLEXNetworkDetailRowSelectionFuture)(void);
];
}
#endif
#pragma mark - View Configuration
+ (NSAttributedString *)attributedTextForRow:(FLEXNetworkDetailRow *)row {
@@ -324,23 +328,24 @@ typedef UIViewController *(^FLEXNetworkDetailRowSelectionFuture)(void);
responseBodyRow.detailText = @"tap to view";
// Avoid a long lived strong reference to the response data in case we need to purge it from the cache.
weakify(responseData)
responseBodyRow.selectionFuture = ^UIViewController *() { strongify(responseData)
__weak NSData *weakResponseData = responseData;
responseBodyRow.selectionFuture = ^UIViewController * () {
// Show the response if we can
NSString *contentType = transaction.response.MIMEType;
if (responseData) {
UIViewController *bodyDetails = [self detailViewControllerForMIMEType:contentType data:responseData];
if (bodyDetails) {
bodyDetails.title = @"Response";
return bodyDetails;
NSData *strongResponseData = weakResponseData;
if (strongResponseData) {
UIViewController *bodyDetailController = [self detailViewControllerForMIMEType:contentType data:strongResponseData];
if (bodyDetailController) {
bodyDetailController.title = @"Response";
return bodyDetailController;
}
}
// We can't show the response, alert user
return [FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(@"Unable to View Response");
if (responseData) {
if (strongResponseData) {
make.message(@"No viewer content type: ").message(contentType);
} else {
make.message(@"The response has been purged from the cache");
@@ -421,8 +426,7 @@ typedef UIViewController *(^FLEXNetworkDetailRowSelectionFuture)(void);
if (transaction.cachedRequestBody.length > 0) {
NSString *contentType = [transaction.request valueForHTTPHeaderField:@"Content-Type"];
if ([contentType hasPrefix:@"application/x-www-form-urlencoded"]) {
NSData *body = [self postBodyDataForTransaction:transaction];
NSString *bodyString = [[NSString alloc] initWithData:body encoding:NSUTF8StringEncoding];
NSString *bodyString = [NSString stringWithCString:[self postBodyDataForTransaction:transaction].bytes encoding:NSUTF8StringEncoding];
postBodySection.rows = [self networkDetailRowsFromQueryItems:[FLEXUtility itemsFromQueryString:bodyString]];
}
}
+11 -41
View File
@@ -136,13 +136,7 @@
NSUserDefaults *defaults = NSUserDefaults.standardUserDefaults;
BOOL hideBackingIvars = defaults.flex_explorerHidesPropertyIvars;
BOOL hidePropertyMethods = defaults.flex_explorerHidesPropertyMethods;
BOOL hidePrivateMethods = defaults.flex_explorerHidesPrivateMethods;
BOOL showMethodOverrides = defaults.flex_explorerShowsMethodOverrides;
NSMutableArray<NSArray<FLEXProperty *> *> *allProperties = [NSMutableArray new];
NSMutableArray<NSArray<FLEXProperty *> *> *allClassProps = [NSMutableArray new];
NSMutableArray<NSArray<FLEXMethod *> *> *allMethods = [NSMutableArray new];
NSMutableArray<NSArray<FLEXMethod *> *> *allClassMethods = [NSMutableArray new];
// Loop over each class and each superclass, collect
// the fresh and unique metadata in each category
@@ -153,13 +147,13 @@
Class cls = self.classHierarchyClasses[i];
superclass = (i < rootIdx) ? self.classHierarchyClasses[i+1] : nil;
[allProperties addObject:[self
[_allProperties addObject:[self
metadataUniquedByName:[cls flex_allInstanceProperties]
superclass:superclass
kind:FLEXMetadataKindProperties
skip:showMethodOverrides
]];
[allClassProps addObject:[self
[_allClassProperties addObject:[self
metadataUniquedByName:[cls flex_allClassProperties]
superclass:superclass
kind:FLEXMetadataKindClassProperties
@@ -171,13 +165,13 @@
kind:FLEXMetadataKindIvars
skip:NO
]];
[allMethods addObject:[self
[_allMethods addObject:[self
metadataUniquedByName:[cls flex_allInstanceMethods]
superclass:superclass
kind:FLEXMetadataKindMethods
skip:showMethodOverrides
]];
[allClassMethods addObject:[self
[_allClassMethods addObject:[self
metadataUniquedByName:[cls flex_allClassMethods]
superclass:superclass
kind:FLEXMetadataKindClassMethods
@@ -204,7 +198,7 @@
_classHierarchy = [FLEXStaticMetadata classHierarchy:self.classHierarchyClasses];
NSArray<NSArray<FLEXProperty *> *> *properties = allProperties;
NSArray<NSArray<FLEXProperty *> *> *properties = _allProperties;
// Potentially filter property-backing ivars
if (hideBackingIvars) {
@@ -214,7 +208,7 @@
NSSet *ivarNames = [NSSet setWithArray:({
[properties[idx] flex_mapped:^id(FLEXProperty *p, NSUInteger idx) {
// Nil if no ivar, and array is flatted
return p.likelyIvarName;
return p.attributes.backingIvar;
}];
})];
@@ -227,7 +221,8 @@
// Potentially filter property-backing methods
if (hidePropertyMethods) {
allMethods = [allMethods flex_mapped:^id(NSArray<FLEXMethod *> *list, NSUInteger idx) {
NSArray<NSArray<FLEXMethod *> *> *methods = _allMethods.copy;
_allMethods = [methods flex_mapped:^id(NSArray<FLEXMethod *> *list, NSUInteger idx) {
// Get a set of all property method names for the current class in the hierarchy
NSSet *methodNames = [NSSet setWithArray:({
[properties[idx] flex_flatmapped:^NSArray *(FLEXProperty *p, NSUInteger idx) {
@@ -245,37 +240,12 @@
}];
})];
// Remove methods whose name is in the property method names list
// Remove ivars whose name is in the ivar names list
return [list flex_filtered:^BOOL(FLEXMethod *method, NSUInteger idx) {
return ![methodNames containsObject:method.selectorString];
}];
}];
}
if (hidePrivateMethods) {
id methodMapBlock = ^id(NSArray<FLEXMethod *> *list, NSUInteger idx) {
// Remove methods which contain an underscore
return [list flex_filtered:^BOOL(FLEXMethod *method, NSUInteger idx) {
return ![method.selectorString containsString:@"_"];
}];
};
id propertyMapBlock = ^id(NSArray<FLEXProperty *> *list, NSUInteger idx) {
// Remove methods which contain an underscore
return [list flex_filtered:^BOOL(FLEXProperty *prop, NSUInteger idx) {
return ![prop.name containsString:@"_"];
}];
};
allMethods = [allMethods flex_mapped:methodMapBlock];
allClassMethods = [allClassMethods flex_mapped:methodMapBlock];
allProperties = [allProperties flex_mapped:propertyMapBlock];
allClassProps = [allClassProps flex_mapped:propertyMapBlock];
}
_allProperties = allProperties;
_allClassProperties = allClassProps;
_allMethods = allMethods;
_allClassMethods = allClassMethods;
// Set up UIKit helper data
// Really, we only need to call this on properties and ivars
@@ -313,8 +283,8 @@
- (NSArray *)metadataUniquedByName:(NSArray *)list
superclass:(Class)superclass
kind:(FLEXMetadataKind)kind
skip:(BOOL)skipUniquing {
if (skipUniquing) {
skip:(BOOL)skip {
if (skip) {
return list;
}
@@ -17,27 +17,17 @@
#import "FLEXColorPreviewSection.h"
#import "FLEXDefaultsContentSection.h"
#import "FLEXBundleShortcuts.h"
#import "FLEXNSStringShortcuts.h"
#import "FLEXNSDataShortcuts.h"
#import "FLEXBlockShortcuts.h"
#import "FLEXUtility.h"
@implementation FLEXObjectExplorerFactory
static NSMutableDictionary<id<NSCopying>, Class> *classesToRegisteredSections = nil;
static NSMutableDictionary<Class, Class> *classesToRegisteredSections = nil;
+ (void)initialize {
if (self == [FLEXObjectExplorerFactory class]) {
// DO NOT USE STRING KEYS HERE
// We NEED to use the class as a key, because we CANNOT
// differentiate a class's name from the metaclass's name.
// These mappings are per-class-object, not per-class-name.
//
// For example, if we used class names, this would result in
// the object explorer trying to render a color preview for
// the UIColor class object, which is not a color itself.
#define ClassKey(name) (id<NSCopying>)[name class]
#define ClassKeyByName(str) (id<NSCopying>)NSClassFromString(@ #str)
#define MetaclassKey(meta) (id<NSCopying>)object_getClass([meta class])
#define ClassKey(name) (Class<NSCopying>)[name class]
#define ClassKeyByName(str) (Class<NSCopying>)NSClassFromString(@ #str)
#define MetaclassKey(meta) (Class<NSCopying>)object_getClass([meta class])
classesToRegisteredSections = [NSMutableDictionary dictionaryWithDictionary:@{
MetaclassKey(NSObject) : [FLEXClassShortcuts class],
ClassKey(NSArray) : [FLEXCollectionContentSection class],
@@ -52,8 +42,6 @@ static NSMutableDictionary<id<NSCopying>, Class> *classesToRegisteredSections =
ClassKey(CALayer) : [FLEXLayerShortcuts class],
ClassKey(UIColor) : [FLEXColorPreviewSection class],
ClassKey(NSBundle) : [FLEXBundleShortcuts class],
ClassKey(NSString) : [FLEXNSStringShortcuts class],
ClassKey(NSData) : [FLEXNSDataShortcuts class],
ClassKeyByName(NSBlock) : [FLEXBlockShortcuts class],
}];
#undef ClassKey
@@ -76,38 +64,24 @@ static NSMutableDictionary<id<NSCopying>, Class> *classesToRegisteredSections =
// shortcut section for NSObject.
//
// TODO: rename it to FLEXNSObjectShortcuts or something?
FLEXShortcutsSection *shortcutsSection = [FLEXShortcutsSection forObject:object];
NSArray *sections = @[shortcutsSection];
Class customSectionClass = nil;
Class sectionClass = nil;
Class cls = object_getClass(object);
do {
customSectionClass = classesToRegisteredSections[(id<NSCopying>)cls];
} while (!customSectionClass && (cls = [cls superclass]));
sectionClass = classesToRegisteredSections[(Class<NSCopying>)cls];
} while (!sectionClass && (cls = [cls superclass]));
if (customSectionClass) {
id customSection = [customSectionClass forObject:object];
BOOL isFLEXShortcutSection = [customSection respondsToSelector:@selector(isNewSection)];
// If the section "replaces" the default shortcuts section,
// only return that section. Otherwise, return both this
// section and the default shortcuts section.
if (isFLEXShortcutSection && ![customSection isNewSection]) {
sections = @[customSection];
} else {
// Custom section will go before shortcuts
sections = @[customSection, shortcutsSection];
}
if (!sectionClass) {
sectionClass = [FLEXShortcutsSection class];
}
return [FLEXObjectExplorerViewController
exploringObject:object
customSections:sections
customSection:[sectionClass forObject:object]
];
}
+ (void)registerExplorerSection:(Class)explorerClass forClass:(Class)objectClass {
classesToRegisteredSections[(id<NSCopying>)objectClass] = explorerClass;
classesToRegisteredSections[(Class<NSCopying>)objectClass] = explorerClass;
}
#pragma mark - FLEXGlobalsEntry
@@ -202,7 +176,7 @@ static NSMutableDictionary<id<NSCopying>, Class> *classesToRegisteredSections =
return [self explorerViewControllerForObject:NSThread.mainThread];
case FLEXGlobalsRowOperationQueue:
return [self explorerViewControllerForObject:NSOperationQueue.mainQueue];
case FLEXGlobalsRowKeyWindow:
return [FLEXObjectExplorerFactory
explorerViewControllerForObject:FLEXUtility.appKeyWindow
@@ -29,9 +29,6 @@ NS_ASSUME_NONNULL_BEGIN
+ (instancetype)exploringObject:(id)objectOrClass;
/// No custom section unless you provide one.
+ (instancetype)exploringObject:(id)objectOrClass customSection:(nullable FLEXTableViewSection *)customSection;
/// No custom sections unless you provide some.
+ (instancetype)exploringObject:(id)objectOrClass
customSections:(nullable NSArray<FLEXTableViewSection *> *)customSections;
/// The object being explored, which may be an instance of a class or a class itself.
@property (nonatomic, readonly) id object;
@@ -30,7 +30,7 @@
#pragma mark - Private properties
@interface FLEXObjectExplorerViewController () <UIGestureRecognizerDelegate>
@property (nonatomic, readonly) FLEXSingleRowSection *descriptionSection;
@property (nonatomic, readonly) NSArray<FLEXTableViewSection *> *customSections;
@property (nonatomic, readonly) FLEXTableViewSection *customSection;
@property (nonatomic) NSIndexSet *customSectionVisibleIndexes;
@property (nonatomic, readonly) NSArray<NSString *> *observedNotifications;
@@ -46,27 +46,23 @@
}
+ (instancetype)exploringObject:(id)target customSection:(FLEXTableViewSection *)section {
return [self exploringObject:target customSections:@[section]];
}
+ (instancetype)exploringObject:(id)target customSections:(NSArray *)customSections {
return [[self alloc]
initWithObject:target
explorer:[FLEXObjectExplorer forObject:target]
customSections:customSections
customSection:section
];
}
- (id)initWithObject:(id)target
explorer:(__kindof FLEXObjectExplorer *)explorer
customSections:(NSArray<FLEXTableViewSection *> *)customSections {
customSection:(FLEXTableViewSection *)customSection {
NSParameterAssert(target);
self = [super initWithStyle:UITableViewStyleGrouped];
if (self) {
_object = target;
_explorer = explorer;
_customSections = customSections;
_customSection = customSection;
}
return self;
@@ -76,8 +72,7 @@
return @[
kFLEXDefaultsHidePropertyIvarsKey,
kFLEXDefaultsHidePropertyMethodsKey,
kFLEXDefaultsHidePrivateMethodsKey,
kFLEXDefaultsShowMethodOverridesKey,
kFLEXDefaultsHideMethodOverridesKey,
kFLEXDefaultsHideVariablePreviewsKey,
];
}
@@ -92,7 +87,7 @@
// Use [object class] here rather than object_getClass
// to avoid the KVO prefix for observed objects
self.title = [FLEXRuntimeUtility safeClassNameForObject:self.object];
self.title = [[self.object class] description];
// Search
self.showsSearchBar = YES;
@@ -199,10 +194,8 @@
referencesSection
]];
if (self.customSections) {
[sections insertObjects:self.customSections atIndexes:[NSIndexSet
indexSetWithIndexesInRange:NSMakeRange(0, self.customSections.count)
]];
if (self.customSection) {
[sections insertObject:self.customSection atIndex:0];
}
if (self.descriptionSection) {
[sections insertObject:self.descriptionSection atIndex:0];
@@ -272,11 +265,9 @@
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)g1 shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)g2 {
// Prioritize important pan gestures over our swipe gesture
if ([g2 isKindOfClass:[UIPanGestureRecognizer class]]) {
if (g2 == self.navigationController.interactivePopGestureRecognizer) {
return NO;
}
if (g2 == self.tableView.panGestureRecognizer) {
if (g2 == self.navigationController.interactivePopGestureRecognizer ||
g2 == self.navigationController.barHideOnSwipeGestureRecognizer ||
g2 == self.tableView.panGestureRecognizer) {
return NO;
}
}
@@ -300,8 +291,7 @@
NSDictionary<NSString *, NSString *> *explorerToggles = @{
kFLEXDefaultsHidePropertyIvarsKey: @"Property-Backing Ivars",
kFLEXDefaultsHidePropertyMethodsKey: @"Property-Backing Methods",
kFLEXDefaultsHidePrivateMethodsKey: @"Likely Private Methods",
kFLEXDefaultsShowMethodOverridesKey: @"Method Overrides",
kFLEXDefaultsHideMethodOverridesKey: @"Method Overrides",
kFLEXDefaultsHideVariablePreviewsKey: @"Variable Previews"
};
@@ -312,8 +302,7 @@
NSDictionary<NSString *, NSDictionary *> *nextStateDescriptions = @{
kFLEXDefaultsHidePropertyIvarsKey: @{ @NO: @"Hide ", @YES: @"Show " },
kFLEXDefaultsHidePropertyMethodsKey: @{ @NO: @"Hide ", @YES: @"Show " },
kFLEXDefaultsHidePrivateMethodsKey: @{ @NO: @"Hide ", @YES: @"Show " },
kFLEXDefaultsShowMethodOverridesKey: @{ @NO: @"Show ", @YES: @"Hide " },
kFLEXDefaultsHideMethodOverridesKey: @{ @NO: @"Show ", @YES: @"Hide " },
kFLEXDefaultsHideVariablePreviewsKey: @{ @NO: @"Hide ", @YES: @"Show " },
};
@@ -13,7 +13,7 @@
@interface FLEXDefaultsContentSection ()
@property (nonatomic) NSUserDefaults *defaults;
@property (nonatomic) NSArray *keys;
@property (nonatomic, readonly) NSDictionary *unexcludedDefaults;
@property (nonatomic, readonly) NSDictionary *whitelistedDefaults;
@end
@implementation FLEXDefaultsContentSection
@@ -33,7 +33,7 @@
FLEXDefaultsContentSection *section = [self forReusableFuture:^id(FLEXDefaultsContentSection *section) {
section.defaults = userDefaults;
section.onlyShowKeysForAppPrefs = YES;
return section.unexcludedDefaults;
return section.whitelistedDefaults;
}];
return section;
}
@@ -87,16 +87,16 @@
_keys = [keys sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)];
}
- (NSDictionary *)unexcludedDefaults {
// Case: no excluding
- (NSDictionary *)whitelistedDefaults {
// Case: no whitelisting
if (!self.onlyShowKeysForAppPrefs) {
return self.defaults.dictionaryRepresentation;
}
// Always regenerate key allowlist when this method is called
// Always regenerate key whitelist when this method is called
_keys = nil;
// Generate new dictionary from unexcluded keys
// Generate new dictionary from whitelisted keys
NSArray *values = [self.defaults.dictionaryRepresentation
objectsForKeys:self.keys notFoundMarker:NSNull.null
];
@@ -189,6 +189,8 @@
cell.accessoryType = [self accessoryTypeForRow:row];
}
#if FLEX_AT_LEAST_IOS13_SDK
- (NSString *)menuSubtitleForRow:(NSInteger)row {
return [self.metadata[row] contextualSubtitleWithTarget:self.explorer.object];
}
@@ -230,4 +232,6 @@
return [self.metadata[row] copiableMetadataWithTarget:self.explorer.object];
}
#endif
@end
@@ -7,7 +7,6 @@
//
#import "FLEXMutableListSection.h"
#import "FLEXMacros.h"
@interface FLEXMutableListSection ()
@property (nonatomic, readonly) FLEXMutableListCellForElement configureCell;
@@ -79,10 +78,12 @@ configurationBlock:(FLEXMutableListCellForElement)cellConfig
}
- (void (^)(__kindof UIViewController *))didSelectRowAction:(NSInteger)row {
if (self.selectionHandler) { weakify(self)
return ^(UIViewController *host) { strongify(self)
if (self) {
self.selectionHandler(host, self.filteredList[row]);
if (self.selectionHandler) {
__weak __typeof(self) weakSelf = self;
return ^(UIViewController *host) {
__strong __typeof(self) strongSelf = weakSelf;
if (strongSelf) {
strongSelf.selectionHandler(host, strongSelf.filteredList[row]);
}
};
}
@@ -9,7 +9,6 @@
#import "FLEXBundleShortcuts.h"
#import "FLEXShortcut.h"
#import "FLEXAlert.h"
#import "FLEXMacros.h"
#import "FLEXRuntimeExporter.h"
#import "FLEXTableListViewController.h"
#import "FLEXFileBrowserController.h"
@@ -18,7 +17,8 @@
@implementation FLEXBundleShortcuts
#pragma mark Overrides
+ (instancetype)forObject:(NSBundle *)bundle { weakify(self)
+ (instancetype)forObject:(NSBundle *)bundle {
__weak __typeof(self) weakSelf = self;
return [self forObject:bundle additionalRows:@[
[FLEXActionShortcut
title:@"Browse Bundle Directory" subtitle:nil
@@ -30,8 +30,11 @@
}
],
[FLEXActionShortcut title:@"Browse Bundle as Database…" subtitle:nil
selectionHandler:^(UIViewController *host, NSBundle *bundle) { strongify(self)
[self promptToExportBundleAsDatabase:bundle host:host];
selectionHandler:^(UIViewController *host, NSBundle *bundle) {
__strong __typeof(self) strongSelf = weakSelf;
if (strongSelf) {
[strongSelf promptToExportBundleAsDatabase:bundle host:host];
}
}
accessoryType:^UITableViewCellAccessoryType(NSBundle *bundle) {
return UITableViewCellAccessoryDisclosureIndicator;
@@ -10,7 +10,6 @@
#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;
@@ -15,11 +15,11 @@
+ (instancetype)forObject:(CALayer *)layer {
return [self forObject:layer additionalRows:@[
[FLEXActionShortcut title:@"Preview Image" subtitle:nil
viewer:^UIViewController *(CALayer *layer) {
viewer:^UIViewController *(id layer) {
return [FLEXImagePreviewViewController previewForLayer:layer];
}
accessoryType:^UITableViewCellAccessoryType(CALayer *layer) {
return CGRectIsEmpty(layer.bounds) ? UITableViewCellAccessoryNone : UITableViewCellAccessoryDisclosureIndicator;
accessoryType:^UITableViewCellAccessoryType(id layer) {
return UITableViewCellAccessoryDisclosureIndicator;
}
]
]];
@@ -1,13 +0,0 @@
//
// FLEXNSDataShortcuts.h
// FLEX
//
// Created by Tanner on 3/29/21.
//
#import "FLEXShortcutsSection.h"
/// Adds a "UTF-8 String" shortcut
@interface FLEXNSDataShortcuts : FLEXShortcutsSection
@end
@@ -1,48 +0,0 @@
//
// 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
@@ -1,13 +0,0 @@
//
// FLEXNSStringShortcuts.h
// FLEX
//
// Created by Tanner on 3/29/21.
//
#import "FLEXShortcutsSection.h"
/// Adds a "UTF-8 Data" shortcut
@interface FLEXNSStringShortcuts : FLEXShortcutsSection
@end
@@ -1,29 +0,0 @@
//
// 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
@@ -32,7 +32,7 @@ NS_ASSUME_NONNULL_BEGIN
@optional
/// Called when the (i) button is pressed if the accessory type includes it
- (UIViewController *)editorWith:(id)object forSection:(FLEXTableViewSection *)section;
- (UIViewController *)editorWith:(id)object;
@end
@@ -25,7 +25,3 @@
@interface FLEXShortcutsFactory (Blocks) @end
@interface FLEXShortcutsFactory (Foundation) @end
@interface FLEXShortcutsFactory (WebKit_Safari) @end
@interface FLEXShortcutsFactory (Pasteboard) @end
@@ -8,12 +8,8 @@
#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
@@ -63,7 +59,6 @@
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));
@@ -146,26 +141,7 @@
@"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);
]).methods(@[@"view"]).forClass(UIViewController.class);
}
@end
@@ -280,164 +256,12 @@
FLEXRuntimeUtilityTryAddObjectProperty(2, abbreviationDictionary, NSTimeZone.flex_metaclass, NSDictionary);
self.append.classMethods(@[
@"timeZoneWithName:", @"timeZoneWithAbbreviation:", @"timeZoneForSecondsFromGMT:",
@"timeZoneWithName:", @"timeZoneWithAbbreviation:", @"timeZoneForSecondsFromGMT:", @"", @"", @"",
]).forClass(NSTimeZone.flex_metaclass);
self.append.classProperties(@[
@"defaultTimeZone", @"systemTimeZone", @"localTimeZone",
@"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
@@ -10,8 +10,6 @@
#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".
///
@@ -27,11 +25,11 @@ NS_ASSUME_NONNULL_BEGIN
@interface FLEXShortcutsSection : FLEXTableViewSection <FLEXObjectInfoSection>
/// Uses \c kFLEXDefaultCell
+ (instancetype)forObject:(id)objectOrClass rowTitles:(nullable NSArray<NSString *> *)titles;
+ (instancetype)forObject:(id)objectOrClass rowTitles:(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;
rowTitles:(NSArray<NSString *> *)titles
rowSubtitles:(NSArray<NSString *> *)subtitles;
/// Uses \c kFLEXDefaultCell for rows that are given a title, otherwise
/// this uses \c kFLEXDetailCell for any other allowed object.
@@ -46,15 +44,16 @@ NS_ASSUME_NONNULL_BEGIN
/// - 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;
/// @return \c nil if no rows are provided
+ (instancetype)forObject:(id)objectOrClass rows:(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;
+ (instancetype)forObject:(id)objectOrClass additionalRows:(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.
/// @return \c nil if the object has no shortcuts registered at all
+ (instancetype)forObject:(id)objectOrClass;
/// Subclasses \e may override this to hide the disclosure indicator
@@ -73,16 +72,10 @@ NS_ASSUME_NONNULL_BEGIN
/// 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 FLEXShortcutsFactory *(^FLEXShortcutsFactoryNames)(NSArray *names);
typedef void (^FLEXShortcutsFactoryTarget)(Class targetClass);
/// The block properties below are to be used like SnapKit or Masonry.
@@ -130,5 +123,3 @@ typedef void (^FLEXShortcutsFactoryTarget)(Class targetClass);
@property (nonatomic, readonly) FLEXShortcutsFactoryTarget forClass;
@end
NS_ASSUME_NONNULL_END
@@ -33,7 +33,6 @@
@end
@implementation FLEXShortcutsSection
@synthesize isNewSection = _isNewSection;
#pragma mark Initialization
@@ -48,13 +47,13 @@
}
+ (instancetype)forObject:(id)objectOrClass rows:(NSArray *)rows {
return [[self alloc] initWithObject:objectOrClass rows:rows isNewSection:YES];
return [[self alloc] initWithObject:objectOrClass rows:rows];
}
+ (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];
return [self forObject:objectOrClass rows:allRows];
}
+ (instancetype)forObject:(id)objectOrClass {
@@ -73,18 +72,16 @@
_object = object;
_allTitles = titles.copy;
_allSubtitles = subtitles.copy;
_isNewSection = YES;
_numberOfLines = 1;
}
return self;
}
- (id)initWithObject:object rows:(NSArray *)rows isNewSection:(BOOL)newSection {
- (id)initWithObject:object rows:(NSArray *)rows {
self = [super init];
if (self) {
_object = object;
_isNewSection = newSection;
_allShortcuts = [rows flex_mapped:^id(id obj, NSUInteger idx) {
return [FLEXShortcut shortcutFor:obj];
@@ -204,10 +201,10 @@
- (void (^)(__kindof UIViewController *))didPressInfoButtonAction:(NSInteger)row {
id<FLEXShortcut> shortcut = self.shortcuts[row];
if ([shortcut respondsToSelector:@selector(editorWith:forSection:)]) {
if ([shortcut respondsToSelector:@selector(editorWith:)]) {
id object = self.object;
return ^(UIViewController *host) {
UIViewController *editor = [shortcut editorWith:object forSection:self];
UIViewController *editor = [shortcut editorWith:object];
[host.navigationController pushViewController:editor animated:YES];
};
}
@@ -258,53 +255,31 @@
}
@end
#define NewAndSet(ivar) ({ FLEXShortcutsFactory *r = [self sharedFactory]; r->ivar = YES; r; })
#define NewAndSet(ivar) ({ FLEXShortcutsFactory *r = [self new]; r->ivar = YES; r; })
#define SetIvar(ivar) ({ self->ivar = YES; self; })
#define SetParamBlock(ivar) ^(NSArray *p) { self->ivar = p; return self; }
@implementation FLEXShortcutsFactory
typedef NSMutableDictionary<Class, NSMutableArray<id<FLEXRuntimeMetadata>> *> RegistrationBuckets;
// Class buckets
static RegistrationBuckets *cProperties = nil;
static RegistrationBuckets *cIvars = nil;
static RegistrationBuckets *cMethods = nil;
// Metaclass buckets
static RegistrationBuckets *mProperties = nil;
static RegistrationBuckets *mMethods = nil;
@implementation FLEXShortcutsFactory {
// Class buckets
RegistrationBuckets *cProperties;
RegistrationBuckets *cIvars;
RegistrationBuckets *cMethods;
// Metaclass buckets
RegistrationBuckets *mProperties;
RegistrationBuckets *mMethods;
}
+ (void)load {
cProperties = [NSMutableDictionary new];
cIvars = [NSMutableDictionary new];
cMethods = [NSMutableDictionary new];
+ (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;
mProperties = [NSMutableDictionary new];
mMethods = [NSMutableDictionary new];
}
+ (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
@@ -350,43 +325,30 @@ typedef NSMutableDictionary<Class, NSMutableArray<id<FLEXRuntimeMetadata>> *> Re
}
- (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;
});
// 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];
}
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);
}
@@ -443,15 +405,9 @@ typedef NSMutableDictionary<Class, NSMutableArray<id<FLEXRuntimeMetadata>> *> Re
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) {
@@ -474,8 +430,6 @@ typedef NSMutableDictionary<Class, NSMutableArray<id<FLEXRuntimeMetadata>> *> Re
}];
[self _register:items to:ivarBucket class:cls];
}
[self reset];
};
}
@@ -70,18 +70,15 @@
return [FLEXObjectExplorerFactory explorerViewControllerForObject:controller];
}
accessoryType:^UITableViewCellAccessoryType(id view) {
return controller ? UITableViewCellAccessoryDisclosureIndicator : UITableViewCellAccessoryNone;
return controller ? UITableViewCellAccessoryDisclosureIndicator : 0;
}
],
[FLEXActionShortcut title:@"Preview Image" subtitle:^NSString *(UIView *view) {
return !CGRectIsEmpty(view.bounds) ? @"" : @"Unavailable with empty bounds";
}
viewer:^UIViewController *(UIView *view) {
[FLEXActionShortcut title:@"Preview Image" subtitle:nil
viewer:^UIViewController *(id view) {
return [FLEXImagePreviewViewController previewForView:view];
}
accessoryType:^UITableViewCellAccessoryType(UIView *view) {
// Disable preview if bounds are CGRectZero
return !CGRectIsEmpty(view.bounds) ? UITableViewCellAccessoryDisclosureIndicator : UITableViewCellAccessoryNone;
accessoryType:^UITableViewCellAccessoryType(id view) {
return UITableViewCellAccessoryDisclosureIndicator;
}
]
]];
@@ -50,6 +50,8 @@
/// Return nil to use the default reuse identifier
- (NSString *)reuseIdentifierWithTarget:(id)object;
#if FLEX_AT_LEAST_IOS13_SDK
/// An array of actions to place in the first section of the context menu.
- (NSArray<UIAction *> *)additionalActionsWithTarget:(id)object sender:(UIViewController *)sender API_AVAILABLE(ios(13.0));
/// An array where every 2 elements are a key-value pair. The key is a description
@@ -58,6 +60,8 @@
/// Properties and ivars return the address of an object, if they hold one.
- (NSString *)contextualSubtitleWithTarget:(id)object;
#endif
@end
// Even if a property is readonly, it still may be editable
@@ -13,7 +13,6 @@
#import "FLEXObjectExplorerFactory.h"
#import "FLEXFieldEditorViewController.h"
#import "FLEXMethodCallingViewController.h"
#import "FLEXObjectListViewController.h"
#import "FLEXTableView.h"
#import "FLEXUtility.h"
#import "NSArray+FLEX.h"
@@ -129,38 +128,18 @@ FLEXObjectExplorerDefaultsImpl
- (NSString *)reuseIdentifierWithTarget:(id)object { return nil; }
#if FLEX_AT_LEAST_IOS13_SDK
- (NSArray<UIAction *> *)additionalActionsWithTarget:(id)object sender:(UIViewController *)sender __IOS_AVAILABLE(13.0) {
BOOL returnsObject = self.attributes.typeEncoding.flex_typeIsObjectOrClass;
BOOL targetNotNil = [self appropriateTargetForPropertyType:object] != nil;
Class propertyClass = self.attributes.typeEncoding.flex_typeClass;
// "Explore PropertyClass" for properties with a concrete class name
if (returnsObject) {
NSMutableArray<UIAction *> *actions = [NSMutableArray new];
// Action for exploring class of this property
Class propertyClass = self.attributes.typeEncoding.flex_typeClass;
if (propertyClass) {
NSString *title = [NSString stringWithFormat:@"Explore %@", NSStringFromClass(propertyClass)];
[actions addObject:[UIAction actionWithTitle:title image:nil identifier:nil handler:^(UIAction *action) {
UIViewController *explorer = [FLEXObjectExplorerFactory explorerViewControllerForObject:propertyClass];
[sender.navigationController pushViewController:explorer animated:YES];
}]];
}
// Action for exploring references to this object
if (targetNotNil) {
// Since the property holder is not nil, check if the property value is nil
id value = [self currentValueBeforeUnboxingWithTarget:object];
if (value) {
NSString *title = @"List all references";
[actions addObject:[UIAction actionWithTitle:title image:nil identifier:nil handler:^(UIAction *action) {
UIViewController *list = [FLEXObjectListViewController objectsWithReferencesToObject:value];
[sender.navigationController pushViewController:list animated:YES];
}]];
}
}
return actions;
if (propertyClass) {
NSString *title = [NSString stringWithFormat:@"Explore %@", NSStringFromClass(propertyClass)];
return @[[UIAction actionWithTitle:title image:nil identifier:nil handler:^(UIAction *action) {
UIViewController *explorer = [FLEXObjectExplorerFactory explorerViewControllerForObject:propertyClass];
[sender.navigationController pushViewController:explorer animated:YES];
}]];
}
return nil;
@@ -205,6 +184,8 @@ FLEXObjectExplorerDefaultsImpl
return nil;
}
#endif
@end
@@ -281,6 +262,8 @@ FLEXObjectExplorerDefaultsImpl
- (NSString *)reuseIdentifierWithTarget:(id)object { return nil; }
#if FLEX_AT_LEAST_IOS13_SDK
- (NSArray<UIAction *> *)additionalActionsWithTarget:(id)object sender:(UIViewController *)sender __IOS_AVAILABLE(13.0) {
Class ivarClass = self.typeEncoding.flex_typeClass;
@@ -331,6 +314,8 @@ FLEXObjectExplorerDefaultsImpl
return nil;
}
#endif
@end
@@ -375,6 +360,8 @@ FLEXObjectExplorerDefaultsImpl
- (NSString *)reuseIdentifierWithTarget:(id)object { return nil; }
#if FLEX_AT_LEAST_IOS13_SDK
- (NSArray<UIAction *> *)additionalActionsWithTarget:(id)object sender:(UIViewController *)sender __IOS_AVAILABLE(13.0) {
return nil;
}
@@ -391,6 +378,8 @@ FLEXObjectExplorerDefaultsImpl
return nil;
}
#endif
@end
@implementation FLEXMethod (UIKitHelpers)
@@ -468,6 +457,8 @@ FLEXObjectExplorerDefaultsImpl
- (NSString *)reuseIdentifierWithTarget:(id)object { return nil; }
#if FLEX_AT_LEAST_IOS13_SDK
- (NSArray<UIAction *> *)additionalActionsWithTarget:(id)object sender:(UIViewController *)sender __IOS_AVAILABLE(13.0) {
return nil;
}
@@ -485,6 +476,8 @@ FLEXObjectExplorerDefaultsImpl
return nil;
}
#endif
@end
@@ -576,6 +569,8 @@ FLEXObjectExplorerDefaultsImpl
return UITableViewCellAccessoryNone;
}
#if FLEX_AT_LEAST_IOS13_SDK
- (NSArray<UIAction *> *)additionalActionsWithTarget:(id)object sender:(UIViewController *)sender __IOS_AVAILABLE(13.0) {
return nil;
}
@@ -588,6 +583,8 @@ FLEXObjectExplorerDefaultsImpl
return nil;
}
#endif
@end
+1 -7
View File
@@ -27,12 +27,6 @@
+ (instancetype)flex_forEachUpTo:(NSUInteger)bound map:(T(^)(NSUInteger i))block;
+ (instancetype)flex_mapped:(id<NSFastEnumeration>)collection block:(id(^)(T obj, NSUInteger idx))mapFunc;
- (instancetype)flex_sortedUsingSelector:(SEL)selector;
@end
@interface NSMutableArray<T> (Functional)
- (void)flex_filter:(BOOL(^)(T obj, NSUInteger idx))filterFunc;
- (instancetype)sortedUsingSelector:(SEL)selector;
@end
+2 -18
View File
@@ -85,6 +85,7 @@
return array;
}
+ (instancetype)flex_mapped:(id<NSFastEnumeration>)collection block:(id(^)(id obj, NSUInteger idx))mapFunc {
NSMutableArray *array = [NSMutableArray new];
NSInteger idx = 0;
@@ -103,7 +104,7 @@
return array;
}
- (instancetype)flex_sortedUsingSelector:(SEL)selector {
- (instancetype)sortedUsingSelector:(SEL)selector {
if (FLEXArrayClassIsMutable(self)) {
NSMutableArray *me = (id)self;
[me sortUsingSelector:selector];
@@ -114,20 +115,3 @@
}
@end
@implementation NSMutableArray (Functional)
- (void)flex_filter:(BOOL (^)(id, NSUInteger))keepObject {
NSMutableIndexSet *toRemove = [NSMutableIndexSet new];
[self enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
if (!keepObject(obj, idx)) {
[toRemove addIndex:idx];
}
}];
[self removeObjectsAtIndexes:toRemove];
}
@end
@@ -21,21 +21,21 @@ NS_ASSUME_NONNULL_BEGIN
/// @return The type encoding string, or \c nil if \e returnType is \c NULL.
NSString * FLEXTypeEncodingString(const char *returnType, NSUInteger count, ...);
NSArray<Class> * _Nullable FLEXGetAllSubclasses(_Nullable Class cls, BOOL includeSelf);
NSArray<Class> * _Nullable FLEXGetClassHierarchy(_Nullable Class cls, BOOL includeSelf);
NSArray<FLEXProtocol *> * _Nullable FLEXGetConformedProtocols(_Nullable Class cls);
NSArray<Class> *FLEXGetAllSubclasses(_Nullable Class cls, BOOL includeSelf);
NSArray<Class> *FLEXGetClassHierarchy(_Nullable Class cls, BOOL includeSelf);
NSArray<FLEXProtocol *> *FLEXGetConformedProtocols(_Nullable Class cls);
NSArray<FLEXIvar *> * _Nullable FLEXGetAllIvars(_Nullable Class cls);
NSArray<FLEXIvar *> *FLEXGetAllIvars(_Nullable Class cls);
/// @param cls a class object to get instance properties,
/// or a metaclass object to get class properties
NSArray<FLEXProperty *> * _Nullable FLEXGetAllProperties(_Nullable Class cls);
NSArray<FLEXProperty *> *FLEXGetAllProperties(_Nullable Class cls);
/// @param cls a class object to get instance methods,
/// or a metaclass object to get class methods
/// @param instance used to mark methods as instance methods or not.
/// Not used to determine whether to get instance or class methods.
NSArray<FLEXMethod *> * _Nullable FLEXGetAllMethods(_Nullable Class cls, BOOL instance);
NSArray<FLEXMethod *> *FLEXGetAllMethods(_Nullable Class cls, BOOL instance);
/// @param cls a class object to get all instance and class methods.
NSArray<FLEXMethod *> * _Nullable FLEXGetAllInstanceAndClassMethods(_Nullable Class cls);
NSArray<FLEXMethod *> *FLEXGetAllInstanceAndClassMethods(_Nullable Class cls);
@@ -20,7 +20,7 @@
NSString * FLEXTypeEncodingString(const char *returnType, NSUInteger count, ...) {
if (!returnType) return nil;
if (returnType == NULL) return nil;
NSMutableString *encoding = [NSMutableString new];
[encoding appendFormat:@"%s%s%s", returnType, @encode(id), @encode(SEL)];
@@ -37,7 +37,9 @@ NSString * FLEXTypeEncodingString(const char *returnType, NSUInteger count, ...)
}
NSArray<Class> *FLEXGetAllSubclasses(Class cls, BOOL includeSelf) {
if (!cls) return nil;
if (!cls) {
return nil;
}
Class *buffer = NULL;
@@ -69,7 +71,9 @@ NSArray<Class> *FLEXGetAllSubclasses(Class cls, BOOL includeSelf) {
}
NSArray<Class> *FLEXGetClassHierarchy(Class cls, BOOL includeSelf) {
if (!cls) return nil;
if (!cls) {
return nil;
}
NSMutableArray *classes = [NSMutableArray new];
if (includeSelf) {
@@ -84,12 +88,13 @@ NSArray<Class> *FLEXGetClassHierarchy(Class cls, BOOL includeSelf) {
}
NSArray<FLEXProtocol *> *FLEXGetConformedProtocols(Class cls) {
if (!cls) return nil;
if (!cls) {
return nil;
}
unsigned int count = 0;
Protocol *__unsafe_unretained *list = class_copyProtocolList(cls, &count);
NSArray<Protocol *> *protocols = [NSArray arrayWithObjects:list count:count];
free(list);
return [protocols flex_mapped:^id(Protocol *pro, NSUInteger idx) {
return [FLEXProtocol protocol:pro];
@@ -241,7 +246,7 @@ NSArray<FLEXMethod *> *FLEXGetAllMethods(_Nullable Class cls, BOOL instance) {
}
+ (NSArray<FLEXMethod *> *)flex_allClassMethods {
return FLEXGetAllMethods(self.flex_metaclass, NO) ?: @[];
return FLEXGetAllMethods(self.flex_metaclass, NO);
}
+ (FLEXMethod *)flex_methodNamed:(NSString *)name {
@@ -389,7 +394,7 @@ NSArray<FLEXMethod *> *FLEXGetAllMethods(_Nullable Class cls, BOOL instance) {
}
+ (NSArray<FLEXProperty *> *)flex_allClassProperties {
return FLEXGetAllProperties(self.flex_metaclass) ?: @[];
return FLEXGetAllProperties(self.flex_metaclass);
}
+ (FLEXProperty *)flex_propertyNamed:(NSString *)name {
@@ -27,7 +27,7 @@
@interface NSString (KeyPaths)
- (NSString *)flex_stringByRemovingLastKeyPathComponent;
- (NSString *)flex_stringByReplacingLastKeyPathComponent:(NSString *)replacement;
- (NSString *)stringByRemovingLastKeyPathComponent;
- (NSString *)stringByReplacingLastKeyPathComponent:(NSString *)replacement;
@end
@@ -128,7 +128,7 @@
@implementation NSString (KeyPaths)
- (NSString *)flex_stringByRemovingLastKeyPathComponent {
- (NSString *)stringByRemovingLastKeyPathComponent {
if (![self containsString:@"."]) {
return @"";
}
@@ -138,7 +138,7 @@
return mself;
}
- (NSString *)flex_stringByReplacingLastKeyPathComponent:(NSString *)replacement {
- (NSString *)stringByReplacingLastKeyPathComponent:(NSString *)replacement {
// replacement should not have any escaped '.' in it,
// so we escape all '.'
if ([replacement containsString:@"."]) {
+1 -1
View File
@@ -11,7 +11,7 @@ typedef void (^VoidBlock)(void);
@interface NSTimer (Blocks)
+ (instancetype)flex_fireSecondsFromNow:(NSTimeInterval)delay block:(VoidBlock)block;
+ (instancetype)fireSecondsFromNow:(NSTimeInterval)delay block:(VoidBlock)block;
// Forward declaration
//+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
+1 -1
View File
@@ -14,7 +14,7 @@
#pragma clang diagnostic ignored "-Wincomplete-implementation"
@implementation NSTimer (Blocks)
+ (instancetype)flex_fireSecondsFromNow:(NSTimeInterval)delay block:(VoidBlock)block {
+ (instancetype)fireSecondsFromNow:(NSTimeInterval)delay block:(VoidBlock)block {
if (@available(iOS 10, *)) {
return [self scheduledTimerWithTimeInterval:delay repeats:NO block:(id)block];
} else {
@@ -13,10 +13,9 @@ extern NSString * const kFLEXDefaultsToolbarTopMarginKey;
extern NSString * const kFLEXDefaultsiOSPersistentOSLogKey;
extern NSString * const kFLEXDefaultsHidePropertyIvarsKey;
extern NSString * const kFLEXDefaultsHidePropertyMethodsKey;
extern NSString * const kFLEXDefaultsHidePrivateMethodsKey;
extern NSString * const kFLEXDefaultsShowMethodOverridesKey;
extern NSString * const kFLEXDefaultsHideMethodOverridesKey;
extern NSString * const kFLEXDefaultsHideVariablePreviewsKey;
extern NSString * const kFLEXDefaultsNetworkHostDenylistKey;
extern NSString * const kFLEXDefaultsNetworkHostBlacklistKey;
extern NSString * const kFLEXDefaultsDisableOSLogForceASLKey;
extern NSString * const kFLEXDefaultsRegisterJSONExplorerKey;
@@ -28,7 +27,7 @@ extern NSString * const kFLEXDefaultsRegisterJSONExplorerKey;
@property (nonatomic) double flex_toolbarTopMargin;
// Not actually stored in defaults, but written to a file
@property (nonatomic) NSArray<NSString *> *flex_networkHostDenylist;
@property (nonatomic) NSArray<NSString *> *flex_networkHostBlacklist;
/// Whether or not to register the object explorer as a JSON viewer on launch
@property (nonatomic) BOOL flex_registerDictionaryJSONViewerOnLaunch;
@@ -39,7 +38,6 @@ extern NSString * const kFLEXDefaultsRegisterJSONExplorerKey;
@property (nonatomic) BOOL flex_explorerHidesPropertyIvars;
@property (nonatomic) BOOL flex_explorerHidesPropertyMethods;
@property (nonatomic) BOOL flex_explorerHidesPrivateMethods;
@property (nonatomic) BOOL flex_explorerShowsMethodOverrides;
@property (nonatomic) BOOL flex_explorerHidesVariablePreviews;
@@ -12,10 +12,9 @@ NSString * const kFLEXDefaultsToolbarTopMarginKey = @"com.flex.FLEXToolbar.topMa
NSString * const kFLEXDefaultsiOSPersistentOSLogKey = @"com.flipborad.flex.enable_persistent_os_log";
NSString * const kFLEXDefaultsHidePropertyIvarsKey = @"com.flipboard.FLEX.hide_property_ivars";
NSString * const kFLEXDefaultsHidePropertyMethodsKey = @"com.flipboard.FLEX.hide_property_methods";
NSString * const kFLEXDefaultsHidePrivateMethodsKey = @"com.flipboard.FLEX.hide_private_or_namespaced_methods";
NSString * const kFLEXDefaultsShowMethodOverridesKey = @"com.flipboard.FLEX.show_method_overrides";
NSString * const kFLEXDefaultsHideMethodOverridesKey = @"com.flipboard.FLEX.hide_method_overrides";
NSString * const kFLEXDefaultsHideVariablePreviewsKey = @"com.flipboard.FLEX.hide_variable_previews";
NSString * const kFLEXDefaultsNetworkHostDenylistKey = @"com.flipboard.FLEX.network_host_denylist";
NSString * const kFLEXDefaultsNetworkHostBlacklistKey = @"com.flipboard.FLEX.network_host_blacklist";
NSString * const kFLEXDefaultsDisableOSLogForceASLKey = @"com.flipboard.FLEX.try_disable_os_log";
NSString * const kFLEXDefaultsRegisterJSONExplorerKey = @"com.flipboard.FLEX.view_json_as_object";
@@ -62,16 +61,16 @@ NSString * const kFLEXDefaultsRegisterJSONExplorerKey = @"com.flipboard.FLEX.vie
[self setDouble:margin forKey:kFLEXDefaultsToolbarTopMarginKey];
}
- (NSArray<NSString *> *)flex_networkHostDenylist {
- (NSArray<NSString *> *)flex_networkHostBlacklist {
return [NSArray arrayWithContentsOfFile:[
self flex_defaultsPathForFile:kFLEXDefaultsNetworkHostDenylistKey
self flex_defaultsPathForFile:kFLEXDefaultsNetworkHostBlacklistKey
]] ?: @[];
}
- (void)setFlex_networkHostDenylist:(NSArray<NSString *> *)denylist {
NSParameterAssert(denylist);
[denylist writeToFile:[
self flex_defaultsPathForFile:kFLEXDefaultsNetworkHostDenylistKey
- (void)setFlex_networkHostBlacklist:(NSArray<NSString *> *)blacklist {
NSParameterAssert(blacklist);
[blacklist writeToFile:[
self flex_defaultsPathForFile:kFLEXDefaultsNetworkHostBlacklistKey
] atomically:YES];
}
@@ -131,26 +130,14 @@ NSString * const kFLEXDefaultsRegisterJSONExplorerKey = @"com.flipboard.FLEX.vie
];
}
- (BOOL)flex_explorerHidesPrivateMethods {
return [self boolForKey:kFLEXDefaultsHidePrivateMethodsKey];
}
- (void)setFlex_explorerHidesPrivateMethods:(BOOL)show {
[self setBool:show forKey:kFLEXDefaultsHidePrivateMethodsKey];
[NSNotificationCenter.defaultCenter
postNotificationName:kFLEXDefaultsHidePrivateMethodsKey
object:nil
];
}
- (BOOL)flex_explorerShowsMethodOverrides {
return [self boolForKey:kFLEXDefaultsShowMethodOverridesKey];
return [self boolForKey:kFLEXDefaultsHideMethodOverridesKey];
}
- (void)setFlex_explorerShowsMethodOverrides:(BOOL)show {
[self setBool:show forKey:kFLEXDefaultsShowMethodOverridesKey];
[self setBool:show forKey:kFLEXDefaultsHideMethodOverridesKey];
[NSNotificationCenter.defaultCenter
postNotificationName:kFLEXDefaultsShowMethodOverridesKey
postNotificationName:kFLEXDefaultsHideMethodOverridesKey
object:nil
];
}
@@ -1,13 +0,0 @@
//
// Cocoa+FLEXShortcuts.h
// Pods
//
// Created by Tanner on 2/24/21.
//
//
#import <UIKit/UIKit.h>
@interface UIAlertAction (FLEXShortcuts)
@property (nonatomic, readonly) NSString *flex_styleName;
@end
@@ -1,25 +0,0 @@
//
// Cocoa+FLEXShortcuts.m
// Pods
//
// Created by Tanner on 2/24/21.
//
//
#import "Cocoa+FLEXShortcuts.h"
@implementation UIAlertAction (FLEXShortcuts)
- (NSString *)flex_styleName {
switch (self.style) {
case UIAlertActionStyleDefault:
return @"Default style";
case UIAlertActionStyleCancel:
return @"Cancel style";
case UIAlertActionStyleDestructive:
return @"Destructive style";
default:
return [NSString stringWithFormat:@"Unknown (%@)", @(self.style)];
}
}
@end
@@ -1,23 +0,0 @@
//
// UIView+FLEX_Layout.h
// FLEX
//
// Created by Tanner Bennett on 7/18/19.
// Copyright © 2020 FLEX Team. All rights reserved.
//
#import <UIKit/UIKit.h>
#define Padding(p) UIEdgeInsetsMake(p, p, p, p)
@interface UIView (FLEX_Layout)
- (void)flex_centerInView:(UIView *)view;
- (void)flex_pinEdgesTo:(UIView *)view;
- (void)flex_pinEdgesTo:(UIView *)view withInsets:(UIEdgeInsets)insets;
- (void)flex_pinEdgesToSuperview;
- (void)flex_pinEdgesToSuperviewWithInsets:(UIEdgeInsets)insets;
- (void)flex_pinEdgesToSuperviewWithInsets:(UIEdgeInsets)insets aboveView:(UIView *)sibling;
- (void)flex_pinEdgesToSuperviewWithInsets:(UIEdgeInsets)insets belowView:(UIView *)sibling;
@end
@@ -64,7 +64,7 @@
return item;
}
- (UIBarButtonItem *)flex_withTintColor:(UIColor *)tint {
- (UIBarButtonItem *)withTintColor:(UIColor *)tint {
self.tintColor = tint;
return self;
}
@@ -13,9 +13,9 @@ typedef void (^GestureBlock)(UIGestureRecognizer *gesture);
@interface UIGestureRecognizer (Blocks)
+ (instancetype)flex_action:(GestureBlock)action;
+ (instancetype)action:(GestureBlock)action;
@property (nonatomic, setter=flex_setAction:) GestureBlock flex_action;
@property (nonatomic) GestureBlock action;
@end
@@ -14,22 +14,22 @@
static void * actionKey;
+ (instancetype)flex_action:(GestureBlock)action {
+ (instancetype)action:(GestureBlock)action {
UIGestureRecognizer *gesture = [[self alloc] initWithTarget:nil action:nil];
[gesture addTarget:gesture action:@selector(flex_invoke)];
gesture.flex_action = action;
gesture.action = action;
return gesture;
}
- (void)flex_invoke {
self.flex_action(self);
self.action(self);
}
- (GestureBlock)flex_action {
- (GestureBlock)action {
return objc_getAssociatedObject(self, &actionKey);
}
- (void)flex_setAction:(GestureBlock)action {
- (void)setAction:(GestureBlock)action {
objc_setAssociatedObject(self, &actionKey, action, OBJC_ASSOCIATION_COPY);
}
@@ -11,21 +11,16 @@
@implementation UIPasteboard (FLEX)
- (void)flex_copy:(id)object {
if (!object) {
return;
}
if ([object isKindOfClass:[NSString class]]) {
UIPasteboard.generalPasteboard.string = object;
} else if([object isKindOfClass:[NSData class]]) {
[UIPasteboard.generalPasteboard setData:object forPasteboardType:@"public.data"];
} else if ([object isKindOfClass:[NSNumber class]]) {
UIPasteboard.generalPasteboard.string = [object stringValue];
} else {
// TODO: make this an alert instead of an exception
[NSException raise:NSInternalInconsistencyException
format:@"Tried to copy unsupported type: %@", [object class]];
}
[NSException raise:NSInternalInconsistencyException
format:@"Tried to copy unsupported type: %@", [object class]];
}
@end
@@ -9,6 +9,6 @@
@interface UITextField (Range)
@property (nonatomic, readonly) NSRange flex_selectedRange;
@property (nonatomic, readonly) NSRange selectedRange;
@end

Some files were not shown because too many files have changed in this diff Show More