Compare commits
69 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 258ec8697b | |||
| 8f9a6e88ec | |||
| c37270e6ac | |||
| ce10d45c29 | |||
| b2716c4b2e | |||
| ab135ba94e | |||
| bcc04f4113 | |||
| 470b3fa3b3 | |||
| e84dfeae5c | |||
| 44e86e59b7 | |||
| ac6d9cfa3f | |||
| 1a59711760 | |||
| b60ce7a057 | |||
| b4ac210bef | |||
| 9360c58975 | |||
| 43d9a460ce | |||
| 15fee5a8a5 | |||
| fad038392b | |||
| 558d65a0b0 | |||
| 077fca36c0 | |||
| 947769f6f9 | |||
| 7c480c5faf | |||
| fa6c72cb08 | |||
| 9af2926ec1 | |||
| 366a8266bd | |||
| 713fbac54a | |||
| 8198fba689 | |||
| 20e14a36c9 | |||
| 176e98518d | |||
| 31440056c1 | |||
| 6c7b39ed03 | |||
| 96989f7e0c | |||
| 22a23b3b12 | |||
| 90a855a289 | |||
| fd67373995 | |||
| 3b5e095f74 | |||
| c7850df186 | |||
| 6bbcc55cf6 | |||
| e63ea4bbff | |||
| 5a760fb1ac | |||
| e63f2ee3ad | |||
| 46c6dcb7e6 | |||
| bf42bbe27b | |||
| e89fec4b2d | |||
| 715bb92929 | |||
| 109074f98e | |||
| 45fbdb7914 | |||
| cb2e0789d8 | |||
| de1ca783b6 | |||
| 3a9c24b784 | |||
| b57a333fc9 | |||
| 288bf1343e | |||
| a0b1caed54 | |||
| 9282c61183 | |||
| ee6677ee08 | |||
| 3276eb3516 | |||
| a3fa7bbadc | |||
| 637074b354 | |||
| 547bfbaec0 | |||
| 4e1fcf4682 | |||
| 4d50fd2020 | |||
| 2c510c8ca1 | |||
| 8283e2a8e7 | |||
| 3f82631a95 | |||
| d77864494d | |||
| c5ed6d4ece | |||
| 9412f6eccf | |||
| d9aa102e77 | |||
| 6f80476135 |
@@ -0,0 +1,27 @@
|
||||
---
|
||||
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.
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest a new feature for FLEX
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
@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
|
||||
@@ -36,10 +37,13 @@
|
||||
self.waitingToAddTab = YES;
|
||||
|
||||
// Add gesture to reveal toolbar if hidden
|
||||
self.navigationBar.userInteractionEnabled = YES;
|
||||
[self.navigationBar addGestureRecognizer:[[UITapGestureRecognizer alloc]
|
||||
UITapGestureRecognizer *navbarTapGesture = [[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, *)) {
|
||||
@@ -96,6 +100,10 @@
|
||||
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (BOOL)canShowToolbar {
|
||||
return self.topViewController.toolbarItems.count;
|
||||
}
|
||||
|
||||
- (void)addNavigationBarItemsToViewController:(UINavigationItem *)navigationItem {
|
||||
if (!self.presentingViewController) {
|
||||
return;
|
||||
@@ -145,8 +153,15 @@
|
||||
}
|
||||
|
||||
- (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) {
|
||||
if (self.toolbarHidden && self.canShowToolbar) {
|
||||
[self setToolbarHidden:NO animated:YES];
|
||||
}
|
||||
}
|
||||
@@ -162,7 +177,7 @@
|
||||
|
||||
- (void)_gestureRecognizedInteractiveHide:(UIPanGestureRecognizer *)sender {
|
||||
if (sender.state == UIGestureRecognizerStateRecognized) {
|
||||
BOOL show = self.topViewController.toolbarItems.count;
|
||||
BOOL show = self.canShowToolbar;
|
||||
CGFloat yTranslation = [sender translationInView:self.view].y;
|
||||
CGFloat yVelocity = [sender velocityInView:self.view].y;
|
||||
if (yVelocity > 2000) {
|
||||
|
||||
@@ -67,6 +67,10 @@ 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.
|
||||
///
|
||||
|
||||
@@ -124,18 +124,15 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
|
||||
_showsCarousel = showsCarousel;
|
||||
|
||||
if (showsCarousel) {
|
||||
_carousel = ({
|
||||
__weak __typeof(self) weakSelf = self;
|
||||
|
||||
_carousel = ({ weakify(self)
|
||||
|
||||
FLEXScopeCarousel *carousel = [FLEXScopeCarousel new];
|
||||
carousel.selectedIndexChangedAction = ^(NSInteger idx) {
|
||||
__typeof(self) self = weakSelf;
|
||||
carousel.selectedIndexChangedAction = ^(NSInteger idx) { strongify(self);
|
||||
[self.searchDelegate updateSearchResults:self.searchText];
|
||||
};
|
||||
|
||||
// UITableView won't update the header size unless you reset the header view
|
||||
[carousel registerBlockForDynamicTypeChanges:^(FLEXScopeCarousel *carousel) {
|
||||
__typeof(self) self = weakSelf;
|
||||
[carousel registerBlockForDynamicTypeChanges:^(FLEXScopeCarousel *_) { strongify(self);
|
||||
[self layoutTableHeaderIfNeeded];
|
||||
}];
|
||||
|
||||
@@ -241,7 +238,7 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
|
||||
self.tableView.keyboardDismissMode = UIScrollViewKeyboardDismissModeOnDrag;
|
||||
|
||||
// Toolbar
|
||||
self.navigationController.toolbarHidden = NO;
|
||||
self.navigationController.toolbarHidden = self.toolbarItems.count > 0;
|
||||
self.navigationController.hidesBarsOnSwipe = YES;
|
||||
|
||||
// On iOS 13, the root view controller shows it's search bar no matter what.
|
||||
@@ -259,12 +256,17 @@ 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];
|
||||
}
|
||||
@@ -286,6 +288,17 @@ 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;
|
||||
@@ -525,6 +538,30 @@ 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 {
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "FLEXMacros.h"
|
||||
#import "NSArray+FLEX.h"
|
||||
@class FLEXTableView;
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
self.selectionIndicatorStripe.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
|
||||
UIView *superview = self.contentView;
|
||||
[self.titleLabel pinEdgesToSuperviewWithInsets:UIEdgeInsetsMake(10, 15, 8 + stripeHeight, 15)];
|
||||
[self.titleLabel flex_pinEdgesToSuperviewWithInsets:UIEdgeInsetsMake(10, 15, 8 + stripeHeight, 15)];
|
||||
|
||||
[self.selectionIndicatorStripe.leadingAnchor constraintEqualToAnchor:superview.leadingAnchor].active = YES;
|
||||
[self.selectionIndicatorStripe.bottomAnchor constraintEqualToAnchor:superview.bottomAnchor].active = YES;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#import "FLEXScopeCarousel.h"
|
||||
#import "FLEXCarouselCell.h"
|
||||
#import "FLEXColor.h"
|
||||
#import "FLEXMacros.h"
|
||||
#import "UIView+FLEX_Layout.h"
|
||||
|
||||
const CGFloat kCarouselItemSpacing = 0;
|
||||
@@ -72,15 +73,14 @@ NSString * const kCarouselCellReuseIdentifier = @"kCarouselCellReuseIdentifier";
|
||||
self.sizingCell.title = @"NSObject";
|
||||
|
||||
// Dynamic type
|
||||
__weak __typeof(self) weakSelf = self;
|
||||
weakify(self);
|
||||
_dynamicTypeObserver = [NSNotificationCenter.defaultCenter
|
||||
addObserverForName:UIContentSizeCategoryDidChangeNotification
|
||||
object:nil queue:nil usingBlock:^(NSNotification *note) {
|
||||
object:nil queue:nil usingBlock:^(NSNotification *note) { strongify(self)
|
||||
[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 pinEdgesToSuperview];
|
||||
[self.collectionView flex_pinEdgesToSuperview];
|
||||
|
||||
self.constraintsInstalled = YES;
|
||||
}
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
///
|
||||
/// 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;
|
||||
- (void)toggleToolWithViewControllerProvider:(UINavigationController *(^)(void))future
|
||||
completion:(void (^)(void))completion;
|
||||
|
||||
// Keyboard shortcut helpers
|
||||
|
||||
|
||||
@@ -45,6 +45,9 @@ 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;
|
||||
@@ -58,6 +61,9 @@ 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;
|
||||
|
||||
@@ -118,6 +124,11 @@ 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 {
|
||||
@@ -450,16 +461,16 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
];
|
||||
[toolbar.selectedViewDescriptionContainer addGestureRecognizer:self.detailsTapGR];
|
||||
// Swipe gestures for selecting deeper / higher views at a point
|
||||
UISwipeGestureRecognizer *leftSwipe = [[UISwipeGestureRecognizer alloc]
|
||||
UIPanGestureRecognizer *leftSwipe = [[UIPanGestureRecognizer alloc]
|
||||
initWithTarget:self action:@selector(handleChangeViewAtPointGesture:)
|
||||
];
|
||||
UISwipeGestureRecognizer *rightSwipe = [[UISwipeGestureRecognizer alloc]
|
||||
initWithTarget:self action:@selector(handleChangeViewAtPointGesture:)
|
||||
];
|
||||
leftSwipe.direction = UISwipeGestureRecognizerDirectionLeft;
|
||||
rightSwipe.direction = UISwipeGestureRecognizerDirectionRight;
|
||||
// UIPanGestureRecognizer *rightSwipe = [[UIPanGestureRecognizer 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]
|
||||
@@ -598,19 +609,54 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handleChangeViewAtPointGesture:(UISwipeGestureRecognizer *)sender {
|
||||
- (void)handleChangeViewAtPointGesture:(UIPanGestureRecognizer *)sender {
|
||||
NSInteger max = self.viewsAtTapPoint.count - 1;
|
||||
NSInteger currentIdx = [self.viewsAtTapPoint indexOfObject:self.selectedView];
|
||||
switch (sender.direction) {
|
||||
case UISwipeGestureRecognizerDirectionLeft:
|
||||
self.selectedView = self.viewsAtTapPoint[MIN(max, currentIdx + 1)];
|
||||
break;
|
||||
case UISwipeGestureRecognizerDirectionRight:
|
||||
self.selectedView = self.viewsAtTapPoint[MAX(0, currentIdx - 1)];
|
||||
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;
|
||||
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];
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
default: break;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)actuateSelectionChangedFeedback {
|
||||
if (@available(iOS 10.0, *)) {
|
||||
[self.selectionFBG selectionChanged];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -872,13 +918,12 @@ 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) {
|
||||
[self dismissViewControllerAnimated:YES completion:completion];
|
||||
} else if (future) {
|
||||
@@ -924,11 +969,7 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
} else {
|
||||
return [FLEXHierarchyViewController delegate:self];
|
||||
}
|
||||
} completion:^{
|
||||
if (completion) {
|
||||
completion();
|
||||
}
|
||||
}];
|
||||
} completion:completion];
|
||||
}
|
||||
|
||||
- (void)toggleMenuTool {
|
||||
|
||||
@@ -12,16 +12,11 @@
|
||||
#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>
|
||||
|
||||
@@ -20,6 +20,5 @@
|
||||
#import <FLEX/FLEX-Categories.h>
|
||||
#import <FLEX/FLEX-ObjectExploring.h>
|
||||
|
||||
#import <FLEX/FLEXMacros.h>
|
||||
#import <FLEX/FLEXAlert.h>
|
||||
#import <FLEX/FLEXResources.h>
|
||||
|
||||
@@ -35,6 +35,7 @@ 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,11 +20,25 @@
|
||||
#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) NSArray<FLEXObjectRef *> *references;
|
||||
@property (nonatomic, readonly, nullable) NSArray<FLEXObjectRef *> *references;
|
||||
@property (nonatomic, readonly) NSArray<NSPredicate *> *predicates;
|
||||
@property (nonatomic, readonly) NSArray<NSString *> *sectionTitles;
|
||||
|
||||
@@ -38,7 +52,7 @@
|
||||
+ (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(^isObserver)(FLEXObjectRef *, NSDictionary *) = ^BOOL(FLEXObjectRef *ref, NSDictionary *bindings) {
|
||||
BOOL(^isKVORelated)(FLEXObjectRef *, NSDictionary *) = ^BOOL(FLEXObjectRef *ref, NSDictionary *bindings) {
|
||||
NSString *row = ref.reference;
|
||||
return [row isEqualToString:@"__NSObserver object"] ||
|
||||
[row isEqualToString:@"_CFXNotificationObjcObserverRegistration _object"];
|
||||
@@ -65,34 +79,50 @@
|
||||
([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 !(isObserver(ref, bindings) || isConstraintRelated(ref, bindings));
|
||||
return !(
|
||||
isKVORelated(ref, bindings) ||
|
||||
isConstraintRelated(ref, bindings) ||
|
||||
isFLEXClass(ref, bindings)
|
||||
);
|
||||
};
|
||||
|
||||
switch (section) {
|
||||
case 0: return [NSPredicate predicateWithBlock:isEssential];
|
||||
case 1: return [NSPredicate predicateWithBlock:isConstraintRelated];
|
||||
case 2: return [NSPredicate predicateWithBlock:isObserver];
|
||||
case FLEXObjectReferenceSectionMain:
|
||||
return [NSPredicate predicateWithBlock:isEssential];
|
||||
case FLEXObjectReferenceSectionAutoLayout:
|
||||
return [NSPredicate predicateWithBlock:isConstraintRelated];
|
||||
case FLEXObjectReferenceSectionKVO:
|
||||
return [NSPredicate predicateWithBlock:isKVORelated];
|
||||
case FLEXObjectReferenceSectionFLEX:
|
||||
return [NSPredicate predicateWithBlock:isFLEXClass];
|
||||
|
||||
default: return nil;
|
||||
}
|
||||
}
|
||||
|
||||
+ (NSArray<NSPredicate *> *)defaultPredicates {
|
||||
return @[[self defaultPredicateForSection:0],
|
||||
[self defaultPredicateForSection:1],
|
||||
[self defaultPredicateForSection:2]];
|
||||
return [NSArray flex_forEachUpTo:FLEXObjectReferenceSectionCount map:^id(NSUInteger i) {
|
||||
return [self defaultPredicateForSection:i];
|
||||
}];
|
||||
}
|
||||
|
||||
+ (NSArray<NSString *> *)defaultSectionTitles {
|
||||
return @[@"", @"AutoLayout", @"Trivial"];
|
||||
return @[
|
||||
@"", @"AutoLayout", @"Key-Value Observing", @"FLEX"
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Initialization
|
||||
|
||||
- (id)initWithReferences:(NSArray<FLEXObjectRef *> *)references {
|
||||
- (id)initWithReferences:(nullable NSArray<FLEXObjectRef *> *)references {
|
||||
return [self initWithReferences:references predicates:nil sectionTitles:nil];
|
||||
}
|
||||
|
||||
@@ -184,19 +214,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
free(ivars);
|
||||
tryClass = class_getSuperclass(tryClass);
|
||||
}
|
||||
}];
|
||||
|
||||
NSArray<NSPredicate *> *predicates = [self defaultPredicates];
|
||||
NSArray<NSString *> *sectionTitles = [self defaultSectionTitles];
|
||||
FLEXObjectListViewController *viewController = [[self alloc]
|
||||
initWithReferences:instances
|
||||
predicates:predicates
|
||||
sectionTitles:sectionTitles
|
||||
predicates:self.defaultPredicates
|
||||
sectionTitles:self.defaultSectionTitles
|
||||
];
|
||||
viewController.title = [NSString stringWithFormat:@"Referencing %@ %p",
|
||||
NSStringFromClass(object_getClass(object)), object
|
||||
[FLEXRuntimeUtility safeClassNameForObject:object], object
|
||||
];
|
||||
return viewController;
|
||||
}
|
||||
@@ -231,7 +260,7 @@
|
||||
}];
|
||||
}
|
||||
|
||||
- (FLEXMutableListSection *)makeSection:(NSArray *)rows title:(NSString *)title {
|
||||
- (FLEXMutableListSection *)makeSection:(NSArray *)rows title:(NSString *)title { weakify(self)
|
||||
FLEXMutableListSection *section = [FLEXMutableListSection list:rows
|
||||
cellConfiguration:^(FLEXTableViewCell *cell, FLEXObjectRef *ref, NSInteger row) {
|
||||
cell.textLabel.text = ref.reference;
|
||||
@@ -246,14 +275,10 @@
|
||||
}
|
||||
];
|
||||
|
||||
__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.selectionHandler = ^(UIViewController *host, FLEXObjectRef *ref) { strongify(self)
|
||||
[self.navigationController pushViewController:[
|
||||
FLEXObjectExplorerFactory explorerViewControllerForObject:ref.object
|
||||
] animated:YES];
|
||||
};
|
||||
|
||||
section.customTitle = title;
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
_object = object;
|
||||
_wantsSummary = showSummary;
|
||||
|
||||
NSString *class = NSStringFromClass(object_getClass(object));
|
||||
NSString *class = [FLEXRuntimeUtility safeClassNameForObject: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 = UIDataDetectorTypeLink;
|
||||
configuration.dataDetectorTypes = WKDataDetectorTypeLink;
|
||||
}
|
||||
|
||||
self.webView = [[WKWebView alloc] initWithFrame:CGRectZero configuration:configuration];
|
||||
|
||||
@@ -54,9 +54,8 @@ typedef NS_ENUM(NSUInteger, FLEXFileBrowserSortAttribute) {
|
||||
self.title = [path lastPathComponent];
|
||||
self.operationQueue = [NSOperationQueue new];
|
||||
|
||||
|
||||
//computing path size
|
||||
FLEXFileBrowserController *__weak weakSelf = self;
|
||||
// Compute path size
|
||||
weakify(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];
|
||||
@@ -66,16 +65,15 @@ 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 (!weakSelf) {
|
||||
// Bail if the interested view controller has gone away
|
||||
if (!self) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
FLEXFileBrowserController *__strong strongSelf = weakSelf;
|
||||
strongSelf.recursiveSize = @(totalSize);
|
||||
[strongSelf.tableView reloadData];
|
||||
dispatch_async(dispatch_get_main_queue(), ^{ strongify(self)
|
||||
self.recursiveSize = @(totalSize);
|
||||
[self.tableView reloadData];
|
||||
});
|
||||
});
|
||||
|
||||
@@ -360,38 +358,41 @@ typedef NS_ENUM(NSUInteger, FLEXFileBrowserSortAttribute) {
|
||||
|
||||
#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]];
|
||||
}];
|
||||
- (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]
|
||||
];
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@@ -170,7 +170,7 @@
|
||||
|
||||
- (NSString *)password {
|
||||
if (self.passwordData.length) {
|
||||
return [NSString stringWithCString:self.passwordData.bytes encoding:NSUTF8StringEncoding];
|
||||
return [[NSString alloc] initWithData:self.passwordData encoding:NSUTF8StringEncoding];
|
||||
}
|
||||
|
||||
return nil;
|
||||
|
||||
@@ -30,10 +30,10 @@
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
self.navigationItem.rightBarButtonItems = @[
|
||||
[UIBarButtonItem flex_systemItem:UIBarButtonSystemItemTrash target:self action:@selector(trashPressed:)],
|
||||
[UIBarButtonItem flex_systemItem:UIBarButtonSystemItemAdd target:self action:@selector(addPressed)],
|
||||
];
|
||||
[self addToolbarItems:@[
|
||||
FLEXBarButtonItemSystem(Add, self, @selector(addPressed)),
|
||||
[FLEXBarButtonItemSystem(Trash, self, @selector(trashPressed:)) flex_withTintColor:UIColor.redColor],
|
||||
]];
|
||||
|
||||
[self reloadData];
|
||||
}
|
||||
@@ -43,14 +43,15 @@
|
||||
cellConfiguration:^(__kindof FLEXTableViewCell *cell, NSDictionary *item, NSInteger row) {
|
||||
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
|
||||
|
||||
id account = item[kFLEXKeychainAccountKey];
|
||||
if ([account isKindOfClass:[NSString class]]) {
|
||||
cell.textLabel.text = account;
|
||||
id service = item[kFLEXKeychainWhereKey];
|
||||
if ([service isKindOfClass:[NSString class]]) {
|
||||
cell.textLabel.text = service;
|
||||
cell.detailTextLabel.text = [item[kFLEXKeychainAccountKey] description];
|
||||
} else {
|
||||
cell.textLabel.text = [NSString stringWithFormat:
|
||||
@"[%@]\n\n%@",
|
||||
NSStringFromClass([account class]),
|
||||
[account description]
|
||||
NSStringFromClass([service class]),
|
||||
[service description]
|
||||
];
|
||||
}
|
||||
} filterMatcher:^BOOL(NSString *filterText, NSDictionary *item) {
|
||||
@@ -129,6 +130,18 @@
|
||||
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];
|
||||
@@ -136,8 +149,12 @@
|
||||
|
||||
[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 source:sender];
|
||||
} showFrom:self];
|
||||
}
|
||||
|
||||
- (void)addPressed {
|
||||
|
||||
@@ -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];
|
||||
}] sortedUsingSelector:@selector(caseInsensitiveCompare:)];
|
||||
}] flex_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);
|
||||
}];
|
||||
}] sortedUsingSelector:@selector(caseInsensitiveCompare:)];
|
||||
}] flex_sortedUsingSelector:@selector(caseInsensitiveCompare:)];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
[self sizeToFit];
|
||||
|
||||
if (@available(iOS 13, *)) {
|
||||
self.appearance = UIKeyboardTypeDefault;
|
||||
self.appearance = UIKeyboardAppearanceDefault;
|
||||
} else {
|
||||
self.appearance = UIKeyboardAppearanceLight;
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@
|
||||
|
||||
// Change "Bundle.fooba" to "Bundle.foobar."
|
||||
NSString *orig = self.delegate.searchController.searchBar.text;
|
||||
NSString *keyPath = [orig stringByReplacingLastKeyPathComponent:text];
|
||||
NSString *keyPath = [orig flex_stringByReplacingLastKeyPathComponent:text];
|
||||
self.delegate.searchController.searchBar.text = keyPath;
|
||||
|
||||
self.keyPath = [FLEXRuntimeKeyPathTokenizer tokenizeString:keyPath];
|
||||
@@ -130,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.selectedRange replacementText:text]) {
|
||||
if ([self searchBar:searchBar shouldChangeTextInRange:field.flex_selectedRange replacementText:text]) {
|
||||
[field replaceRange:field.selectedTextRange withText:text];
|
||||
}
|
||||
}
|
||||
@@ -266,7 +266,7 @@
|
||||
self.filteredClasses = nil;
|
||||
}
|
||||
|
||||
self.timer = [NSTimer fireSecondsFromNow:0.15 block:^{
|
||||
self.timer = [NSTimer flex_fireSecondsFromNow:0.15 block:^{
|
||||
[self updateTable];
|
||||
}];
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
||||
|
||||
if (@available(iOS 13, *)) {
|
||||
self.appearance = UIKeyboardTypeDefault;
|
||||
self.appearance = UIKeyboardAppearanceDefault;
|
||||
} else {
|
||||
self.appearance = UIKeyboardAppearanceLight;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#import "FLEXTableView.h"
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
#import "FLEXAlert.h"
|
||||
#import "FLEXRuntimeClient.h"
|
||||
|
||||
@interface FLEXObjcRuntimeViewController () <FLEXKeyPathSearchControllerDelegate>
|
||||
|
||||
@@ -28,9 +29,24 @@
|
||||
- (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.
|
||||
//
|
||||
@@ -57,15 +73,6 @@
|
||||
[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,10 +100,9 @@ static BOOL my_os_log_shim_enabled(void *addr) {
|
||||
self.showsSearchBar = YES;
|
||||
self.showSearchBarInitially = NO;
|
||||
|
||||
__weak __typeof(self) weakSelf = self;
|
||||
id logHandler = ^(NSArray<FLEXSystemLogMessage *> *newMessages) {
|
||||
__strong __typeof(weakSelf) strongSelf = weakSelf;
|
||||
[strongSelf handleUpdateWithNewMessages:newMessages];
|
||||
weakify(self)
|
||||
id logHandler = ^(NSArray<FLEXSystemLogMessage *> *newMessages) { strongify(self)
|
||||
[self handleUpdateWithNewMessages:newMessages];
|
||||
};
|
||||
|
||||
if (FLEXOSLogAvailable() && !FLEXNSLogHookWorks) {
|
||||
@@ -137,20 +136,18 @@ static BOOL my_os_log_shim_enabled(void *addr) {
|
||||
[self.logController startMonitoring];
|
||||
}
|
||||
|
||||
- (NSArray<FLEXTableViewSection *> *)makeSections {
|
||||
__weak __typeof(self) weakSelf = self;
|
||||
- (NSArray<FLEXTableViewSection *> *)makeSections { weakify(self)
|
||||
_logMessages = [FLEXMutableListSection list:@[]
|
||||
cellConfiguration:^(FLEXSystemLogCell *cell, FLEXSystemLogMessage *message, NSInteger row) {
|
||||
__strong __typeof(self) strongSelf = weakSelf;
|
||||
if (strongSelf) {
|
||||
cell.logMessage = message;
|
||||
cell.highlightedText = strongSelf.filterText;
|
||||
strongify(self)
|
||||
|
||||
cell.logMessage = message;
|
||||
cell.highlightedText = self.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];
|
||||
@@ -265,8 +262,30 @@ 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 ?: @"";
|
||||
}
|
||||
}
|
||||
|
||||
#if FLEX_AT_LEAST_IOS13_SDK
|
||||
|
||||
- (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]];
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@end
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
#import "FLEXManager.h"
|
||||
#import "FLEXGlobalsEntry.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@@ -14,7 +15,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
#pragma mark - Globals Screen Entries
|
||||
|
||||
/// Adds an entry at the bottom of the list of Global State items.
|
||||
/// Adds an entry at the top 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
|
||||
@@ -26,7 +27,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 bottom of the list of Global State items.
|
||||
/// Adds an entry at the top 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
|
||||
@@ -34,10 +35,23 @@ 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.
|
||||
/// you may want to use __weak references as needed.
|
||||
- (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.
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
#import "FLEXManager+Extensibility.h"
|
||||
#import "FLEXManager+Private.h"
|
||||
#import "FLEXNavigationController.h"
|
||||
#import "FLEXGlobalsEntry.h"
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
#import "FLEXKeyboardShortcutManager.h"
|
||||
#import "FLEXExplorerViewController.h"
|
||||
@@ -58,6 +57,23 @@
|
||||
[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
|
||||
|
||||
@@ -95,60 +111,49 @@
|
||||
[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];
|
||||
@@ -156,15 +161,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"];
|
||||
@@ -183,7 +188,7 @@
|
||||
if (@available(iOS 11, *)) {
|
||||
return scrollView.adjustedContentInset;
|
||||
}
|
||||
|
||||
|
||||
return scrollView.contentInset;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 blacklisted entries in this array will be not be recorded (eg. google.com).
|
||||
/// Requests whose host ends with one of the excluded 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 *> *networkRequestHostBlacklist;
|
||||
@property (nonatomic) NSMutableArray<NSString *> *networkRequestHostDenylist;
|
||||
|
||||
/// Sets custom viewer for specific content type.
|
||||
/// @param contentType Mime type like application/json
|
||||
|
||||
@@ -48,12 +48,12 @@
|
||||
FLEXNetworkRecorder.defaultRecorder.responseCacheByteLimit = networkResponseCacheByteLimit;
|
||||
}
|
||||
|
||||
- (NSMutableArray<NSString *> *)networkRequestHostBlacklist {
|
||||
return FLEXNetworkRecorder.defaultRecorder.hostBlacklist;
|
||||
- (NSMutableArray<NSString *> *)networkRequestHostDenylist {
|
||||
return FLEXNetworkRecorder.defaultRecorder.hostDenylist;
|
||||
}
|
||||
|
||||
- (void)setNetworkRequestHostBlacklist:(NSMutableArray<NSString *> *)networkRequestHostBlacklist {
|
||||
FLEXNetworkRecorder.defaultRecorder.hostBlacklist = networkRequestHostBlacklist;
|
||||
- (void)setNetworkRequestHostDenylist:(NSMutableArray<NSString *> *)networkRequestHostDenylist {
|
||||
FLEXNetworkRecorder.defaultRecorder.hostDenylist = networkRequestHostDenylist;
|
||||
}
|
||||
|
||||
- (void)setCustomViewerForContentType:(NSString *)contentType
|
||||
|
||||
@@ -25,6 +25,14 @@ 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));
|
||||
|
||||
@@ -91,6 +91,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (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.explorerViewController toggleToolWithViewControllerProvider:future completion:completion];
|
||||
}
|
||||
|
||||
- (void)showExplorerFromScene:(UIWindowScene *)scene {
|
||||
#if FLEX_AT_LEAST_IOS13_SDK
|
||||
if (@available(iOS 13.0, *)) {
|
||||
|
||||
@@ -28,7 +28,8 @@
|
||||
}
|
||||
|
||||
if (request.HTTPBody) {
|
||||
[curlCommandString appendFormat:@"-d \'%@\'", [NSString stringWithCString:request.HTTPBody.bytes encoding:NSUTF8StringEncoding]];
|
||||
NSString *body = [[NSString alloc] initWithData:request.HTTPBody encoding:NSUTF8StringEncoding];
|
||||
[curlCommandString appendFormat:@"-d \'%@\'", body];
|
||||
}
|
||||
|
||||
return curlCommandString;
|
||||
|
||||
@@ -406,22 +406,22 @@
|
||||
UIPasteboard.generalPasteboard.string = request.URL.absoluteString ?: @"";
|
||||
}
|
||||
];
|
||||
UIAction *blacklist = [UIAction
|
||||
actionWithTitle:[NSString stringWithFormat:@"Blacklist '%@'", request.URL.host]
|
||||
UIAction *denylist = [UIAction
|
||||
actionWithTitle:[NSString stringWithFormat:@"Exclude '%@'", request.URL.host]
|
||||
image:nil
|
||||
identifier:nil
|
||||
handler:^(__kindof UIAction *action) {
|
||||
NSMutableArray *blacklist = FLEXNetworkRecorder.defaultRecorder.hostBlacklist;
|
||||
[blacklist addObject:request.URL.host];
|
||||
[FLEXNetworkRecorder.defaultRecorder clearBlacklistedTransactions];
|
||||
[FLEXNetworkRecorder.defaultRecorder synchronizeBlacklist];
|
||||
NSMutableArray *denylist = FLEXNetworkRecorder.defaultRecorder.hostDenylist;
|
||||
[denylist addObject:request.URL.host];
|
||||
[FLEXNetworkRecorder.defaultRecorder clearExcludedTransactions];
|
||||
[FLEXNetworkRecorder.defaultRecorder synchronizeDenylist];
|
||||
[self tryUpdateTransactions];
|
||||
}
|
||||
];
|
||||
return [UIMenu
|
||||
menuWithTitle:@"" image:nil identifier:nil
|
||||
options:UIMenuOptionsDisplayInline
|
||||
children:@[copy, blacklist]
|
||||
children:@[copy, denylist]
|
||||
];
|
||||
}
|
||||
];
|
||||
|
||||
@@ -28,13 +28,13 @@ extern NSString *const kFLEXNetworkRecorderTransactionsClearedNotification;
|
||||
/// with an "image", "video", or "audio" prefix.
|
||||
@property (nonatomic) BOOL shouldCacheMediaResponses;
|
||||
|
||||
@property (nonatomic) NSMutableArray<NSString *> *hostBlacklist;
|
||||
@property (nonatomic) NSMutableArray<NSString *> *hostDenylist;
|
||||
|
||||
/// Call this after adding to or setting the \c hostBlacklist to remove blacklisted transactions
|
||||
- (void)clearBlacklistedTransactions;
|
||||
/// Call this after adding to or setting the \c hostDenylist to remove excluded transactions
|
||||
- (void)clearExcludedTransactions;
|
||||
|
||||
/// Call this to save the blacklist to the disk to be loaded next time
|
||||
- (void)synchronizeBlacklist;
|
||||
/// Call this to save the denylist to the disk to be loaded next time
|
||||
- (void)synchronizeDenylist;
|
||||
|
||||
|
||||
// Accessing recorded network activity
|
||||
|
||||
@@ -45,7 +45,7 @@ NSString *const kFLEXNetworkRecorderResponseCacheLimitDefaultsKey = @"com.flex.r
|
||||
|
||||
self.orderedTransactions = [NSMutableArray new];
|
||||
self.requestIDsToTransactions = [NSMutableDictionary new];
|
||||
self.hostBlacklist = NSUserDefaults.standardUserDefaults.flex_networkHostBlacklist.mutableCopy;
|
||||
self.hostDenylist = NSUserDefaults.standardUserDefaults.flex_networkHostDenylist.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)clearBlacklistedTransactions {
|
||||
- (void)clearExcludedTransactions {
|
||||
dispatch_sync(self.queue, ^{
|
||||
self.orderedTransactions = ({
|
||||
[self.orderedTransactions flex_filtered:^BOOL(FLEXNetworkTransaction *ta, NSUInteger idx) {
|
||||
NSString *host = ta.request.URL.host;
|
||||
for (NSString *blacklisted in self.hostBlacklist) {
|
||||
if ([host hasSuffix:blacklisted]) {
|
||||
for (NSString *excluded in self.hostDenylist) {
|
||||
if ([host hasSuffix:excluded]) {
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
@@ -117,8 +117,8 @@ NSString *const kFLEXNetworkRecorderResponseCacheLimitDefaultsKey = @"com.flex.r
|
||||
});
|
||||
}
|
||||
|
||||
- (void)synchronizeBlacklist {
|
||||
NSUserDefaults.standardUserDefaults.flex_networkHostBlacklist = self.hostBlacklist;
|
||||
- (void)synchronizeDenylist {
|
||||
NSUserDefaults.standardUserDefaults.flex_networkHostDenylist = self.hostDenylist;
|
||||
}
|
||||
|
||||
#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.hostBlacklist) {
|
||||
for (NSString *host in self.hostDenylist) {
|
||||
if ([request.URL.host hasSuffix:host]) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
@property (nonatomic, readonly) UISlider *cacheLimitSlider;
|
||||
@property (nonatomic) UILabel *cacheLimitLabel;
|
||||
|
||||
@property (nonatomic) NSMutableArray<NSString *> *hostBlacklist;
|
||||
@property (nonatomic) NSMutableArray<NSString *> *hostDenylist;
|
||||
@end
|
||||
|
||||
@implementation FLEXNetworkSettingsController
|
||||
@@ -32,7 +32,7 @@
|
||||
[super viewDidLoad];
|
||||
|
||||
[self disableToolbar];
|
||||
self.hostBlacklist = FLEXNetworkRecorder.defaultRecorder.hostBlacklist.mutableCopy;
|
||||
self.hostDenylist = FLEXNetworkRecorder.defaultRecorder.hostDenylist.mutableCopy;
|
||||
|
||||
NSUserDefaults *defaults = NSUserDefaults.standardUserDefaults;
|
||||
|
||||
@@ -107,13 +107,13 @@
|
||||
#pragma mark - Table View Data Source
|
||||
|
||||
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
|
||||
return self.hostBlacklist.count ? 2 : 1;
|
||||
return self.hostDenylist.count ? 2 : 1;
|
||||
}
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
|
||||
switch (section) {
|
||||
case 0: return 5;
|
||||
case 1: return self.hostBlacklist.count;
|
||||
case 1: return self.hostDenylist.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 Blacklist";
|
||||
case 1: return @"Host Denylist";
|
||||
default: return nil;
|
||||
}
|
||||
}
|
||||
@@ -162,7 +162,7 @@
|
||||
cell.accessoryView = self.jsonViewerSwitch;
|
||||
break;
|
||||
case 3:
|
||||
cell.textLabel.text = @"Reset Host Blacklist";
|
||||
cell.textLabel.text = @"Reset Host Denylist";
|
||||
cell.textLabel.textColor = tableView.tintColor;
|
||||
break;
|
||||
case 4:
|
||||
@@ -195,9 +195,9 @@
|
||||
break;
|
||||
}
|
||||
|
||||
// Blacklist entries
|
||||
// Denylist entries
|
||||
case 1: {
|
||||
cell.textLabel.text = self.hostBlacklist[indexPath.row];
|
||||
cell.textLabel.text = self.hostDenylist[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 Blacklist" row
|
||||
// Can only select the "Reset Host Denylist" 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 Blacklist");
|
||||
make.title(@"Reset Host Denylist");
|
||||
make.message(@"You cannot undo this action. Are you sure?");
|
||||
make.button(@"Reset").destructiveStyle().handler(^(NSArray<NSString *> *strings) {
|
||||
self.hostBlacklist = nil;
|
||||
[FLEXNetworkRecorder.defaultRecorder.hostBlacklist removeAllObjects];
|
||||
[FLEXNetworkRecorder.defaultRecorder synchronizeBlacklist];
|
||||
self.hostDenylist = nil;
|
||||
[FLEXNetworkRecorder.defaultRecorder.hostDenylist removeAllObjects];
|
||||
[FLEXNetworkRecorder.defaultRecorder synchronizeDenylist];
|
||||
[self.tableView deleteSections:
|
||||
[NSIndexSet indexSetWithIndex:1]
|
||||
withRowAnimation:UITableViewRowAnimationAutomatic];
|
||||
@@ -242,10 +242,10 @@
|
||||
forRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
NSParameterAssert(style == UITableViewCellEditingStyleDelete);
|
||||
|
||||
NSString *host = self.hostBlacklist[indexPath.row];
|
||||
[self.hostBlacklist removeObjectAtIndex:indexPath.row];
|
||||
[FLEXNetworkRecorder.defaultRecorder.hostBlacklist removeObject:host];
|
||||
[FLEXNetworkRecorder.defaultRecorder synchronizeBlacklist];
|
||||
NSString *host = self.hostDenylist[indexPath.row];
|
||||
[self.hostDenylist removeObjectAtIndex:indexPath.row];
|
||||
[FLEXNetworkRecorder.defaultRecorder.hostDenylist removeObject:host];
|
||||
[FLEXNetworkRecorder.defaultRecorder synchronizeDenylist];
|
||||
|
||||
[tableView deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
|
||||
}
|
||||
|
||||
@@ -328,24 +328,23 @@ 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.
|
||||
__weak NSData *weakResponseData = responseData;
|
||||
responseBodyRow.selectionFuture = ^UIViewController * () {
|
||||
weakify(responseData)
|
||||
responseBodyRow.selectionFuture = ^UIViewController *() { strongify(responseData)
|
||||
|
||||
// Show the response if we can
|
||||
NSString *contentType = transaction.response.MIMEType;
|
||||
NSData *strongResponseData = weakResponseData;
|
||||
if (strongResponseData) {
|
||||
UIViewController *bodyDetailController = [self detailViewControllerForMIMEType:contentType data:strongResponseData];
|
||||
if (bodyDetailController) {
|
||||
bodyDetailController.title = @"Response";
|
||||
return bodyDetailController;
|
||||
if (responseData) {
|
||||
UIViewController *bodyDetails = [self detailViewControllerForMIMEType:contentType data:responseData];
|
||||
if (bodyDetails) {
|
||||
bodyDetails.title = @"Response";
|
||||
return bodyDetails;
|
||||
}
|
||||
}
|
||||
|
||||
// We can't show the response, alert user
|
||||
return [FLEXAlert makeAlert:^(FLEXAlert *make) {
|
||||
make.title(@"Unable to View Response");
|
||||
if (strongResponseData) {
|
||||
if (responseData) {
|
||||
make.message(@"No viewer content type: ").message(contentType);
|
||||
} else {
|
||||
make.message(@"The response has been purged from the cache");
|
||||
@@ -426,7 +425,8 @@ typedef UIViewController *(^FLEXNetworkDetailRowSelectionFuture)(void);
|
||||
if (transaction.cachedRequestBody.length > 0) {
|
||||
NSString *contentType = [transaction.request valueForHTTPHeaderField:@"Content-Type"];
|
||||
if ([contentType hasPrefix:@"application/x-www-form-urlencoded"]) {
|
||||
NSString *bodyString = [NSString stringWithCString:[self postBodyDataForTransaction:transaction].bytes encoding:NSUTF8StringEncoding];
|
||||
NSData *body = [self postBodyDataForTransaction:transaction];
|
||||
NSString *bodyString = [[NSString alloc] initWithData:body encoding:NSUTF8StringEncoding];
|
||||
postBodySection.rows = [self networkDetailRowsFromQueryItems:[FLEXUtility itemsFromQueryString:bodyString]];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -136,7 +136,13 @@
|
||||
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
|
||||
@@ -147,13 +153,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
|
||||
]];
|
||||
[_allClassProperties addObject:[self
|
||||
[allClassProps addObject:[self
|
||||
metadataUniquedByName:[cls flex_allClassProperties]
|
||||
superclass:superclass
|
||||
kind:FLEXMetadataKindClassProperties
|
||||
@@ -165,13 +171,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
|
||||
@@ -198,7 +204,7 @@
|
||||
|
||||
_classHierarchy = [FLEXStaticMetadata classHierarchy:self.classHierarchyClasses];
|
||||
|
||||
NSArray<NSArray<FLEXProperty *> *> *properties = _allProperties;
|
||||
NSArray<NSArray<FLEXProperty *> *> *properties = allProperties;
|
||||
|
||||
// Potentially filter property-backing ivars
|
||||
if (hideBackingIvars) {
|
||||
@@ -208,7 +214,7 @@
|
||||
NSSet *ivarNames = [NSSet setWithArray:({
|
||||
[properties[idx] flex_mapped:^id(FLEXProperty *p, NSUInteger idx) {
|
||||
// Nil if no ivar, and array is flatted
|
||||
return p.attributes.backingIvar;
|
||||
return p.likelyIvarName;
|
||||
}];
|
||||
})];
|
||||
|
||||
@@ -221,8 +227,7 @@
|
||||
|
||||
// Potentially filter property-backing methods
|
||||
if (hidePropertyMethods) {
|
||||
NSArray<NSArray<FLEXMethod *> *> *methods = _allMethods.copy;
|
||||
_allMethods = [methods flex_mapped:^id(NSArray<FLEXMethod *> *list, NSUInteger idx) {
|
||||
allMethods = [allMethods flex_mapped:^id(NSArray<FLEXMethod *> *list, NSUInteger idx) {
|
||||
// Get a set of all property method names for the current class in the hierarchy
|
||||
NSSet *methodNames = [NSSet setWithArray:({
|
||||
[properties[idx] flex_flatmapped:^NSArray *(FLEXProperty *p, NSUInteger idx) {
|
||||
@@ -240,12 +245,37 @@
|
||||
}];
|
||||
})];
|
||||
|
||||
// Remove ivars whose name is in the ivar names list
|
||||
// Remove methods whose name is in the property method names list
|
||||
return [list flex_filtered:^BOOL(FLEXMethod *method, NSUInteger idx) {
|
||||
return ![methodNames containsObject:method.selectorString];
|
||||
}];
|
||||
}];
|
||||
}
|
||||
|
||||
if (hidePrivateMethods) {
|
||||
id methodMapBlock = ^id(NSArray<FLEXMethod *> *list, NSUInteger idx) {
|
||||
// Remove methods which contain an underscore
|
||||
return [list flex_filtered:^BOOL(FLEXMethod *method, NSUInteger idx) {
|
||||
return ![method.selectorString containsString:@"_"];
|
||||
}];
|
||||
};
|
||||
id propertyMapBlock = ^id(NSArray<FLEXProperty *> *list, NSUInteger idx) {
|
||||
// Remove methods which contain an underscore
|
||||
return [list flex_filtered:^BOOL(FLEXProperty *prop, NSUInteger idx) {
|
||||
return ![prop.name containsString:@"_"];
|
||||
}];
|
||||
};
|
||||
|
||||
allMethods = [allMethods flex_mapped:methodMapBlock];
|
||||
allClassMethods = [allClassMethods flex_mapped:methodMapBlock];
|
||||
allProperties = [allProperties flex_mapped:propertyMapBlock];
|
||||
allClassProps = [allClassProps flex_mapped:propertyMapBlock];
|
||||
}
|
||||
|
||||
_allProperties = allProperties;
|
||||
_allClassProperties = allClassProps;
|
||||
_allMethods = allMethods;
|
||||
_allClassMethods = allClassMethods;
|
||||
|
||||
// Set up UIKit helper data
|
||||
// Really, we only need to call this on properties and ivars
|
||||
@@ -283,8 +313,8 @@
|
||||
- (NSArray *)metadataUniquedByName:(NSArray *)list
|
||||
superclass:(Class)superclass
|
||||
kind:(FLEXMetadataKind)kind
|
||||
skip:(BOOL)skip {
|
||||
if (skip) {
|
||||
skip:(BOOL)skipUniquing {
|
||||
if (skipUniquing) {
|
||||
return list;
|
||||
}
|
||||
|
||||
|
||||
@@ -17,17 +17,27 @@
|
||||
#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<Class, Class> *classesToRegisteredSections = nil;
|
||||
static NSMutableDictionary<id<NSCopying>, Class> *classesToRegisteredSections = nil;
|
||||
|
||||
+ (void)initialize {
|
||||
if (self == [FLEXObjectExplorerFactory class]) {
|
||||
#define ClassKey(name) (Class<NSCopying>)[name class]
|
||||
#define ClassKeyByName(str) (Class<NSCopying>)NSClassFromString(@ #str)
|
||||
#define MetaclassKey(meta) (Class<NSCopying>)object_getClass([meta class])
|
||||
// DO NOT USE STRING KEYS HERE
|
||||
// We NEED to use the class as a key, because we CANNOT
|
||||
// differentiate a class's name from the metaclass's name.
|
||||
// These mappings are per-class-object, not per-class-name.
|
||||
//
|
||||
// For example, if we used class names, this would result in
|
||||
// the object explorer trying to render a color preview for
|
||||
// the UIColor class object, which is not a color itself.
|
||||
#define ClassKey(name) (id<NSCopying>)[name class]
|
||||
#define ClassKeyByName(str) (id<NSCopying>)NSClassFromString(@ #str)
|
||||
#define MetaclassKey(meta) (id<NSCopying>)object_getClass([meta class])
|
||||
classesToRegisteredSections = [NSMutableDictionary dictionaryWithDictionary:@{
|
||||
MetaclassKey(NSObject) : [FLEXClassShortcuts class],
|
||||
ClassKey(NSArray) : [FLEXCollectionContentSection class],
|
||||
@@ -42,6 +52,8 @@ static NSMutableDictionary<Class, Class> *classesToRegisteredSections = nil;
|
||||
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
|
||||
@@ -67,7 +79,7 @@ static NSMutableDictionary<Class, Class> *classesToRegisteredSections = nil;
|
||||
Class sectionClass = nil;
|
||||
Class cls = object_getClass(object);
|
||||
do {
|
||||
sectionClass = classesToRegisteredSections[(Class<NSCopying>)cls];
|
||||
sectionClass = classesToRegisteredSections[(id<NSCopying>)cls];
|
||||
} while (!sectionClass && (cls = [cls superclass]));
|
||||
|
||||
if (!sectionClass) {
|
||||
@@ -81,7 +93,7 @@ static NSMutableDictionary<Class, Class> *classesToRegisteredSections = nil;
|
||||
}
|
||||
|
||||
+ (void)registerExplorerSection:(Class)explorerClass forClass:(Class)objectClass {
|
||||
classesToRegisteredSections[(Class<NSCopying>)objectClass] = explorerClass;
|
||||
classesToRegisteredSections[(id<NSCopying>)objectClass] = explorerClass;
|
||||
}
|
||||
|
||||
#pragma mark - FLEXGlobalsEntry
|
||||
@@ -176,7 +188,7 @@ static NSMutableDictionary<Class, Class> *classesToRegisteredSections = nil;
|
||||
return [self explorerViewControllerForObject:NSThread.mainThread];
|
||||
case FLEXGlobalsRowOperationQueue:
|
||||
return [self explorerViewControllerForObject:NSOperationQueue.mainQueue];
|
||||
|
||||
|
||||
case FLEXGlobalsRowKeyWindow:
|
||||
return [FLEXObjectExplorerFactory
|
||||
explorerViewControllerForObject:FLEXUtility.appKeyWindow
|
||||
|
||||
@@ -72,7 +72,8 @@
|
||||
return @[
|
||||
kFLEXDefaultsHidePropertyIvarsKey,
|
||||
kFLEXDefaultsHidePropertyMethodsKey,
|
||||
kFLEXDefaultsHideMethodOverridesKey,
|
||||
kFLEXDefaultsHidePrivateMethodsKey,
|
||||
kFLEXDefaultsShowMethodOverridesKey,
|
||||
kFLEXDefaultsHideVariablePreviewsKey,
|
||||
];
|
||||
}
|
||||
@@ -87,7 +88,7 @@
|
||||
|
||||
// Use [object class] here rather than object_getClass
|
||||
// to avoid the KVO prefix for observed objects
|
||||
self.title = [[self.object class] description];
|
||||
self.title = [FLEXRuntimeUtility safeClassNameForObject:self.object];
|
||||
|
||||
// Search
|
||||
self.showsSearchBar = YES;
|
||||
@@ -265,9 +266,11 @@
|
||||
- (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 ||
|
||||
g2 == self.navigationController.barHideOnSwipeGestureRecognizer ||
|
||||
g2 == self.tableView.panGestureRecognizer) {
|
||||
if (g2 == self.navigationController.interactivePopGestureRecognizer) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (g2 == self.tableView.panGestureRecognizer) {
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
@@ -291,7 +294,8 @@
|
||||
NSDictionary<NSString *, NSString *> *explorerToggles = @{
|
||||
kFLEXDefaultsHidePropertyIvarsKey: @"Property-Backing Ivars",
|
||||
kFLEXDefaultsHidePropertyMethodsKey: @"Property-Backing Methods",
|
||||
kFLEXDefaultsHideMethodOverridesKey: @"Method Overrides",
|
||||
kFLEXDefaultsHidePrivateMethodsKey: @"Likely Private Methods",
|
||||
kFLEXDefaultsShowMethodOverridesKey: @"Method Overrides",
|
||||
kFLEXDefaultsHideVariablePreviewsKey: @"Variable Previews"
|
||||
};
|
||||
|
||||
@@ -302,7 +306,8 @@
|
||||
NSDictionary<NSString *, NSDictionary *> *nextStateDescriptions = @{
|
||||
kFLEXDefaultsHidePropertyIvarsKey: @{ @NO: @"Hide ", @YES: @"Show " },
|
||||
kFLEXDefaultsHidePropertyMethodsKey: @{ @NO: @"Hide ", @YES: @"Show " },
|
||||
kFLEXDefaultsHideMethodOverridesKey: @{ @NO: @"Show ", @YES: @"Hide " },
|
||||
kFLEXDefaultsHidePrivateMethodsKey: @{ @NO: @"Hide ", @YES: @"Show " },
|
||||
kFLEXDefaultsShowMethodOverridesKey: @{ @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 *whitelistedDefaults;
|
||||
@property (nonatomic, readonly) NSDictionary *unexcludedDefaults;
|
||||
@end
|
||||
|
||||
@implementation FLEXDefaultsContentSection
|
||||
@@ -33,7 +33,7 @@
|
||||
FLEXDefaultsContentSection *section = [self forReusableFuture:^id(FLEXDefaultsContentSection *section) {
|
||||
section.defaults = userDefaults;
|
||||
section.onlyShowKeysForAppPrefs = YES;
|
||||
return section.whitelistedDefaults;
|
||||
return section.unexcludedDefaults;
|
||||
}];
|
||||
return section;
|
||||
}
|
||||
@@ -87,16 +87,16 @@
|
||||
_keys = [keys sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)];
|
||||
}
|
||||
|
||||
- (NSDictionary *)whitelistedDefaults {
|
||||
// Case: no whitelisting
|
||||
- (NSDictionary *)unexcludedDefaults {
|
||||
// Case: no excluding
|
||||
if (!self.onlyShowKeysForAppPrefs) {
|
||||
return self.defaults.dictionaryRepresentation;
|
||||
}
|
||||
|
||||
// Always regenerate key whitelist when this method is called
|
||||
// Always regenerate key allowlist when this method is called
|
||||
_keys = nil;
|
||||
|
||||
// Generate new dictionary from whitelisted keys
|
||||
// Generate new dictionary from unexcluded keys
|
||||
NSArray *values = [self.defaults.dictionaryRepresentation
|
||||
objectsForKeys:self.keys notFoundMarker:NSNull.null
|
||||
];
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
#import "FLEXMutableListSection.h"
|
||||
#import "FLEXMacros.h"
|
||||
|
||||
@interface FLEXMutableListSection ()
|
||||
@property (nonatomic, readonly) FLEXMutableListCellForElement configureCell;
|
||||
@@ -78,12 +79,10 @@ configurationBlock:(FLEXMutableListCellForElement)cellConfig
|
||||
}
|
||||
|
||||
- (void (^)(__kindof UIViewController *))didSelectRowAction:(NSInteger)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]);
|
||||
if (self.selectionHandler) { weakify(self)
|
||||
return ^(UIViewController *host) { strongify(self)
|
||||
if (self) {
|
||||
self.selectionHandler(host, self.filteredList[row]);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#import "FLEXBundleShortcuts.h"
|
||||
#import "FLEXShortcut.h"
|
||||
#import "FLEXAlert.h"
|
||||
#import "FLEXMacros.h"
|
||||
#import "FLEXRuntimeExporter.h"
|
||||
#import "FLEXTableListViewController.h"
|
||||
#import "FLEXFileBrowserController.h"
|
||||
@@ -17,8 +18,7 @@
|
||||
@implementation FLEXBundleShortcuts
|
||||
#pragma mark Overrides
|
||||
|
||||
+ (instancetype)forObject:(NSBundle *)bundle {
|
||||
__weak __typeof(self) weakSelf = self;
|
||||
+ (instancetype)forObject:(NSBundle *)bundle { weakify(self)
|
||||
return [self forObject:bundle additionalRows:@[
|
||||
[FLEXActionShortcut
|
||||
title:@"Browse Bundle Directory" subtitle:nil
|
||||
@@ -30,11 +30,8 @@
|
||||
}
|
||||
],
|
||||
[FLEXActionShortcut title:@"Browse Bundle as Database…" subtitle:nil
|
||||
selectionHandler:^(UIViewController *host, NSBundle *bundle) {
|
||||
__strong __typeof(self) strongSelf = weakSelf;
|
||||
if (strongSelf) {
|
||||
[strongSelf promptToExportBundleAsDatabase:bundle host:host];
|
||||
}
|
||||
selectionHandler:^(UIViewController *host, NSBundle *bundle) { strongify(self)
|
||||
[self promptToExportBundleAsDatabase:bundle host:host];
|
||||
}
|
||||
accessoryType:^UITableViewCellAccessoryType(NSBundle *bundle) {
|
||||
return UITableViewCellAccessoryDisclosureIndicator;
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#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 *(id layer) {
|
||||
viewer:^UIViewController *(CALayer *layer) {
|
||||
return [FLEXImagePreviewViewController previewForLayer:layer];
|
||||
}
|
||||
accessoryType:^UITableViewCellAccessoryType(id layer) {
|
||||
return UITableViewCellAccessoryDisclosureIndicator;
|
||||
accessoryType:^UITableViewCellAccessoryType(CALayer *layer) {
|
||||
return CGRectIsEmpty(layer.bounds) ? UITableViewCellAccessoryNone : UITableViewCellAccessoryDisclosureIndicator;
|
||||
}
|
||||
]
|
||||
]];
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// FLEXNSDataShortcuts.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 3/29/21.
|
||||
//
|
||||
|
||||
#import "FLEXShortcutsSection.h"
|
||||
|
||||
/// Adds a "UTF-8 String" shortcut
|
||||
@interface FLEXNSDataShortcuts : FLEXShortcutsSection
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,48 @@
|
||||
//
|
||||
// FLEXNSDataShortcuts.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 3/29/21.
|
||||
//
|
||||
|
||||
#import "FLEXNSDataShortcuts.h"
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
#import "FLEXShortcut.h"
|
||||
|
||||
@implementation FLEXNSDataShortcuts
|
||||
|
||||
+ (instancetype)forObject:(NSData *)data {
|
||||
NSString *string = [self stringForData:data];
|
||||
|
||||
return [self forObject:data additionalRows:@[
|
||||
[FLEXActionShortcut title:@"UTF-8 String" subtitle:^(NSData *object) {
|
||||
return string.length ? string : (string ?
|
||||
@"Data is not a UTF8 String" : @"Empty string"
|
||||
);
|
||||
} viewer:^UIViewController *(id object) {
|
||||
return [FLEXObjectExplorerFactory explorerViewControllerForObject:string];
|
||||
} accessoryType:^UITableViewCellAccessoryType(NSData *object) {
|
||||
if (string.length) {
|
||||
return UITableViewCellAccessoryDisclosureIndicator;
|
||||
}
|
||||
|
||||
return UITableViewCellAccessoryNone;
|
||||
}]
|
||||
]];
|
||||
}
|
||||
|
||||
+ (NSString *)stringForData:(NSData *)data {
|
||||
return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface NSData (Overrides) @end
|
||||
@implementation NSData (Overrides)
|
||||
|
||||
// This normally crashes
|
||||
- (NSUInteger)length {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// FLEXNSStringShortcuts.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 3/29/21.
|
||||
//
|
||||
|
||||
#import "FLEXShortcutsSection.h"
|
||||
|
||||
/// Adds a "UTF-8 Data" shortcut
|
||||
@interface FLEXNSStringShortcuts : FLEXShortcutsSection
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,29 @@
|
||||
//
|
||||
// FLEXNSStringShortcuts.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 3/29/21.
|
||||
//
|
||||
|
||||
#import "FLEXNSStringShortcuts.h"
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
#import "FLEXShortcut.h"
|
||||
|
||||
@implementation FLEXNSStringShortcuts
|
||||
|
||||
+ (instancetype)forObject:(NSString *)string {
|
||||
NSUInteger length = [string lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
|
||||
NSData *data = [NSData dataWithBytesNoCopy:(void *)string.UTF8String length:length freeWhenDone:NO];
|
||||
|
||||
return [self forObject:string additionalRows:@[
|
||||
[FLEXActionShortcut title:@"UTF-8 Data" subtitle:^NSString *(id _) {
|
||||
return data.description;
|
||||
} viewer:^UIViewController *(id _) {
|
||||
return [FLEXObjectExplorerFactory explorerViewControllerForObject:data];
|
||||
} accessoryType:^UITableViewCellAccessoryType(id _) {
|
||||
return UITableViewCellAccessoryDisclosureIndicator;
|
||||
}]
|
||||
]];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -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;
|
||||
- (UIViewController *)editorWith:(id)object forSection:(FLEXTableViewSection *)section;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -8,8 +8,10 @@
|
||||
|
||||
#import "FLEXShortcutsFactory+Defaults.h"
|
||||
#import "FLEXShortcut.h"
|
||||
#import "FLEXMacros.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
#import "NSObject+FLEX_Reflection.h"
|
||||
#import "Cocoa+FLEXShortcuts.h"
|
||||
|
||||
#pragma mark - UIApplication
|
||||
|
||||
@@ -59,6 +61,7 @@
|
||||
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));
|
||||
@@ -141,7 +144,26 @@
|
||||
@"viewIfLoaded", @"title", @"navigationItem", @"toolbarItems", @"tabBarItem",
|
||||
@"childViewControllers", @"navigationController", @"tabBarController", @"splitViewController",
|
||||
@"parentViewController", @"presentedViewController", @"presentingViewController",
|
||||
]).methods(@[@"view"]).forClass(UIViewController.class);
|
||||
])
|
||||
.methods(@[@"view"])
|
||||
.forClass(UIViewController.class);
|
||||
|
||||
// UIAlertController
|
||||
NSMutableArray *alertControllerProps = @[
|
||||
@"title", @"message", @"actions", @"textFields",
|
||||
@"preferredAction", @"presentingViewController", @"viewIfLoaded",
|
||||
].mutableCopy;
|
||||
if (@available(iOS 14.0, *)) {
|
||||
[alertControllerProps insertObject:@"image" atIndex:4];
|
||||
}
|
||||
self.append
|
||||
.properties(alertControllerProps)
|
||||
.methods(@[@"addAction:"])
|
||||
.forClass(UIAlertController.class);
|
||||
self.append.properties(@[
|
||||
@"title", @"style", @"enabled", @"flex_styleName",
|
||||
@"image", @"keyCommandInput", @"_isPreferred", @"_alertController",
|
||||
]).forClass(UIAlertAction.class);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -256,12 +278,18 @@
|
||||
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"
|
||||
]).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.properties(@[@"length", @"bytes"]).forClass(NSData.class);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -201,10 +201,10 @@
|
||||
|
||||
- (void (^)(__kindof UIViewController *))didPressInfoButtonAction:(NSInteger)row {
|
||||
id<FLEXShortcut> shortcut = self.shortcuts[row];
|
||||
if ([shortcut respondsToSelector:@selector(editorWith:)]) {
|
||||
if ([shortcut respondsToSelector:@selector(editorWith:forSection:)]) {
|
||||
id object = self.object;
|
||||
return ^(UIViewController *host) {
|
||||
UIViewController *editor = [shortcut editorWith:object];
|
||||
UIViewController *editor = [shortcut editorWith:object forSection:self];
|
||||
[host.navigationController pushViewController:editor animated:YES];
|
||||
};
|
||||
}
|
||||
@@ -255,31 +255,52 @@
|
||||
}
|
||||
@end
|
||||
|
||||
#define NewAndSet(ivar) ({ FLEXShortcutsFactory *r = [self new]; r->ivar = YES; r; })
|
||||
#define NewAndSet(ivar) ({ FLEXShortcutsFactory *r = [self sharedFactory]; r->ivar = YES; r; })
|
||||
#define SetIvar(ivar) ({ self->ivar = YES; self; })
|
||||
#define SetParamBlock(ivar) ^(NSArray *p) { self->ivar = p; return self; }
|
||||
|
||||
@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;
|
||||
|
||||
+ (void)load {
|
||||
cProperties = [NSMutableDictionary new];
|
||||
cIvars = [NSMutableDictionary new];
|
||||
cMethods = [NSMutableDictionary new];
|
||||
@implementation FLEXShortcutsFactory {
|
||||
// Class buckets
|
||||
RegistrationBuckets *cProperties;
|
||||
RegistrationBuckets *cIvars;
|
||||
RegistrationBuckets *cMethods;
|
||||
// Metaclass buckets
|
||||
RegistrationBuckets *mProperties;
|
||||
RegistrationBuckets *mMethods;
|
||||
}
|
||||
|
||||
mProperties = [NSMutableDictionary new];
|
||||
mMethods = [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;
|
||||
}
|
||||
|
||||
+ (NSArray<id<FLEXRuntimeMetadata>> *)shortcutsForObjectOrClass:(id)objectOrClass {
|
||||
return [[self sharedFactory] shortcutsForObjectOrClass:objectOrClass];
|
||||
}
|
||||
|
||||
- (NSArray<id<FLEXRuntimeMetadata>> *)shortcutsForObjectOrClass:(id)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
|
||||
@@ -325,30 +346,43 @@ static RegistrationBuckets *mMethods = nil;
|
||||
}
|
||||
|
||||
- (void)_register:(NSArray<id<FLEXRuntimeMetadata>> *)items to:(RegistrationBuckets *)global class:(Class)key {
|
||||
// Get (or initialize) the bucket for this class
|
||||
NSMutableArray *bucket = ({
|
||||
id bucket = global[key];
|
||||
if (!bucket) {
|
||||
bucket = [NSMutableArray new];
|
||||
global[(id)key] = bucket;
|
||||
}
|
||||
bucket;
|
||||
});
|
||||
@synchronized (self) {
|
||||
// Get (or initialize) the bucket for this class
|
||||
NSMutableArray *bucket = ({
|
||||
id bucket = global[key];
|
||||
if (!bucket) {
|
||||
bucket = [NSMutableArray new];
|
||||
global[(id)key] = bucket;
|
||||
}
|
||||
bucket;
|
||||
});
|
||||
|
||||
if (self->_append) { [bucket addObjectsFromArray:items]; }
|
||||
if (self->_replace) { [bucket setArray:items]; }
|
||||
if (self->_prepend) {
|
||||
if (bucket.count) {
|
||||
// Set new items as array, add old items behind them
|
||||
id copy = bucket.copy;
|
||||
[bucket setArray:items];
|
||||
[bucket addObjectsFromArray:copy];
|
||||
} else {
|
||||
[bucket addObjectsFromArray:items];
|
||||
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);
|
||||
}
|
||||
@@ -405,9 +439,15 @@ static RegistrationBuckets *mMethods = nil;
|
||||
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) {
|
||||
@@ -430,6 +470,8 @@ static RegistrationBuckets *mMethods = nil;
|
||||
}];
|
||||
[self _register:items to:ivarBucket class:cls];
|
||||
}
|
||||
|
||||
[self reset];
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -70,15 +70,18 @@
|
||||
return [FLEXObjectExplorerFactory explorerViewControllerForObject:controller];
|
||||
}
|
||||
accessoryType:^UITableViewCellAccessoryType(id view) {
|
||||
return controller ? UITableViewCellAccessoryDisclosureIndicator : 0;
|
||||
return controller ? UITableViewCellAccessoryDisclosureIndicator : UITableViewCellAccessoryNone;
|
||||
}
|
||||
],
|
||||
[FLEXActionShortcut title:@"Preview Image" subtitle:nil
|
||||
viewer:^UIViewController *(id view) {
|
||||
[FLEXActionShortcut title:@"Preview Image" subtitle:^NSString *(UIView *view) {
|
||||
return !CGRectIsEmpty(view.bounds) ? @"" : @"Unavailable with empty bounds";
|
||||
}
|
||||
viewer:^UIViewController *(UIView *view) {
|
||||
return [FLEXImagePreviewViewController previewForView:view];
|
||||
}
|
||||
accessoryType:^UITableViewCellAccessoryType(id view) {
|
||||
return UITableViewCellAccessoryDisclosureIndicator;
|
||||
accessoryType:^UITableViewCellAccessoryType(UIView *view) {
|
||||
// Disable preview if bounds are CGRectZero
|
||||
return !CGRectIsEmpty(view.bounds) ? UITableViewCellAccessoryDisclosureIndicator : UITableViewCellAccessoryNone;
|
||||
}
|
||||
]
|
||||
]];
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
#import "FLEXFieldEditorViewController.h"
|
||||
#import "FLEXMethodCallingViewController.h"
|
||||
#import "FLEXObjectListViewController.h"
|
||||
#import "FLEXTableView.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "NSArray+FLEX.h"
|
||||
@@ -131,15 +132,37 @@ FLEXObjectExplorerDefaultsImpl
|
||||
#if FLEX_AT_LEAST_IOS13_SDK
|
||||
|
||||
- (NSArray<UIAction *> *)additionalActionsWithTarget:(id)object sender:(UIViewController *)sender __IOS_AVAILABLE(13.0) {
|
||||
Class propertyClass = self.attributes.typeEncoding.flex_typeClass;
|
||||
BOOL returnsObject = self.attributes.typeEncoding.flex_typeIsObjectOrClass;
|
||||
BOOL targetNotNil = [self appropriateTargetForPropertyType:object] != nil;
|
||||
|
||||
// "Explore PropertyClass" for properties with a concrete class name
|
||||
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];
|
||||
}]];
|
||||
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;
|
||||
}
|
||||
|
||||
return nil;
|
||||
|
||||
@@ -27,6 +27,12 @@
|
||||
+ (instancetype)flex_forEachUpTo:(NSUInteger)bound map:(T(^)(NSUInteger i))block;
|
||||
+ (instancetype)flex_mapped:(id<NSFastEnumeration>)collection block:(id(^)(T obj, NSUInteger idx))mapFunc;
|
||||
|
||||
- (instancetype)sortedUsingSelector:(SEL)selector;
|
||||
- (instancetype)flex_sortedUsingSelector:(SEL)selector;
|
||||
|
||||
@end
|
||||
|
||||
@interface NSMutableArray<T> (Functional)
|
||||
|
||||
- (void)flex_filter:(BOOL(^)(T obj, NSUInteger idx))filterFunc;
|
||||
|
||||
@end
|
||||
|
||||
@@ -85,7 +85,6 @@
|
||||
return array;
|
||||
}
|
||||
|
||||
|
||||
+ (instancetype)flex_mapped:(id<NSFastEnumeration>)collection block:(id(^)(id obj, NSUInteger idx))mapFunc {
|
||||
NSMutableArray *array = [NSMutableArray new];
|
||||
NSInteger idx = 0;
|
||||
@@ -104,7 +103,7 @@
|
||||
return array;
|
||||
}
|
||||
|
||||
- (instancetype)sortedUsingSelector:(SEL)selector {
|
||||
- (instancetype)flex_sortedUsingSelector:(SEL)selector {
|
||||
if (FLEXArrayClassIsMutable(self)) {
|
||||
NSMutableArray *me = (id)self;
|
||||
[me sortUsingSelector:selector];
|
||||
@@ -115,3 +114,20 @@
|
||||
}
|
||||
|
||||
@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> *FLEXGetAllSubclasses(_Nullable Class cls, BOOL includeSelf);
|
||||
NSArray<Class> *FLEXGetClassHierarchy(_Nullable Class cls, BOOL includeSelf);
|
||||
NSArray<FLEXProtocol *> *FLEXGetConformedProtocols(_Nullable Class cls);
|
||||
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<FLEXIvar *> *FLEXGetAllIvars(_Nullable Class cls);
|
||||
NSArray<FLEXIvar *> * _Nullable FLEXGetAllIvars(_Nullable Class cls);
|
||||
/// @param cls a class object to get instance properties,
|
||||
/// or a metaclass object to get class properties
|
||||
NSArray<FLEXProperty *> *FLEXGetAllProperties(_Nullable Class cls);
|
||||
NSArray<FLEXProperty *> * _Nullable 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 *> *FLEXGetAllMethods(_Nullable Class cls, BOOL instance);
|
||||
NSArray<FLEXMethod *> * _Nullable FLEXGetAllMethods(_Nullable Class cls, BOOL instance);
|
||||
/// @param cls a class object to get all instance and class methods.
|
||||
NSArray<FLEXMethod *> *FLEXGetAllInstanceAndClassMethods(_Nullable Class cls);
|
||||
NSArray<FLEXMethod *> * _Nullable FLEXGetAllInstanceAndClassMethods(_Nullable Class cls);
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
|
||||
NSString * FLEXTypeEncodingString(const char *returnType, NSUInteger count, ...) {
|
||||
if (returnType == NULL) return nil;
|
||||
if (!returnType) return nil;
|
||||
|
||||
NSMutableString *encoding = [NSMutableString new];
|
||||
[encoding appendFormat:@"%s%s%s", returnType, @encode(id), @encode(SEL)];
|
||||
@@ -37,9 +37,7 @@ 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;
|
||||
|
||||
@@ -71,9 +69,7 @@ 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) {
|
||||
@@ -88,13 +84,12 @@ 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];
|
||||
@@ -246,7 +241,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 {
|
||||
@@ -394,7 +389,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 {
|
||||
|
||||
@@ -11,7 +11,7 @@ typedef void (^VoidBlock)(void);
|
||||
|
||||
@interface NSTimer (Blocks)
|
||||
|
||||
+ (instancetype)fireSecondsFromNow:(NSTimeInterval)delay block:(VoidBlock)block;
|
||||
+ (instancetype)flex_fireSecondsFromNow:(NSTimeInterval)delay block:(VoidBlock)block;
|
||||
|
||||
// Forward declaration
|
||||
//+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
#pragma clang diagnostic ignored "-Wincomplete-implementation"
|
||||
@implementation NSTimer (Blocks)
|
||||
|
||||
+ (instancetype)fireSecondsFromNow:(NSTimeInterval)delay block:(VoidBlock)block {
|
||||
+ (instancetype)flex_fireSecondsFromNow:(NSTimeInterval)delay block:(VoidBlock)block {
|
||||
if (@available(iOS 10, *)) {
|
||||
return [self scheduledTimerWithTimeInterval:delay repeats:NO block:(id)block];
|
||||
} else {
|
||||
|
||||
@@ -13,9 +13,10 @@ extern NSString * const kFLEXDefaultsToolbarTopMarginKey;
|
||||
extern NSString * const kFLEXDefaultsiOSPersistentOSLogKey;
|
||||
extern NSString * const kFLEXDefaultsHidePropertyIvarsKey;
|
||||
extern NSString * const kFLEXDefaultsHidePropertyMethodsKey;
|
||||
extern NSString * const kFLEXDefaultsHideMethodOverridesKey;
|
||||
extern NSString * const kFLEXDefaultsHidePrivateMethodsKey;
|
||||
extern NSString * const kFLEXDefaultsShowMethodOverridesKey;
|
||||
extern NSString * const kFLEXDefaultsHideVariablePreviewsKey;
|
||||
extern NSString * const kFLEXDefaultsNetworkHostBlacklistKey;
|
||||
extern NSString * const kFLEXDefaultsNetworkHostDenylistKey;
|
||||
extern NSString * const kFLEXDefaultsDisableOSLogForceASLKey;
|
||||
extern NSString * const kFLEXDefaultsRegisterJSONExplorerKey;
|
||||
|
||||
@@ -27,7 +28,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_networkHostBlacklist;
|
||||
@property (nonatomic) NSArray<NSString *> *flex_networkHostDenylist;
|
||||
|
||||
/// Whether or not to register the object explorer as a JSON viewer on launch
|
||||
@property (nonatomic) BOOL flex_registerDictionaryJSONViewerOnLaunch;
|
||||
@@ -38,6 +39,7 @@ 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,9 +12,10 @@ 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 kFLEXDefaultsHideMethodOverridesKey = @"com.flipboard.FLEX.hide_method_overrides";
|
||||
NSString * const kFLEXDefaultsHidePrivateMethodsKey = @"com.flipboard.FLEX.hide_private_or_namespaced_methods";
|
||||
NSString * const kFLEXDefaultsShowMethodOverridesKey = @"com.flipboard.FLEX.show_method_overrides";
|
||||
NSString * const kFLEXDefaultsHideVariablePreviewsKey = @"com.flipboard.FLEX.hide_variable_previews";
|
||||
NSString * const kFLEXDefaultsNetworkHostBlacklistKey = @"com.flipboard.FLEX.network_host_blacklist";
|
||||
NSString * const kFLEXDefaultsNetworkHostDenylistKey = @"com.flipboard.FLEX.network_host_denylist";
|
||||
NSString * const kFLEXDefaultsDisableOSLogForceASLKey = @"com.flipboard.FLEX.try_disable_os_log";
|
||||
NSString * const kFLEXDefaultsRegisterJSONExplorerKey = @"com.flipboard.FLEX.view_json_as_object";
|
||||
|
||||
@@ -61,16 +62,16 @@ NSString * const kFLEXDefaultsRegisterJSONExplorerKey = @"com.flipboard.FLEX.vie
|
||||
[self setDouble:margin forKey:kFLEXDefaultsToolbarTopMarginKey];
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)flex_networkHostBlacklist {
|
||||
- (NSArray<NSString *> *)flex_networkHostDenylist {
|
||||
return [NSArray arrayWithContentsOfFile:[
|
||||
self flex_defaultsPathForFile:kFLEXDefaultsNetworkHostBlacklistKey
|
||||
self flex_defaultsPathForFile:kFLEXDefaultsNetworkHostDenylistKey
|
||||
]] ?: @[];
|
||||
}
|
||||
|
||||
- (void)setFlex_networkHostBlacklist:(NSArray<NSString *> *)blacklist {
|
||||
NSParameterAssert(blacklist);
|
||||
[blacklist writeToFile:[
|
||||
self flex_defaultsPathForFile:kFLEXDefaultsNetworkHostBlacklistKey
|
||||
- (void)setFlex_networkHostDenylist:(NSArray<NSString *> *)denylist {
|
||||
NSParameterAssert(denylist);
|
||||
[denylist writeToFile:[
|
||||
self flex_defaultsPathForFile:kFLEXDefaultsNetworkHostDenylistKey
|
||||
] atomically:YES];
|
||||
}
|
||||
|
||||
@@ -130,14 +131,26 @@ 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:kFLEXDefaultsHideMethodOverridesKey];
|
||||
return [self boolForKey:kFLEXDefaultsShowMethodOverridesKey];
|
||||
}
|
||||
|
||||
- (void)setFlex_explorerShowsMethodOverrides:(BOOL)show {
|
||||
[self setBool:show forKey:kFLEXDefaultsHideMethodOverridesKey];
|
||||
[self setBool:show forKey:kFLEXDefaultsShowMethodOverridesKey];
|
||||
[NSNotificationCenter.defaultCenter
|
||||
postNotificationName:kFLEXDefaultsHideMethodOverridesKey
|
||||
postNotificationName:kFLEXDefaultsShowMethodOverridesKey
|
||||
object:nil
|
||||
];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// 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
|
||||
@@ -0,0 +1,25 @@
|
||||
//
|
||||
// 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
|
||||
+2
-2
@@ -27,7 +27,7 @@
|
||||
|
||||
@interface NSString (KeyPaths)
|
||||
|
||||
- (NSString *)stringByRemovingLastKeyPathComponent;
|
||||
- (NSString *)stringByReplacingLastKeyPathComponent:(NSString *)replacement;
|
||||
- (NSString *)flex_stringByRemovingLastKeyPathComponent;
|
||||
- (NSString *)flex_stringByReplacingLastKeyPathComponent:(NSString *)replacement;
|
||||
|
||||
@end
|
||||
+2
-2
@@ -128,7 +128,7 @@
|
||||
|
||||
@implementation NSString (KeyPaths)
|
||||
|
||||
- (NSString *)stringByRemovingLastKeyPathComponent {
|
||||
- (NSString *)flex_stringByRemovingLastKeyPathComponent {
|
||||
if (![self containsString:@"."]) {
|
||||
return @"";
|
||||
}
|
||||
@@ -138,7 +138,7 @@
|
||||
return mself;
|
||||
}
|
||||
|
||||
- (NSString *)stringByReplacingLastKeyPathComponent:(NSString *)replacement {
|
||||
- (NSString *)flex_stringByReplacingLastKeyPathComponent:(NSString *)replacement {
|
||||
// replacement should not have any escaped '.' in it,
|
||||
// so we escape all '.'
|
||||
if ([replacement containsString:@"."]) {
|
||||
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// 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
|
||||
+9
-9
@@ -10,14 +10,14 @@
|
||||
|
||||
@implementation UIView (FLEX_Layout)
|
||||
|
||||
- (void)centerInView:(UIView *)view {
|
||||
- (void)flex_centerInView:(UIView *)view {
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
[self.centerXAnchor constraintEqualToAnchor:view.centerXAnchor],
|
||||
[self.centerYAnchor constraintEqualToAnchor:view.centerYAnchor],
|
||||
]];
|
||||
}
|
||||
|
||||
- (void)pinEdgesTo:(UIView *)view {
|
||||
- (void)flex_pinEdgesTo:(UIView *)view {
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
[self.topAnchor constraintEqualToAnchor:view.topAnchor],
|
||||
[self.leftAnchor constraintEqualToAnchor:view.leftAnchor],
|
||||
@@ -26,7 +26,7 @@
|
||||
]];
|
||||
}
|
||||
|
||||
- (void)pinEdgesTo:(UIView *)view withInsets:(UIEdgeInsets)i {
|
||||
- (void)flex_pinEdgesTo:(UIView *)view withInsets:(UIEdgeInsets)i {
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
[self.topAnchor constraintEqualToAnchor:view.topAnchor constant:i.top],
|
||||
[self.leftAnchor constraintEqualToAnchor:view.leftAnchor constant:i.left],
|
||||
@@ -35,15 +35,15 @@
|
||||
]];
|
||||
}
|
||||
|
||||
- (void)pinEdgesToSuperview {
|
||||
[self pinEdgesTo:self.superview];
|
||||
- (void)flex_pinEdgesToSuperview {
|
||||
[self flex_pinEdgesTo:self.superview];
|
||||
}
|
||||
|
||||
- (void)pinEdgesToSuperviewWithInsets:(UIEdgeInsets)insets {
|
||||
[self pinEdgesTo:self.superview withInsets:insets];
|
||||
- (void)flex_pinEdgesToSuperviewWithInsets:(UIEdgeInsets)insets {
|
||||
[self flex_pinEdgesTo:self.superview withInsets:insets];
|
||||
}
|
||||
|
||||
- (void)pinEdgesToSuperviewWithInsets:(UIEdgeInsets)i aboveView:(UIView *)sibling {
|
||||
- (void)flex_pinEdgesToSuperviewWithInsets:(UIEdgeInsets)i aboveView:(UIView *)sibling {
|
||||
UIView *view = self.superview;
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
[self.topAnchor constraintEqualToAnchor:view.topAnchor constant:i.top],
|
||||
@@ -53,7 +53,7 @@
|
||||
]];
|
||||
}
|
||||
|
||||
- (void)pinEdgesToSuperviewWithInsets:(UIEdgeInsets)i belowView:(UIView *)sibling {
|
||||
- (void)flex_pinEdgesToSuperviewWithInsets:(UIEdgeInsets)i belowView:(UIView *)sibling {
|
||||
UIView *view = self.superview;
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
[self.topAnchor constraintEqualToAnchor:sibling.bottomAnchor constant:i.top],
|
||||
@@ -64,7 +64,7 @@
|
||||
return item;
|
||||
}
|
||||
|
||||
- (UIBarButtonItem *)withTintColor:(UIColor *)tint {
|
||||
- (UIBarButtonItem *)flex_withTintColor:(UIColor *)tint {
|
||||
self.tintColor = tint;
|
||||
return self;
|
||||
}
|
||||
|
||||
@@ -13,9 +13,9 @@ typedef void (^GestureBlock)(UIGestureRecognizer *gesture);
|
||||
|
||||
@interface UIGestureRecognizer (Blocks)
|
||||
|
||||
+ (instancetype)action:(GestureBlock)action;
|
||||
+ (instancetype)flex_action:(GestureBlock)action;
|
||||
|
||||
@property (nonatomic) GestureBlock action;
|
||||
@property (nonatomic, setter=flex_setAction:) GestureBlock flex_action;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -14,22 +14,22 @@
|
||||
|
||||
static void * actionKey;
|
||||
|
||||
+ (instancetype)action:(GestureBlock)action {
|
||||
+ (instancetype)flex_action:(GestureBlock)action {
|
||||
UIGestureRecognizer *gesture = [[self alloc] initWithTarget:nil action:nil];
|
||||
[gesture addTarget:gesture action:@selector(flex_invoke)];
|
||||
gesture.action = action;
|
||||
gesture.flex_action = action;
|
||||
return gesture;
|
||||
}
|
||||
|
||||
- (void)flex_invoke {
|
||||
self.action(self);
|
||||
self.flex_action(self);
|
||||
}
|
||||
|
||||
- (GestureBlock)action {
|
||||
- (GestureBlock)flex_action {
|
||||
return objc_getAssociatedObject(self, &actionKey);
|
||||
}
|
||||
|
||||
- (void)setAction:(GestureBlock)action {
|
||||
- (void)flex_setAction:(GestureBlock)action {
|
||||
objc_setAssociatedObject(self, &actionKey, action, OBJC_ASSOCIATION_COPY);
|
||||
}
|
||||
|
||||
|
||||
@@ -11,16 +11,21 @@
|
||||
@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 selectedRange;
|
||||
@property (nonatomic, readonly) NSRange flex_selectedRange;
|
||||
|
||||
@end
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
@implementation UITextField (Range)
|
||||
|
||||
- (NSRange)selectedRange {
|
||||
- (NSRange)flex_selectedRange {
|
||||
UITextRange *r = self.selectedTextRange;
|
||||
if (r) {
|
||||
NSInteger loc = [self offsetFromPosition:self.beginningOfDocument toPosition:r.start];
|
||||
|
||||
@@ -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)centerInView:(UIView *)view;
|
||||
- (void)pinEdgesTo:(UIView *)view;
|
||||
- (void)pinEdgesTo:(UIView *)view withInsets:(UIEdgeInsets)insets;
|
||||
- (void)pinEdgesToSuperview;
|
||||
- (void)pinEdgesToSuperviewWithInsets:(UIEdgeInsets)insets;
|
||||
- (void)pinEdgesToSuperviewWithInsets:(UIEdgeInsets)insets aboveView:(UIView *)sibling;
|
||||
- (void)pinEdgesToSuperviewWithInsets:(UIEdgeInsets)insets belowView:(UIView *)sibling;
|
||||
|
||||
@end
|
||||
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
#import "FLEXAlert.h"
|
||||
#import "FLEXMacros.h"
|
||||
|
||||
@interface FLEXAlert ()
|
||||
@property (nonatomic, readonly) UIAlertController *_controller;
|
||||
@@ -203,10 +204,9 @@ NSAssert(!self._action, @"Cannot mutate action after retreiving underlying UIAle
|
||||
FLEXAlertActionMutationAssertion();
|
||||
|
||||
// Get weak reference to the alert to avoid block <--> alert retain cycle
|
||||
__weak __typeof(self._controller) weakController = self._controller;
|
||||
self._handler = ^(UIAlertAction *action) {
|
||||
UIAlertController *controller = self._controller; weakify(controller)
|
||||
self._handler = ^(UIAlertAction *action) { strongify(controller)
|
||||
// Strongify that reference and pass the text field strings to the handler
|
||||
__strong __typeof(weakController) controller = weakController;
|
||||
NSArray *strings = [controller.textFields valueForKeyPath:@"text"];
|
||||
handler(strings);
|
||||
};
|
||||
|
||||
@@ -13,11 +13,19 @@
|
||||
#define ctor flex_keywordify __attribute__((constructor)) void __flex_ctor_##__LINE__()
|
||||
#define dtor flex_keywordify __attribute__((destructor)) void __flex_dtor_##__LINE__()
|
||||
|
||||
#define weakify(var) __weak __typeof(var) __weak__##var = var;
|
||||
|
||||
#define strongify(var) \
|
||||
_Pragma("clang diagnostic push") \
|
||||
_Pragma("clang diagnostic ignored \"-Wshadow\"") \
|
||||
__strong typeof(var) var = __weak__##var; \
|
||||
_Pragma("clang diagnostic pop")
|
||||
|
||||
// A macro to check if we are running in a test environment
|
||||
#define FLEX_IS_TESTING() (NSClassFromString(@"XCTest") != nil)
|
||||
|
||||
/// Whether we want the majority of constructors to run upon load or not.
|
||||
extern BOOL FLEXConstructorsShouldRun();
|
||||
extern BOOL FLEXConstructorsShouldRun(void);
|
||||
|
||||
/// A macro to return from the current procedure if we don't want to run constructors
|
||||
#define FLEX_EXIT_IF_NO_CTORS() if (!FLEXConstructorsShouldRun()) return;
|
||||
|
||||
@@ -52,7 +52,8 @@
|
||||
|
||||
#pragma mark - Misc Icons
|
||||
|
||||
@property(readonly, class) UIImage *checkerPattern;
|
||||
@property(readonly, class) UIImage *hierarchyIndentPattern;
|
||||
@property (readonly, class) UIImage *checkerPattern;
|
||||
@property (readonly, class) UIColor *checkerPatternColor;
|
||||
@property (readonly, class) UIImage *hierarchyIndentPattern;
|
||||
|
||||
@end
|
||||
|
||||
@@ -8838,6 +8838,10 @@ static const u_int8_t FLEXHierarchyIndentPattern3x[] = {
|
||||
return FLEXImage(FLEXCheckerPattern);
|
||||
}
|
||||
|
||||
+ (UIColor *)checkerPatternColor {
|
||||
return [UIColor colorWithPatternImage:FLEXResources.checkerPattern];
|
||||
}
|
||||
|
||||
+ (UIImage *)hierarchyIndentPattern {
|
||||
return FLEXImageTemplate(FLEXHierarchyIndentPattern);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
#import "FLEXAlert.h"
|
||||
#import "NSArray+FLEX.h"
|
||||
#import "UIFont+FLEX.h"
|
||||
#import "NSMapTable+FLEX_Subscripting.h"
|
||||
#import "FLEXMacros.h"
|
||||
|
||||
#if !FLEX_AT_LEAST_IOS13_SDK
|
||||
|
||||
@@ -135,7 +135,7 @@ BOOL FLEXConstructorsShouldRun() {
|
||||
|
||||
+ (UIImage *)previewImageForView:(UIView *)view {
|
||||
if (CGRectIsEmpty(view.bounds)) {
|
||||
return nil;
|
||||
return [UIImage new];
|
||||
}
|
||||
|
||||
CGSize viewSize = view.bounds.size;
|
||||
@@ -359,14 +359,18 @@ BOOL FLEXConstructorsShouldRun() {
|
||||
|
||||
id jsonObject = [NSJSONSerialization JSONObjectWithData:data options:0 error:NULL];
|
||||
if ([NSJSONSerialization isValidJSONObject:jsonObject]) {
|
||||
prettyString = [NSString stringWithCString:[NSJSONSerialization
|
||||
dataWithJSONObject:jsonObject options:NSJSONWritingPrettyPrinted error:NULL
|
||||
].bytes encoding:NSUTF8StringEncoding];
|
||||
// Thanks RaziPour1993
|
||||
prettyString = [[NSString alloc]
|
||||
initWithData:[NSJSONSerialization
|
||||
dataWithJSONObject:jsonObject options:NSJSONWritingPrettyPrinted error:NULL
|
||||
]
|
||||
encoding:NSUTF8StringEncoding
|
||||
];
|
||||
// NSJSONSerialization escapes forward slashes.
|
||||
// We want pretty json, so run through and unescape the slashes.
|
||||
prettyString = [prettyString stringByReplacingOccurrencesOfString:@"\\/" withString:@"/"];
|
||||
} else {
|
||||
prettyString = [NSString stringWithCString:data.bytes encoding:NSUTF8StringEncoding];
|
||||
prettyString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
|
||||
}
|
||||
|
||||
return prettyString;
|
||||
|
||||
@@ -34,6 +34,15 @@
|
||||
#define FLEXRuntimeUtilityTryAddObjectProperty(iOS_atLeast, name, cls, type, ...) \
|
||||
FLEXRuntimeUtilityTryAddProperty(iOS_atLeast, name, cls, FLEXEncodeClass(type), PropertyKey(NonAtomic), __VA_ARGS__);
|
||||
|
||||
extern NSString * const FLEXRuntimeUtilityErrorDomain;
|
||||
|
||||
typedef NS_ENUM(NSInteger, FLEXRuntimeUtilityErrorCode) {
|
||||
// Start at a random value instead of 0 to avoid confusion with an absent code
|
||||
FLEXRuntimeUtilityErrorCodeDoesNotRecognizeSelector = 0xbabe,
|
||||
FLEXRuntimeUtilityErrorCodeInvocationFailed,
|
||||
FLEXRuntimeUtilityErrorCodeArgumentTypeMismatch
|
||||
};
|
||||
|
||||
@interface FLEXRuntimeUtility : NSObject
|
||||
|
||||
// General Helpers
|
||||
@@ -53,6 +62,7 @@
|
||||
|
||||
/// Used to describe an object in brief within an explorer row
|
||||
+ (NSString *)summaryForObject:(id)value;
|
||||
+ (NSString *)safeClassNameForObject:(id)object;
|
||||
+ (NSString *)safeDescriptionForObject:(id)object;
|
||||
+ (NSString *)safeDebugDescriptionForObject:(id)object;
|
||||
|
||||
@@ -74,6 +84,12 @@
|
||||
onObject:(id)object
|
||||
withArguments:(NSArray *)arguments
|
||||
error:(NSError * __autoreleasing *)error;
|
||||
+ (id)performSelector:(SEL)selector
|
||||
onObject:(id)object
|
||||
withArguments:(NSArray *)arguments
|
||||
allowForwarding:(BOOL)mightForwardMsgSend
|
||||
error:(NSError * __autoreleasing *)error;
|
||||
|
||||
+ (NSString *)editableJSONStringForObject:(id)object;
|
||||
+ (id)objectValueFromEditableJSONString:(NSString *)string;
|
||||
+ (NSValue *)valueForNumberWithObjCType:(const char *)typeEncoding fromInputString:(NSString *)inputString;
|
||||
|
||||
@@ -11,12 +11,7 @@
|
||||
#import "FLEXObjcInternal.h"
|
||||
#import "FLEXTypeEncodingParser.h"
|
||||
|
||||
static NSString *const FLEXRuntimeUtilityErrorDomain = @"FLEXRuntimeUtilityErrorDomain";
|
||||
typedef NS_ENUM(NSInteger, FLEXRuntimeUtilityErrorCode) {
|
||||
FLEXRuntimeUtilityErrorCodeDoesNotRecognizeSelector = 0,
|
||||
FLEXRuntimeUtilityErrorCodeInvocationFailed = 1,
|
||||
FLEXRuntimeUtilityErrorCodeArgumentTypeMismatch = 2
|
||||
};
|
||||
NSString * const FLEXRuntimeUtilityErrorDomain = @"FLEXRuntimeUtilityErrorDomain";
|
||||
|
||||
@implementation FLEXRuntimeUtility
|
||||
|
||||
@@ -96,12 +91,24 @@ typedef NS_ENUM(NSInteger, FLEXRuntimeUtilityErrorCode) {
|
||||
return superClasses;
|
||||
}
|
||||
|
||||
+ (NSString *)safeClassNameForObject:(id)object {
|
||||
// Don't assume that we have an NSObject subclass
|
||||
if ([self safeObject:object respondsToSelector:@selector(class)]) {
|
||||
return NSStringFromClass([object class]);
|
||||
}
|
||||
|
||||
return NSStringFromClass(object_getClass(object));
|
||||
}
|
||||
|
||||
/// Could be nil
|
||||
+ (NSString *)safeDescriptionForObject:(id)object {
|
||||
// Don't assume that we have an NSObject subclass.
|
||||
// Check to make sure the object responds to the description method
|
||||
// Don't assume that we have an NSObject subclass; not all objects respond to -description
|
||||
if ([self safeObject:object respondsToSelector:@selector(description)]) {
|
||||
return [object description];
|
||||
@try {
|
||||
return [object description];
|
||||
} @catch (NSException *exception) {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
return nil;
|
||||
@@ -111,10 +118,10 @@ typedef NS_ENUM(NSInteger, FLEXRuntimeUtilityErrorCode) {
|
||||
+ (NSString *)safeDebugDescriptionForObject:(id)object {
|
||||
NSString *description = nil;
|
||||
|
||||
// Don't assume that we have an NSObject subclass.
|
||||
// Check to make sure the object responds to the description method
|
||||
if ([self safeObject:object respondsToSelector:@selector(debugDescription)]) {
|
||||
description = [object debugDescription];
|
||||
@try {
|
||||
description = [object debugDescription];
|
||||
} @catch (NSException *exception) { }
|
||||
} else {
|
||||
description = [self safeDescriptionForObject:object];
|
||||
}
|
||||
@@ -177,18 +184,18 @@ typedef NS_ENUM(NSInteger, FLEXRuntimeUtilityErrorCode) {
|
||||
}
|
||||
|
||||
+ (BOOL)safeObject:(id)object respondsToSelector:(SEL)sel {
|
||||
static BOOL (*respondsToSelector)(id, SEL, SEL) = nil;
|
||||
static BOOL (*respondsToSelector_meta)(id, SEL, SEL) = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
respondsToSelector = (BOOL(*)(id, SEL, SEL))[NSObject instanceMethodForSelector:@selector(respondsToSelector:)];
|
||||
respondsToSelector_meta = (BOOL(*)(id, SEL, SEL))[NSObject methodForSelector:@selector(respondsToSelector:)];
|
||||
});
|
||||
|
||||
// If we're given a class, we want to know if classes respond to this selector.
|
||||
// Similarly, if we're given an instance, we want to know if instances respond.
|
||||
BOOL isClass = object_isClass(object);
|
||||
return (isClass ? respondsToSelector_meta : respondsToSelector)(
|
||||
object, @selector(respondsToSelector:), sel
|
||||
);
|
||||
Class cls = isClass ? object : object_getClass(object);
|
||||
// BOOL isMetaclass = class_isMetaClass(cls);
|
||||
|
||||
if (isClass) {
|
||||
// In theory, this should also work for metaclasses...
|
||||
return class_getClassMethod(cls, sel) != nil;
|
||||
} else {
|
||||
return class_getInstanceMethod(cls, sel) != nil;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -288,18 +295,31 @@ typedef NS_ENUM(NSInteger, FLEXRuntimeUtilityErrorCode) {
|
||||
onObject:(id)object
|
||||
withArguments:(NSArray *)arguments
|
||||
error:(NSError * __autoreleasing *)error {
|
||||
return [self performSelector:selector
|
||||
onObject:object
|
||||
withArguments:arguments
|
||||
allowForwarding:NO
|
||||
error:error
|
||||
];
|
||||
}
|
||||
|
||||
+ (id)performSelector:(SEL)selector
|
||||
onObject:(id)object
|
||||
withArguments:(NSArray *)arguments
|
||||
allowForwarding:(BOOL)mightForwardMsgSend
|
||||
error:(NSError * __autoreleasing *)error {
|
||||
static dispatch_once_t onceToken;
|
||||
static SEL stdStringExclusion = nil;
|
||||
dispatch_once(&onceToken, ^{
|
||||
stdStringExclusion = NSSelectorFromString(@"stdString");
|
||||
});
|
||||
|
||||
// Bail if the object won't respond to this selector.
|
||||
if (![self safeObject:object respondsToSelector:selector]) {
|
||||
// Bail if the object won't respond to this selector
|
||||
if (mightForwardMsgSend || ![self safeObject:object respondsToSelector:selector]) {
|
||||
if (error) {
|
||||
NSString *msg = [NSString
|
||||
stringWithFormat:@"%@ does not respond to the selector %@",
|
||||
object, NSStringFromSelector(selector)
|
||||
stringWithFormat:@"This object does not respond to the selector %@",
|
||||
NSStringFromSelector(selector)
|
||||
];
|
||||
NSDictionary<NSString *, id> *userInfo = @{ NSLocalizedDescriptionKey : msg };
|
||||
*error = [NSError
|
||||
|
||||
@@ -51,6 +51,10 @@
|
||||
@property (nonatomic, readonly) NSString *likelyGetterString;
|
||||
/// Not valid unless initialized with the owning class.
|
||||
@property (nonatomic, readonly) BOOL likelyGetterExists;
|
||||
/// Always \c nil for class properties.
|
||||
@property (nonatomic, readonly) NSString *likelyIvarName;
|
||||
/// Not valid unless initialized with the owning class.
|
||||
@property (nonatomic, readonly) BOOL likelyIvarExists;
|
||||
|
||||
/// Whether there are certainly multiple definitions of this property,
|
||||
/// such as in categories in other binary images or something.
|
||||
|
||||
@@ -124,6 +124,10 @@
|
||||
_likelySetterString = NSStringFromSelector(_likelySetter);
|
||||
|
||||
_isClassProperty = _cls ? class_isMetaClass(_cls) : NO;
|
||||
|
||||
_likelyIvarName = _isClassProperty ? nil : (
|
||||
self.attributes.backingIvar ?: [@"_" stringByAppendingString:_name]
|
||||
);
|
||||
}
|
||||
|
||||
#pragma mark Overrides
|
||||
@@ -187,6 +191,14 @@
|
||||
return _imageName;
|
||||
}
|
||||
|
||||
- (BOOL)likelyIvarExists {
|
||||
if (_likelyIvarName && _cls) {
|
||||
return class_getInstanceVariable(_cls, _likelyIvarName.UTF8String) != nil;
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (NSString *)fullDescription {
|
||||
NSMutableArray<NSString *> *attributesStrings = [NSMutableArray new];
|
||||
FLEXPropertyAttributes *attributes = self.attributes;
|
||||
|
||||
@@ -15,6 +15,9 @@
|
||||
@property (nonatomic) UIImage *image;
|
||||
@property (nonatomic) UIScrollView *scrollView;
|
||||
@property (nonatomic) UIImageView *imageView;
|
||||
@property (nonatomic) UITapGestureRecognizer *bgColorTapGesture;
|
||||
@property (nonatomic) NSInteger backgroundColorIndex;
|
||||
@property (nonatomic, readonly) NSArray<UIColor *> *backgroundColors;
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
@@ -31,19 +34,19 @@
|
||||
}
|
||||
|
||||
+ (instancetype)forImage:(UIImage *)image {
|
||||
if (!image) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return [[self alloc] initWithImage:image];
|
||||
}
|
||||
|
||||
- (id)initWithImage:(UIImage *)image {
|
||||
NSParameterAssert(image);
|
||||
|
||||
self = [super init];
|
||||
if (self) {
|
||||
self.title = @"Preview";
|
||||
self.image = image;
|
||||
_backgroundColors = @[FLEXResources.checkerPatternColor, UIColor.whiteColor, UIColor.blackColor];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
@@ -53,12 +56,10 @@
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
self.view.backgroundColor = [UIColor colorWithPatternImage:FLEXResources.checkerPattern];
|
||||
|
||||
self.imageView = [[UIImageView alloc] initWithImage:self.image];
|
||||
self.scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds];
|
||||
self.scrollView.delegate = self;
|
||||
self.scrollView.backgroundColor = self.view.backgroundColor;
|
||||
self.scrollView.backgroundColor = self.backgroundColors.firstObject;
|
||||
self.scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
||||
[self.scrollView addSubview:self.imageView];
|
||||
self.scrollView.contentSize = self.imageView.frame.size;
|
||||
@@ -66,7 +67,14 @@
|
||||
self.scrollView.maximumZoomScale = 2.0;
|
||||
[self.view addSubview:self.scrollView];
|
||||
|
||||
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAction target:self action:@selector(actionButtonPressed:)];
|
||||
self.bgColorTapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(changeBackground)];
|
||||
[self.scrollView addGestureRecognizer:self.bgColorTapGesture];
|
||||
|
||||
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc]
|
||||
initWithBarButtonSystemItem:UIBarButtonSystemItemAction
|
||||
target:self
|
||||
action:@selector(actionButtonPressed:)
|
||||
];
|
||||
}
|
||||
|
||||
- (void)viewDidLayoutSubviews {
|
||||
@@ -99,6 +107,12 @@
|
||||
self.scrollView.contentInset = UIEdgeInsetsMake(verticalInset, horizontalInset, verticalInset, horizontalInset);
|
||||
}
|
||||
|
||||
- (void)changeBackground {
|
||||
self.backgroundColorIndex++;
|
||||
self.backgroundColorIndex %= self.backgroundColors.count;
|
||||
self.scrollView.backgroundColor = self.backgroundColors[self.backgroundColorIndex];
|
||||
}
|
||||
|
||||
- (void)actionButtonPressed:(id)sender {
|
||||
static BOOL canSaveToCameraRoll = NO, didShowWarning = NO;
|
||||
static dispatch_once_t onceToken;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
#import "FLEXColor.h"
|
||||
#import "FLEXHierarchyTableViewController.h"
|
||||
#import "NSMapTable+FLEX_Subscripting.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXHierarchyTableViewCell.h"
|
||||
#import "FLEXObjectExplorerViewController.h"
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
C386D70C241AA67800699085 /* Owner.m in Sources */ = {isa = PBXBuildFile; fileRef = C386D70A241AA67800699085 /* Owner.m */; };
|
||||
C3A67856241AB8AD005A4681 /* MiscNetworkRequests.m in Sources */ = {isa = PBXBuildFile; fileRef = C3A67855241AB8AD005A4681 /* MiscNetworkRequests.m */; };
|
||||
C3A67858241ADDF7005A4681 /* Commit.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A67857241ADDF7005A4681 /* Commit.swift */; };
|
||||
C3B3760025B8CDA300AD43AB /* Person.m in Sources */ = {isa = PBXBuildFile; fileRef = C3B375FF25B8CDA300AD43AB /* Person.m */; };
|
||||
E211705F801A8167D308F94A /* libPods-FLEXample.a in Frameworks */ = {isa = PBXBuildFile; fileRef = BBD699DDBAC5A16D8CFD39AC /* libPods-FLEXample.a */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
@@ -54,6 +55,8 @@
|
||||
C3A67854241AB8AD005A4681 /* MiscNetworkRequests.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MiscNetworkRequests.h; sourceTree = "<group>"; };
|
||||
C3A67855241AB8AD005A4681 /* MiscNetworkRequests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MiscNetworkRequests.m; sourceTree = "<group>"; };
|
||||
C3A67857241ADDF7005A4681 /* Commit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Commit.swift; sourceTree = "<group>"; };
|
||||
C3B375FE25B8CDA300AD43AB /* Person.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = Person.h; sourceTree = "<group>"; };
|
||||
C3B375FF25B8CDA300AD43AB /* Person.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = Person.m; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@@ -138,6 +141,8 @@
|
||||
C3A67852241AB86D005A4681 /* App */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C3B375FE25B8CDA300AD43AB /* Person.h */,
|
||||
C3B375FF25B8CDA300AD43AB /* Person.m */,
|
||||
C3A67857241ADDF7005A4681 /* Commit.swift */,
|
||||
C386D6E42419984700699085 /* CommitListViewController.h */,
|
||||
C386D6E52419984700699085 /* CommitListViewController.m */,
|
||||
@@ -279,6 +284,7 @@
|
||||
files = (
|
||||
C3A67858241ADDF7005A4681 /* Commit.swift in Sources */,
|
||||
C386D6D02419975A00699085 /* AppDelegate.swift in Sources */,
|
||||
C3B3760025B8CDA300AD43AB /* Person.m in Sources */,
|
||||
C386D6E62419984700699085 /* CommitListViewController.m in Sources */,
|
||||
C386D70B241AA67800699085 /* Dog.m in Sources */,
|
||||
C3A67856241AB8AD005A4681 /* MiscNetworkRequests.m in Sources */,
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
#import "CommitListViewController.h"
|
||||
#import "FLEXample-Swift.h"
|
||||
#import "Person.h"
|
||||
#import <FLEX.h>
|
||||
|
||||
@interface CommitListViewController ()
|
||||
@@ -46,6 +47,18 @@
|
||||
];
|
||||
}
|
||||
}];
|
||||
|
||||
FLEXManager *flex = FLEXManager.sharedManager;
|
||||
|
||||
// Register 't' for testing: quickly present an object explorer for debugging
|
||||
[flex registerSimulatorShortcutWithKey:@"t" modifiers:0 action:^{
|
||||
[flex showExplorer];
|
||||
[flex presentTool:^UINavigationController *{
|
||||
return [FLEXNavigationController withRootViewController:[FLEXObjectExplorerFactory
|
||||
explorerViewControllerForObject:Person.bob
|
||||
]];
|
||||
} completion:nil];
|
||||
} description:@"Present an object explorer for debugging"];
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// Person.h
|
||||
// UICatalog
|
||||
//
|
||||
// Created by Tanner on 4/17/19.
|
||||
// Copyright © 2019 . All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface Person : NSObject <NSCoding>
|
||||
|
||||
+ (instancetype)bob;
|
||||
|
||||
@property (nonatomic, readonly) NSString *name;
|
||||
@property (nonatomic, readonly) NSInteger age;
|
||||
@property (nonatomic, readonly) CGFloat height;
|
||||
@property (nonatomic, readonly) NSNumber *numberOfKids;
|
||||
|
||||
@property (nonatomic) NSDecimalNumber *netWorth;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,63 @@
|
||||
//
|
||||
// Person.m
|
||||
// UICatalog
|
||||
//
|
||||
// Created by Tanner on 4/17/19.
|
||||
// Copyright © 2019 f. All rights reserved.
|
||||
//
|
||||
|
||||
#import "Person.h"
|
||||
|
||||
@implementation Person
|
||||
|
||||
+ (id)bob {
|
||||
Person *bob = [Person new];
|
||||
bob->_name = @"Bob";
|
||||
bob->_age = 50;
|
||||
bob->_height = 5.8;
|
||||
bob->_numberOfKids = @3;
|
||||
bob->_netWorth = [NSDecimalNumber decimalNumberWithString:@"12345.67"];
|
||||
return bob;
|
||||
}
|
||||
|
||||
- (void)encodeWithCoder:(nonnull NSCoder *)coder {
|
||||
[coder encodeObject:self.name forKey:@"name"];
|
||||
[coder encodeInteger:self.age forKey:@"age"];
|
||||
[coder encodeDouble:self.height forKey:@"height"];
|
||||
[coder encodeObject:self.numberOfKids forKey:@"numberOfKids"];
|
||||
[coder encodeObject:self.netWorth forKey:@"netWorth"];
|
||||
}
|
||||
|
||||
- (nullable instancetype)initWithCoder:(nonnull NSCoder *)coder {
|
||||
self->_name = [coder decodeObjectForKey:@"name"];
|
||||
self->_age = [coder decodeIntegerForKey:@"age"];
|
||||
self->_height = [coder decodeDoubleForKey:@"height"];
|
||||
self->_numberOfKids = [coder decodeObjectForKey:@"numberOfKids"];
|
||||
self->_netWorth = [coder decodeObjectForKey:@"netWorth"];
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setNetWorth:(NSDecimalNumber *)netWorth {
|
||||
_netWorth = netWorth;
|
||||
}
|
||||
|
||||
- (NSUInteger)hash {
|
||||
return self.name.hash ^ @(self.age).hash ^ self.numberOfKids.hash ^ self.netWorth.hash;
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)object {
|
||||
if ([object isKindOfClass:[Person class]])
|
||||
return [self isEqualToPerson:object];
|
||||
|
||||
return [super isEqual:object];
|
||||
}
|
||||
|
||||
- (BOOL)isEqualToPerson:(Person *)person {
|
||||
return [self.name isEqualToString:person.name];
|
||||
}
|
||||
|
||||
+ (NSInteger)version {
|
||||
return 2;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -12,6 +12,15 @@ import UIKit
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
var repeatingLogExampleTimer: Timer!
|
||||
var window: UIWindow?
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
func application(_ application: UIApplication,
|
||||
configurationForConnecting session: UISceneSession,
|
||||
options: UIScene.ConnectionOptions) -> UISceneConfiguration {
|
||||
// Called when a new scene session is being created.
|
||||
// Use this method to select a configuration to create the new scene with.
|
||||
return UISceneConfiguration(name: nil, sessionRole: session.role)
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions options: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
@@ -21,48 +30,53 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
UserDefaults.standard.set("foo", forKey: "FLEXamplePrefFoo")
|
||||
|
||||
// To show off the system log viewer, send 10 example log messages at 3 second intervals
|
||||
self.repeatingLogExampleTimer = Timer(
|
||||
timeInterval: 3, target: self,
|
||||
selector: #selector(sendExampleLogMessage),
|
||||
userInfo: nil, repeats: true
|
||||
)
|
||||
self.repeatingLogExampleTimer = Timer.scheduledTimer(withTimeInterval: 3, repeats: true) { [weak self] (_) in
|
||||
if let self = self {
|
||||
NSLog("Example log \(self.exampleLogSent)")
|
||||
|
||||
self.exampleLogSent += 1
|
||||
if self.exampleLogSent > self.exampleLogLimit {
|
||||
self.repeatingLogExampleTimer.invalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// To show off the network logger, send several misc network requests
|
||||
MiscNetworkRequests.sendExampleRequests()
|
||||
|
||||
// For testing unarchiving of objects
|
||||
self.archiveBob()
|
||||
|
||||
// For < iOS 13, set up the window here
|
||||
if ProcessInfo.processInfo.operatingSystemVersion.majorVersion < 13 {
|
||||
let window = UIWindow(frame: UIScreen.main.bounds)
|
||||
window.rootViewController = FLEXNavigationController(
|
||||
rootViewController: CommitListViewController()
|
||||
)
|
||||
self.window = window
|
||||
window.makeKeyAndVisible()
|
||||
FLEXManager.shared.showExplorer()
|
||||
}
|
||||
self.setupWindow()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@available(iOS 13.0, *)
|
||||
func application(_ application: UIApplication,
|
||||
configurationForConnecting session: UISceneSession,
|
||||
options: UIScene.ConnectionOptions) -> UISceneConfiguration {
|
||||
// Called when a new scene session is being created.
|
||||
// Use this method to select a configuration to create the new scene with.
|
||||
return UISceneConfiguration(name: nil, sessionRole: session.role)
|
||||
|
||||
func setupWindow() {
|
||||
guard ProcessInfo.processInfo.operatingSystemVersion.majorVersion < 13 else {
|
||||
return
|
||||
}
|
||||
|
||||
let window = UIWindow(frame: UIScreen.main.bounds)
|
||||
window.rootViewController = FLEXNavigationController(
|
||||
rootViewController: CommitListViewController()
|
||||
)
|
||||
self.window = window
|
||||
window.makeKeyAndVisible()
|
||||
FLEXManager.shared.showExplorer()
|
||||
}
|
||||
|
||||
func archiveBob() {
|
||||
let documents = NSSearchPathForDirectoriesInDomains(
|
||||
.documentDirectory, .userDomainMask, true
|
||||
).first! as NSString
|
||||
let whereToSaveBob = documents.appendingPathComponent("Bob.plist")
|
||||
try! NSKeyedArchiver.archivedData(
|
||||
withRootObject: Person.bob(), requiringSecureCoding: false
|
||||
).write(to: URL(fileURLWithPath: whereToSaveBob), options: [])
|
||||
}
|
||||
|
||||
let exampleLogLimit = 10
|
||||
var exampleLogSent = 0
|
||||
|
||||
func sendExampleLogMessage() {
|
||||
NSLog("Example log \(self.exampleLogSent)")
|
||||
|
||||
self.exampleLogSent += 1
|
||||
if self.exampleLogSent > self.exampleLogLimit {
|
||||
self.repeatingLogExampleTimer.invalidate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,3 +5,4 @@
|
||||
#import <FLEX.h>
|
||||
#import "MiscNetworkRequests.h"
|
||||
#import "CommitListViewController.h"
|
||||
#import "Person.h"
|
||||
|
||||
+3
-3
@@ -1,6 +1,6 @@
|
||||
Pod::Spec.new do |spec|
|
||||
spec.name = "FLEX"
|
||||
spec.version = "4.2.0"
|
||||
spec.version = "4.4.1"
|
||||
spec.summary = "A set of in-app debugging and exploration tools for iOS"
|
||||
spec.description = <<-DESC
|
||||
- Inspect and modify views in the hierarchy.
|
||||
@@ -40,6 +40,6 @@ Pod::Spec.new do |spec|
|
||||
"Classes/GlobalStateExplorers/Globals/FLEXGlobalsEntry.h",
|
||||
"Classes/Core/**/*.h", "Classes/Utility/Runtime/Objc/**/*.h",
|
||||
"Classes/ObjectExplorers/**/*.h", "Classes/Editing/**/*.h",
|
||||
"Classes/Utility/FLEXMacros.h", "Classes/Utility/Categories/*.h",
|
||||
"Classes/Utility/FLEXAlert.h", "Classes/Utility/FLEXResources.h" ]
|
||||
"Classes/Utility/Categories/*.h", "Classes/Utility/FLEXAlert.h",
|
||||
"Classes/Utility/FLEXResources.h" ]
|
||||
end
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user