Compare commits

..

96 Commits

Author SHA1 Message Date
Tanner Bennett 67097982ea Bump version 2021-08-19 00:58:13 -05:00
matrush 1342d3029c Add availability check for iOS 11+ only SFSafariViewController properities 2021-08-09 09:37:23 -07:00
Hossam Ghareeb 62dcef4644 Add ability to delete SQLite rows 2021-07-25 18:15:26 -05:00
Tanner Bennett 7ee296143e Add button to clear SQLite table (@hossamghareeb) 2021-07-25 17:31:29 -05:00
Tanner Bennett c6bac54597 Fix #544; utilize smartQuotesType = .no 2021-07-25 17:03:05 -05:00
Tanner Bennett 5f7bce64ed Create FUNDING.yml 2021-07-25 16:52:59 -05:00
Tanner Bennett d2dde55bb1 Lay groundwork for multiple shortcut sections 2021-05-25 17:47:29 -05:00
Tanner Bennett f1a0a5c5e5 Add shortcuts for collections 2021-05-25 17:46:50 -05:00
Tanner Bennett 0db073459e iOS 13 is now the minimum required SDK
Remove FLEX_AT_LEAST_IOS13_SDK and usages. It was causing several issues being in a header that needed to be private but also included in public headers.
2021-05-25 15:30:46 -05:00
Tanner Bennett 7c7ed9286f Fix code typos 2021-05-25 15:10:45 -05:00
Tanner Bennett aac88dd6c8 Fix presentTool:
presentTool
2021-05-07 14:01:57 -05:00
Tanner Bennett d7376b75cd Add flex_observers shortcut to NSNotificationCenter 2021-05-07 13:58:04 -05:00
Tanner Bennett 5b39b3ed03 Add UIPasteboard shortcuts 2021-05-07 13:58:04 -05:00
Tanner Bennett bbaa85bdbf Add WKWebView/SFVC shortcuts 2021-05-07 12:03:06 -05:00
Tanner Bennett 34e27bc5d9 Stable sort and sort integers numerically 2021-05-01 01:04:12 -05:00
Tanner Bennett 714307273e Show full text for DB browser column headers 2021-05-01 01:04:12 -05:00
TekYin 5242d3c5a1 Fix crash when sharing single file on iPad 2021-04-30 23:28:32 -05:00
ray cf2e94a1d2 Fix crash when viewing keychain item 2021-04-30 23:17:01 -05:00
canius 800acb4cad Fix private header warnings and install path (#514)
Co-authored-by: Canius Chu <canius.chu@farfetch.com>
2021-04-30 22:37:39 -05:00
Saafo 368ce64121 Fix iPad/multi-scene FLEX bar layout bugs (#534)
Prior to this, FLEX could appear in the wrong scene or with the wrong width if the scene is not the full width of the screen, which is almost always the case in multi scene environments.

Thanks @Saafo!
2021-04-25 14:54:23 -05:00
Bang Lee 05f03090a9 Display access group for keychain items (#532) 2021-04-12 01:44:29 -05:00
Tanner Bennett a8803781e8 Add missing files to xcodeproj; oops, thanks @BB9z 2021-04-08 16:41:54 -05:00
Tanner Bennett 170f74b297 Don't codesign FLEX.framework 2021-04-08 16:39:39 -05:00
Tanner Bennett 0d0f2a3073 Add FLEXAlert nullabilities for Swift 2021-04-08 16:39:39 -05:00
Michael Gray d6bddf5199 Quote table names in SQL commands (#529)
Sqlite is often smart enough to not need quotes for identifiers, unless you want to use a keyword as a name. Then you need to quote it

https://www.sqlite.org/lang_keywords.html
2021-04-05 15:02:05 -05:00
Tanner Bennett 258ec8697b Bump version 2021-03-30 14:20:02 -05:00
Tanner Bennett 8f9a6e88ec Fix toolbar appearing when empty 2021-03-30 14:20:02 -05:00
Tanner Bennett c37270e6ac Add shortcuts for NSString and NSData 2021-03-30 14:20:02 -05:00
Tanner Bennett ce10d45c29 Fix "hide likely private methods" behavior
Hide relevant private properties too, also class properties and methods
2021-03-30 14:20:02 -05:00
Tanner Bennett b2716c4b2e Catch potential exceptions thrown from descriptions 2021-03-30 14:20:02 -05:00
Tanner Bennett ab135ba94e Remove uninclusive terms
Blacklist → exclude[d] / denylist
Whitelist → allow[ed] / unexcluded
2021-03-30 14:20:02 -05:00
Tanner Bennett bcc04f4113 Make FLEXMacros.h private 2021-03-30 14:20:02 -05:00
Tanner Bennett 470b3fa3b3 Add tintColor as real property 2021-03-30 14:20:02 -05:00
Tanner Bennett e84dfeae5c Fix object explorer class pan gesture… again… 2021-03-30 14:20:02 -05:00
Tanner Bennett 44e86e59b7 Fix Xcode 12 compile errors 2021-03-30 14:20:02 -05:00
Tanner Bennett ac6d9cfa3f Misc refactoring 2021-03-30 14:20:02 -05:00
Tanner Bennett 1a59711760 Fix a crash, add some missing nullabilities 2021-03-30 14:20:01 -05:00
Tanner Bennett b60ce7a057 Add UIAlertController shortcuts
Fix alert controller
2021-03-30 14:20:01 -05:00
Tanner Bennett b4ac210bef Use likely ivar to determine matching property 2021-03-29 13:55:52 -05:00
Tanner Bennett 9360c58975 Add likely ivar data to FLEXProperty 2021-03-29 13:55:52 -05:00
Tanner Bennett 43d9a460ce Make the keyboard seem to appear faster
On screens where the keyboard is set to appear right away, make it appear faster—or at least, make it look like it appears faster…

Also add a property to make the search bar appear initially instead of duplicating becomeFirstResponder code across several classes
2021-03-29 13:55:51 -05:00
ray 15fee5a8a5 Fix keychain viewer crash (#521) 2021-03-23 10:36:29 -05:00
Alexey Salangin fad038392b Don't show toolbar when tapping on a nav bar button (#517) 2021-03-11 13:45:23 -06:00
Tanner Bennett 558d65a0b0 Update license 2021-02-02 14:18:09 -06:00
Tanner Bennett 077fca36c0 Bump deployment targets (oops?) 2021-01-27 23:24:39 -06:00
Tanner Bennett 947769f6f9 Bump version 2021-01-27 17:10:26 -06:00
Tanner Bennett 7c480c5faf Mention tvOS fork in README 2021-01-27 17:09:41 -06:00
Tanner Bennett fa6c72cb08 Fix memory leak 2021-01-27 16:49:10 -06:00
Tanner Bennett 9af2926ec1 Use weakify/strongify throughout project 2021-01-26 12:46:53 -06:00
Tanner Bennett 366a8266bd Add weakify/strongify macros 2021-01-26 12:46:53 -06:00
Tanner Bennett 713fbac54a Move 't' simulator shortcut to the example project
This demonstrates how to register a shortcut from a project with the new pubilc APIs for presenting tools.

I put it in the commit list screen because I didn't feel like rewriting it in Swift.
2021-01-26 12:46:53 -06:00
Tanner Bennett 8198fba689 Add public API to dismiss/present FLEX tools
This API will allow you to present a tool in front of the FLEX toolbar and will automatically dismiss any existing FLEX tool
2021-01-26 12:46:53 -06:00
Tanner Bennett 20e14a36c9 Add -[NSMutableArray flex_filter:] 2021-01-26 11:18:43 -06:00
Tanner Bennett 176e98518d Add context menu item to view references to object 2021-01-20 17:00:37 -06:00
Tanner Bennett 31440056c1 Add option to hide potentially private methods 2021-01-20 17:00:10 -06:00
Tanner Bennett 6c7b39ed03 Fix object explorer swipe gesture precedence 2021-01-20 15:31:25 -06:00
Tanner Bennett 96989f7e0c Refactor FLEXample's AppDelegate.swift 2021-01-20 15:08:06 -06:00
Tanner Bennett 22a23b3b12 Add the Person class back to the example project 2021-01-20 14:59:09 -06:00
Tanner Bennett 90a855a289 Allow registration of global entries with actions
This was previously a private API
2021-01-20 14:37:44 -06:00
matrush fd67373995 Revise object reference section titles logic 2021-01-20 14:37:13 -06:00
Tanner Bennett 3b5e095f74 Add some tests, fix some tests
- Test safeClassNameForObject:
- Test safeObject:respondsToSelector:
- Fix testAssumptionsAboutClasses: which was incorrectly written
2020-12-30 20:06:49 -06:00
Tanner Bennett c7850df186 Add safeClassNameForObject: and use it 2020-12-30 20:06:49 -06:00
Tanner Bennett 6bbcc55cf6 Improve safeObject:respondsToSelector:
The old code assumes the object at least responds to respondsToSelector: which may not always be true. I also I think the old code was not quite correct for all cases anyway.

Using the runtime functions ensures no methods are ever called on the object, and properly handles metaclass objects, in theory.
2020-12-30 20:06:49 -06:00
Tanner Bennett e63ea4bbff Group FLEX objects together in the object refs list 2020-12-30 20:06:49 -06:00
Tanner Bennett 5a760fb1ac Update issue templates 2020-12-30 20:06:49 -06:00
Tanner Bennett e63f2ee3ad Prefix some categories, fix #496 2020-12-30 20:06:49 -06:00
Tanner Bennett 46c6dcb7e6 Fix bug introduced in cb2e0789 2020-12-30 17:31:37 -06:00
Alexey Salangin bf42bbe27b Fix bug with navbar tap gesture
Tap gesture in FLEXNavigationController's navigation bar causes buttons to reportedly become unresponsive on iOS 11 and 12 unless `cancelsTouchesInView` is turned off
2020-12-30 15:27:04 -08:00
Tanner Bennett e89fec4b2d Bump version 2020-12-17 01:57:25 -06:00
Tanner Bennett 715bb92929 Add confirm dialog for clearing the keychain 2020-12-17 01:35:03 -06:00
Tanner Bennett 109074f98e Show service AND account in keychain rows 2020-12-17 01:34:44 -06:00
Tanner Bennett 45fbdb7914 Move keychain buttons to toolbar 2020-12-17 01:34:33 -06:00
Tanner Bennett cb2e0789d8 Fix several memory leaks, fix #483
Also move a bunch of globals related to FLEXShortcutsFactory into ivars of a FLEXShortcutsFactory singleton so that it is easier to trace the origin of the global storage of FLEX[Property|Ivar|Method] objects in a memory graph
2020-12-17 01:17:40 -06:00
vvveiii de1ca783b6 Fix flex_copy: crash when object is nil 2020-12-16 22:30:11 -08:00
vvveiii 3a9c24b784 Add missing else for flex_copy method 2020-12-16 22:30:11 -08:00
Tanner Bennett b57a333fc9 Fix #491, close #473
Remove (most) uses of +[NSString stringWithCString:encoding:]
2020-12-17 00:20:17 -06:00
matrush 288bf1343e Add ability to toggle bg color for image previews
Also fix availability warning
2020-12-15 16:35:27 -08:00
matrush a0b1caed54 Use UITableViewCellAccessoryNone instead of 0 in FLEX[Layer/View]Shortcuts 2020-12-14 18:38:04 -08:00
Tanner Bennett 9282c61183 Allow scrubbing between selected views
Instead of a singular swipe gesture to navigate between views in the view hierarchy at the tap point, we can now pan or drag along that same area to "srub" the hierarchy.
2020-12-14 18:36:44 -06:00
Tanner Bennett ee6677ee08 Add GUI shortcut for initializeWebKitLegacy
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.

In this case, you can now long press on the navigation bar to call initializeWebKitLegacy
2020-12-14 17:10:48 -06:00
Tanner Bennett 3276eb3516 Automatically activate search bar in heap explorer
2f952c38 apparently didn't do this, oops
2020-12-14 17:10:48 -06:00
Chaoshuai Lü a3fa7bbadc Fix view/controller preview image issue (#493) 2020-12-14 16:08:58 -06:00
matrush 637074b354 Use id<NSCopying> as key instead of Class<NSCopying> in FLEXObjectExplorerFactory 2020-12-01 16:56:08 -08:00
Tanner Bennett 547bfbaec0 Disallow preview of views with no rect 2020-11-02 20:42:10 -06:00
Tanner Bennett 4e1fcf4682 Bump version, 4.2.2 2020-11-02 19:36:15 -06:00
Tanner Bennett 4d50fd2020 Revert "Use NSString as key …" in d7786449
We NEED to use the class as a key, because you cannot differentiate a class's name from the metaclass's name.
2020-11-02 19:36:15 -06:00
Tanner Bennett 2c510c8ca1 Fix log timer in example app not firing 2020-11-02 17:45:21 -06:00
Tanner Bennett 8283e2a8e7 Fix #415, restore ability to copy log messages 2020-11-02 17:32:29 -06:00
Tanner Bennett 3f82631a95 Fix bug introduced in 4.2.0, unable to edit members
Oopsie
2020-11-02 17:03:37 -06:00
matrush d77864494d Use NSString as key instead of Class<NSCopying> in FLEXObjectExplorerFactory 2020-11-02 16:58:59 -06:00
matrush c5ed6d4ece Add void to function definition of FLEXConstructorsShouldRun 2020-11-02 16:58:59 -06:00
matrush 9412f6eccf Add a clear global entries method for FLEXManager 2020-11-02 16:58:59 -06:00
Tanner Bennett d9aa102e77 Bump version 2020-10-23 16:42:44 -05:00
Oleg 6f80476135 Fix crash on flex_withTintColor (#476) 2020-10-23 16:23:39 -05:00
Tanner Bennett e18a854a9f Bump version, close #465 #466 2020-10-22 18:18:26 -05:00
Tanner Bennett 0d676e2504 Add FLEX_DISABLE_CTORS, close #471 2020-10-22 18:16:18 -05:00
133 changed files with 1904 additions and 737 deletions
+3
View File
@@ -0,0 +1,3 @@
# These are supported funding model platforms
github: [NSExceptional]
+27
View File
@@ -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.
+10
View File
@@ -0,0 +1,10 @@
---
name: Feature request
about: Suggest a new feature for FLEX
title: ''
labels: enhancement
assignees: ''
---
@@ -9,6 +9,7 @@
#import "FLEXFilteringTableViewController.h"
#import "FLEXTableViewSection.h"
#import "NSArray+FLEX.h"
#import "FLEXMacros.h"
@interface FLEXFilteringTableViewController ()
@@ -187,8 +188,6 @@
[self.filterDelegate.sections[indexPath.section] didPressInfoButtonAction:indexPath.row](self);
}
#if FLEX_AT_LEAST_IOS13_SDK
- (UIContextMenuConfiguration *)tableView:(UITableView *)tableView contextMenuConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath point:(CGPoint)point __IOS_AVAILABLE(13.0) {
FLEXTableViewSection *section = self.filterDelegate.sections[indexPath.section];
NSString *title = [section menuTitleForRow:indexPath.row];
@@ -207,6 +206,4 @@
return nil;
}
#endif
@end
@@ -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.
///
@@ -50,15 +50,12 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
#pragma mark - Initialization
- (id)init {
#if FLEX_AT_LEAST_IOS13_SDK
if (@available(iOS 13.0, *)) {
self = [self initWithStyle:UITableViewStyleInsetGrouped];
} else {
self = [self initWithStyle:UITableViewStyleGrouped];
}
#else
self = [self initWithStyle:UITableViewStyleGrouped];
#endif
return self;
}
@@ -106,11 +103,9 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
self.automaticallyShowsSearchBarCancelButton = YES;
#if FLEX_AT_LEAST_IOS13_SDK
if (@available(iOS 13, *)) {
self.searchController.automaticallyShowsScopeBar = NO;
}
#endif
[self addSearchController:self.searchController];
} else {
@@ -124,18 +119,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];
}];
@@ -173,21 +165,17 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
}
- (BOOL)automaticallyShowsSearchBarCancelButton {
#if FLEX_AT_LEAST_IOS13_SDK
if (@available(iOS 13, *)) {
return self.searchController.automaticallyShowsCancelButton;
}
#endif
return _automaticallyShowsSearchBarCancelButton;
}
- (void)setAutomaticallyShowsSearchBarCancelButton:(BOOL)value {
#if FLEX_AT_LEAST_IOS13_SDK
if (@available(iOS 13, *)) {
self.searchController.automaticallyShowsCancelButton = value;
}
#endif
_automaticallyShowsSearchBarCancelButton = value;
}
@@ -241,7 +229,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 +247,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 +279,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 +529,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 {
-3
View File
@@ -7,7 +7,6 @@
//
#import <UIKit/UIKit.h>
#import "FLEXMacros.h"
#import "NSArray+FLEX.h"
@class FLEXTableView;
@@ -101,7 +100,6 @@ NS_ASSUME_NONNULL_BEGIN
- (nullable void(^)(__kindof UIViewController *host))didPressInfoButtonAction:(NSInteger)row;
#pragma mark - Context Menus
#if FLEX_AT_LEAST_IOS13_SDK
/// By default, this is the title of the row.
/// @return The title of the context menu, if any.
@@ -121,7 +119,6 @@ NS_ASSUME_NONNULL_BEGIN
/// should be a description of what will be copied, and the values should be
/// the strings to copy. Return an empty string as a value to show a disabled action.
- (nullable NSArray<NSString *> *)copyMenuItemsForRow:(NSInteger)row API_AVAILABLE(ios(13.0));
#endif
#pragma mark - Cell Configuration
-4
View File
@@ -64,8 +64,6 @@
return kFLEXDefaultCell;
}
#if FLEX_AT_LEAST_IOS13_SDK
- (NSString *)menuTitleForRow:(NSInteger)row {
NSString *title = [self titleForRow:row];
NSString *subtitle = [self menuSubtitleForRow:row];
@@ -127,8 +125,6 @@
return @[];
}
#endif
- (NSArray<NSString *> *)copyMenuItemsForRow:(NSInteger)row {
return nil;
}
@@ -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;
}
-8
View File
@@ -30,29 +30,21 @@ FLEXTableViewCellReuseIdentifier const kFLEXCodeFontCell = @"kFLEXCodeFontCell";
@implementation FLEXTableView
+ (instancetype)flexDefaultTableView {
#if FLEX_AT_LEAST_IOS13_SDK
if (@available(iOS 13.0, *)) {
return [[self alloc] initWithFrame:CGRectZero style:UITableViewStyleInsetGrouped];
} else {
return [[self alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];
}
#else
return [[self alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];
#endif
}
#pragma mark - Initialization
+ (id)groupedTableView {
#if FLEX_AT_LEAST_IOS13_SDK
if (@available(iOS 13.0, *)) {
return [[self alloc] initWithFrame:CGRectZero style:UITableViewStyleInsetGrouped];
} else {
return [[self alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];
}
#else
return [[self alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];
#endif
}
+ (id)plainTableView {
@@ -33,6 +33,7 @@
self.inputTextView.delegate = self;
self.inputTextView.inputAccessoryView = [self createToolBar];
if (@available(iOS 11, *)) {
self.inputTextView.smartQuotesType = UITextSmartQuotesTypeNo;
[self.inputTextView.layer setValue:@YES forKey:@"continuousCorners"];
} else {
self.inputTextView.layer.borderWidth = 1.f;
@@ -21,11 +21,21 @@
- (BOOL)shouldReceiveTouchAtWindowPoint:(CGPoint)pointInWindowCoordinates;
/// @brief Used to present (or dismiss) a modal view controller ("tool"), typically triggered by pressing a button in the toolbar.
/// @brief Used to present (or dismiss) a modal view controller ("tool"),
/// typically triggered by pressing a button in the toolbar.
///
/// If a tool is already presented, this method simply dismisses it and calls the completion block.
/// If no tool is presented, @code future() @endcode is presented and the completion block is called.
- (void)toggleToolWithViewControllerProvider:(UINavigationController *(^)(void))future completion:(void(^)(void))completion;
- (void)toggleToolWithViewControllerProvider:(UINavigationController *(^)(void))future
completion:(void (^)(void))completion;
/// @brief Used to present (or dismiss) a modal view controller ("tool"),
/// typically triggered by pressing a button in the toolbar.
///
/// If a tool is already presented, this method dismisses it and presents the given tool.
/// The completion block is called once the tool has been presented.
- (void)presentTool:(UINavigationController *(^)(void))future
completion:(void (^)(void))completion;
// 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,20 +918,33 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
[super dismissViewControllerAnimated:animated completion:completion];
}
- (BOOL)wantsWindowToBecomeKey
{
- (BOOL)wantsWindowToBecomeKey {
return self.window.previousKeyWindow != nil;
}
- (void)toggleToolWithViewControllerProvider:(UINavigationController *(^)(void))future
completion:(void(^)(void))completion {
completion:(void (^)(void))completion {
if (self.presentedViewController) {
// We do NOT want to present the future; this is
// a convenience method for toggling the SAME TOOL
[self dismissViewControllerAnimated:YES completion:completion];
} else if (future) {
[self presentViewController:future() animated:YES completion:completion];
}
}
- (void)presentTool:(UINavigationController *(^)(void))future
completion:(void (^)(void))completion {
if (self.presentedViewController) {
// If a tool is already presented, dismiss it first
[self dismissViewControllerAnimated:YES completion:^{
[self presentViewController:future() animated:YES completion:completion];
}];
} else if (future) {
[self presentViewController:future() animated:YES completion:completion];
}
}
- (FLEXWindow *)window {
return (id)self.view.window;
}
@@ -924,11 +983,7 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
} else {
return [FLEXHierarchyViewController delegate:self];
}
} completion:^{
if (completion) {
completion();
}
}];
} completion:completion];
}
- (void)toggleMenuTool {
-5
View File
@@ -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>
-1
View File
@@ -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>
@@ -8,12 +8,21 @@
#import <UIKit/UIKit.h>
@class FLEXDBQueryRowCell;
extern NSString * const kFLEXDBQueryRowCellReuse;
@protocol FLEXDBQueryRowCellLayoutSource <NSObject>
- (CGFloat)dbQueryRowCell:(FLEXDBQueryRowCell *)dbQueryRowCell minXForColumn:(NSUInteger)column;
- (CGFloat)dbQueryRowCell:(FLEXDBQueryRowCell *)dbQueryRowCell widthForColumn:(NSUInteger)column;
@end
@interface FLEXDBQueryRowCell : UITableViewCell
/// An array of NSString, NSNumber, or NSData objects
@property (nonatomic) NSArray *data;
@property (nonatomic, weak) id<FLEXDBQueryRowCellLayoutSource> layoutSource;
@end
@@ -63,11 +63,12 @@ NSString * const kFLEXDBQueryRowCellReuse = @"kFLEXDBQueryRowCellReuse";
- (void)layoutSubviews {
[super layoutSubviews];
CGFloat width = self.contentView.frame.size.width / self.labels.count;
CGFloat height = self.contentView.frame.size.height;
[self.labels flex_forEach:^(UILabel *label, NSUInteger i) {
label.frame = CGRectMake(width * i + 5, 0, (width - 10), height);
CGFloat width = [self.layoutSource dbQueryRowCell:self widthForColumn:i];
CGFloat minX = [self.layoutSource dbQueryRowCell:self minXForColumn:i];
label.frame = CGRectMake(minX + 5, 0, (width - 10), height);
}];
}
@@ -29,6 +29,7 @@
@optional
- (NSArray<NSString *> *)queryRowIDsInTable:(NSString *)tableName;
- (FLEXSQLResult *)executeStatement:(NSString *)SQLStatement;
@end
@@ -29,7 +29,7 @@
- (NSString *)rowTitle:(NSInteger)row;
- (NSArray<NSString *> *)contentForRow:(NSInteger)row;
- (CGFloat)multiColumnTableView:(FLEXMultiColumnTableView *)tableView widthForContentCellInColumn:(NSInteger)column;
- (CGFloat)multiColumnTableView:(FLEXMultiColumnTableView *)tableView minWidthForContentCellInColumn:(NSInteger)column;
- (CGFloat)multiColumnTableView:(FLEXMultiColumnTableView *)tableView heightForContentCellInRow:(NSInteger)row;
- (CGFloat)heightForTopHeaderInTableView:(FLEXMultiColumnTableView *)tableView;
- (CGFloat)widthForLeftHeaderInTableView:(FLEXMultiColumnTableView *)tableView;
@@ -9,10 +9,12 @@
#import "FLEXMultiColumnTableView.h"
#import "FLEXDBQueryRowCell.h"
#import "FLEXTableLeftCell.h"
#import "NSArray+FLEX.h"
#import "FLEXColor.h"
@interface FLEXMultiColumnTableView () <
UITableViewDataSource, UITableViewDelegate, UIScrollViewDelegate
UITableViewDataSource, UITableViewDelegate,
UIScrollViewDelegate, FLEXDBQueryRowCellLayoutSource
>
@property (nonatomic) UIScrollView *contentScrollView;
@@ -21,12 +23,12 @@
@property (nonatomic) UITableView *contentTableView;
@property (nonatomic) UIView *leftHeader;
@property (nonatomic) NSArray<UIView *> *headerViews;
/// \c NSNotFound if no column selected
@property (nonatomic) NSInteger sortColumn;
@property (nonatomic) FLEXTableColumnHeaderSortType sortType;
@property (nonatomic) NSArray *rowData;
@property (nonatomic, readonly) NSInteger numberOfColumns;
@property (nonatomic, readonly) NSInteger numberOfRows;
@property (nonatomic, readonly) CGFloat topHeaderHeight;
@@ -71,9 +73,9 @@ static const CGFloat kColumnMargin = 1;
}
CGFloat contentWidth = 0.0;
NSInteger rowsCount = self.numberOfColumns;
for (int i = 0; i < rowsCount; i++) {
contentWidth += [self contentWidthForColumn:i];
NSInteger columnsCount = self.numberOfColumns;
for (int i = 0; i < columnsCount; i++) {
contentWidth += CGRectGetWidth(self.headerViews[i].bounds);
}
CGFloat contentHeight = height - topheaderHeight - topInsets;
@@ -147,26 +149,30 @@ static const CGFloat kColumnMargin = 1;
#pragma mark - Data
- (void)reloadData {
[self loadHeaderData];
[self loadLeftViewData];
[self loadContentData];
[self loadHeaderData];
}
- (void)loadHeaderData {
// Remove existing headers, if any
for (UIView *subview in self.headerScrollView.subviews) {
for (UIView *subview in self.headerViews) {
[subview removeFromSuperview];
}
CGFloat xOffset = 0.0;
for (NSInteger column = 0; column < self.numberOfColumns; column++) {
CGFloat width = [self contentWidthForColumn:column] + self.columnMargin;
FLEXTableColumnHeader *header = [[FLEXTableColumnHeader alloc]
initWithFrame:CGRectMake(xOffset, 0, width, self.topHeaderHeight - 1)
];
__block CGFloat xOffset = 0;
self.headerViews = [NSArray flex_forEachUpTo:self.numberOfColumns map:^id(NSUInteger column) {
FLEXTableColumnHeader *header = [FLEXTableColumnHeader new];
header.titleLabel.text = [self columnTitle:column];
CGSize fittingSize = CGSizeMake(CGFLOAT_MAX, self.topHeaderHeight - 1);
CGFloat width = self.columnMargin + MAX(
[self minContentWidthForColumn:column],
[header sizeThatFits:fittingSize].width
);
header.frame = CGRectMake(xOffset, 0, width, self.topHeaderHeight - 1);
if (column == self.sortColumn) {
header.sortType = self.sortType;
}
@@ -178,21 +184,22 @@ static const CGFloat kColumnMargin = 1;
[header addGestureRecognizer:gesture];
header.userInteractionEnabled = YES;
[self.headerScrollView addSubview:header];
xOffset += width;
}
[self.headerScrollView addSubview:header];
return header;
}];
}
- (void)contentHeaderTap:(UIGestureRecognizer *)gesture {
NSInteger newSortColumn = [self.headerScrollView.subviews indexOfObject:gesture.view];
NSInteger newSortColumn = [self.headerViews indexOfObject:gesture.view];
FLEXTableColumnHeaderSortType newType = FLEXNextTableColumnHeaderSortType(self.sortType);
// Reset old header
FLEXTableColumnHeader *oldHeader = (id)self.headerScrollView.subviews[self.sortColumn];
FLEXTableColumnHeader *oldHeader = (id)self.headerViews[self.sortColumn];
oldHeader.sortType = FLEXTableColumnHeaderSortTypeNone;
// Update new header
FLEXTableColumnHeader *newHeader = (id)self.headerScrollView.subviews[newSortColumn];
FLEXTableColumnHeader *newHeader = (id)self.headerViews[newSortColumn];
newHeader.sortType = newType;
// Update self
@@ -227,13 +234,13 @@ static const CGFloat kColumnMargin = 1;
}
// Right side table view for data
else {
self.rowData = [self.dataSource contentForRow:indexPath.row];
FLEXDBQueryRowCell *cell = [tableView
dequeueReusableCellWithIdentifier:kFLEXDBQueryRowCellReuse forIndexPath:indexPath
];
cell.contentView.backgroundColor = backgroundColor;
cell.data = [self.dataSource contentForRow:indexPath.row];
cell.layoutSource = self;
NSAssert(cell.data.count == self.numberOfColumns, @"Count of data provided was incorrect");
return cell;
}
@@ -280,6 +287,17 @@ static const CGFloat kColumnMargin = 1;
}
#pragma mark FLEXDBQueryRowCellLayoutSource
- (CGFloat)dbQueryRowCell:(FLEXDBQueryRowCell *)dbQueryRowCell minXForColumn:(NSUInteger)column {
return CGRectGetMinX(self.headerViews[column].frame);
}
- (CGFloat)dbQueryRowCell:(FLEXDBQueryRowCell *)dbQueryRowCell widthForColumn:(NSUInteger)column {
return CGRectGetWidth(self.headerViews[column].bounds);
}
#pragma mark DataSource Accessor
- (NSInteger)numberOfRows {
@@ -298,8 +316,8 @@ static const CGFloat kColumnMargin = 1;
return [self.dataSource rowTitle:row];
}
- (CGFloat)contentWidthForColumn:(NSInteger)column {
return [self.dataSource multiColumnTableView:self widthForContentCellInColumn:column];
- (CGFloat)minContentWidthForColumn:(NSInteger)column {
return [self.dataSource multiColumnTableView:self minWidthForContentCellInColumn:column];
}
- (CGFloat)contentHeightForRow:(NSInteger)row {
@@ -30,7 +30,7 @@ static NSString * const QUERY_TABLENAMES = @"SELECT name FROM sqlite_master WHER
- (instancetype)initWithPath:(NSString *)path {
self = [super init];
if (self) {
self.path = path;;
self.path = path;
}
return self;
@@ -114,9 +114,17 @@ static NSString * const QUERY_TABLENAMES = @"SELECT name FROM sqlite_master WHER
}
- (NSArray<NSArray *> *)queryAllDataInTable:(NSString *)tableName {
return [self executeStatement:[@"SELECT * FROM "
stringByAppendingString:tableName
]].rows ?: @[];
NSString *command = [NSString stringWithFormat:@"SELECT * FROM \"%@\"", tableName];
return [self executeStatement:command].rows ?: @[];
}
- (NSArray<NSString *> *)queryRowIDsInTable:(NSString *)tableName {
NSString *command = [NSString stringWithFormat:@"SELECT rowid FROM \"%@\"", tableName];
NSArray<NSArray<NSString *> *> *data = [self executeStatement:command].rows ?: @[];
return [data flex_mapped:^id(NSArray<NSString *> *obj, NSUInteger idx) {
return obj.firstObject;
}];
}
- (FLEXSQLResult *)executeStatement:(NSString *)sql {
@@ -257,7 +265,7 @@ static NSString * const QUERY_TABLENAMES = @"SELECT name FROM sqlite_master WHER
- (FLEXSQLResult *)errorResult:(NSString *)description {
const char *error = sqlite3_errmsg(_db);
NSString *message = error ? @(error) : [NSString
stringWithFormat:@"(%@: empty error", description
stringWithFormat:@"(%@: empty error)", description
];
return [FLEXSQLResult error:message];
@@ -11,6 +11,9 @@
#import "UIFont+FLEX.h"
#import "FLEXUtility.h"
static const CGFloat kMargin = 5;
static const CGFloat kArrowWidth = 20;
@interface FLEXTableColumnHeader ()
@property (nonatomic, readonly) UILabel *arrowLabel;
@property (nonatomic, readonly) UIView *lineView;
@@ -60,9 +63,16 @@
CGSize size = self.frame.size;
self.titleLabel.frame = CGRectMake(5, 0, size.width - 25, size.height);
self.arrowLabel.frame = CGRectMake(size.width - 20, 0, 20, size.height);
self.titleLabel.frame = CGRectMake(kMargin, 0, size.width - kArrowWidth - kMargin, size.height);
self.arrowLabel.frame = CGRectMake(size.width - kArrowWidth, 0, kArrowWidth, size.height);
self.lineView.frame = CGRectMake(size.width - 1, 2, FLEXPointsToPixels(1), size.height - 4);
}
- (CGSize)sizeThatFits:(CGSize)size {
CGFloat margins = kArrowWidth - 2 * kMargin;
size = CGSizeMake(size.width - margins, size.height);
CGFloat width = [_titleLabel sizeThatFits:size].width + margins;
return CGSizeMake(width, size.height);
}
@end
@@ -7,10 +7,20 @@
//
#import <UIKit/UIKit.h>
#import "FLEXDatabaseManager.h"
NS_ASSUME_NONNULL_BEGIN
@interface FLEXTableContentViewController : UIViewController
/// Display a table with the given columns, rows, and name.
/// @param databaseManager an optional manager to allow modifying the table.
+ (instancetype)columns:(NSArray<NSString *> *)columnNames
rows:(NSArray<NSArray<NSString *> *> *)rowData;
rows:(NSArray<NSArray<NSString *> *> *)rowData
rowIDs:(nullable NSArray<NSString *> *)rowIds
tableName:(NSString *)tableName
database:(nullable id<FLEXDatabaseManager>)databaseManager;
@end
NS_ASSUME_NONNULL_END
@@ -10,12 +10,16 @@
#import "FLEXMultiColumnTableView.h"
#import "FLEXWebViewController.h"
#import "FLEXUtility.h"
#import "UIBarButtonItem+FLEX.h"
@interface FLEXTableContentViewController () <
FLEXMultiColumnTableViewDataSource, FLEXMultiColumnTableViewDelegate
>
@property (nonatomic, readonly) NSArray<NSString *> *columns;
@property (nonatomic, copy) NSArray<NSArray *> *rows;
@property (nonatomic) NSMutableArray<NSArray *> *rows;
@property (nonatomic, readonly) NSString *tableName;
@property (nonatomic, nullable) NSMutableArray<NSString *> *rowIDs;
@property (nonatomic, readonly, nullable) id<FLEXDatabaseManager> databaseManager;
@property (nonatomic) FLEXMultiColumnTableView *multiColumnView;
@end
@@ -23,10 +27,16 @@
@implementation FLEXTableContentViewController
+ (instancetype)columns:(NSArray<NSString *> *)columnNames
rows:(NSArray<NSArray<NSString *> *> *)rowData {
rows:(NSArray<NSArray<NSString *> *> *)rowData
rowIDs:(nullable NSArray<NSString *> *)rowIDs
tableName:(NSString *)tableName
database:(nullable id<FLEXDatabaseManager>)databaseManager {
FLEXTableContentViewController *controller = [self new];
controller->_columns = columnNames;
controller->_rows = rowData;
controller->_columns = columnNames.copy;
controller->_rows = rowData.mutableCopy;
controller->_rowIDs = rowIDs.mutableCopy;
controller->_tableName = tableName.copy;
controller->_databaseManager = databaseManager;
return controller;
}
@@ -38,9 +48,10 @@
- (void)viewDidLoad {
[super viewDidLoad];
self.title = self.tableName;
self.edgesForExtendedLayout = UIRectEdgeNone;
[self.multiColumnView reloadData];
[self setupToolbarItems];
}
- (FLEXMultiColumnTableView *)multiColumnView {
@@ -84,8 +95,8 @@
}
- (CGFloat)multiColumnTableView:(FLEXMultiColumnTableView *)tableView
widthForContentCellInColumn:(NSInteger)column {
return 120;
minWidthForContentCellInColumn:(NSInteger)column {
return 100;
}
- (CGFloat)heightForTopHeaderInTableView:(FLEXMultiColumnTableView *)tableView {
@@ -118,6 +129,27 @@
make.button(@"Copy").handler(^(NSArray<NSString *> *strings) {
UIPasteboard.generalPasteboard.string = message;
});
// Option to delete row
BOOL hasRowID = self.rows.count && row < self.rows.count;
if (hasRowID) {
make.button(@"Delete").destructiveStyle().handler(^(NSArray<NSString *> *strings) {
NSString *deleteRow = [NSString stringWithFormat:
@"DELETE FROM %@ WHERE rowid = %@",
self.tableName, self.rowIDs[row]
];
[self executeStatementAndShowResult:deleteRow completion:^(BOOL success) {
// Remove deleted row and reload view
if (success) {
[self.rowIDs removeObjectAtIndex:row];
[self.rows removeObjectAtIndex:row];
[self.multiColumnView reloadData];
}
}];
});
}
make.button(@"Dismiss").cancelStyle();
} showFrom:self];
}
@@ -127,7 +159,8 @@
sortType:(FLEXTableColumnHeaderSortType)sortType {
NSArray<NSArray *> *sortContentData = [self.rows
sortedArrayUsingComparator:^NSComparisonResult(NSArray *obj1, NSArray *obj2) {
sortedArrayWithOptions:NSSortStable
usingComparator:^NSComparisonResult(NSArray *obj1, NSArray *obj2) {
id a = obj1[column], b = obj2[column];
if (a == NSNull.null) {
return NSOrderedAscending;
@@ -135,6 +168,11 @@
if (b == NSNull.null) {
return NSOrderedDescending;
}
if ([a respondsToSelector:@selector(compare:options:)] &&
[b respondsToSelector:@selector(compare:options:)]) {
return [a compare:b options:NSNumericSearch];
}
if ([a respondsToSelector:@selector(compare:)] && [b respondsToSelector:@selector(compare:)]) {
return [a compare:b];
@@ -148,12 +186,11 @@
sortContentData = sortContentData.reverseObjectEnumerator.allObjects.copy;
}
self.rows = sortContentData;
self.rows = sortContentData.mutableCopy;
[self.multiColumnView reloadData];
}
#pragma mark -
#pragma mark About Transition
#pragma mark - About Transition
- (void)willTransitionToTraitCollection:(UITraitCollection *)newCollection
withTransitionCoordinator:(id <UIViewControllerTransitionCoordinator>)coordinator {
@@ -171,4 +208,57 @@
} completion:nil];
}
#pragma mark - Toolbar
- (void)setupToolbarItems {
// We do not support modifying realm databases
if (![self.databaseManager respondsToSelector:@selector(executeStatement:)]) {
return;
}
UIBarButtonItem *trashButton = [FLEXBarButtonItemSystem(Trash, self, @selector(trashPressed))
flex_withTintColor:UIColor.redColor
];
trashButton.enabled = self.databaseManager && self.rows.count;
self.toolbarItems = @[UIBarButtonItem.flex_flexibleSpace, trashButton];
}
- (void)trashPressed {
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(@"Delete All Rows");
make.message(@"All rows in this table will be permanently deleted.\nDo you want to proceed?");
make.button(@"Yes, I'm sure").destructiveStyle().handler(^(NSArray<NSString *> *strings) {
NSString *deleteAll = [NSString stringWithFormat:@"DELETE FROM %@", self.tableName];
[self executeStatementAndShowResult:deleteAll completion:^(BOOL success) {
// Only dismiss on success
if (success) {
[self.navigationController popViewControllerAnimated:YES];
}
}];
});
make.button(@"Cancel").cancelStyle();
} showFrom:self];
}
#pragma mark - Helpers
- (void)executeStatementAndShowResult:(NSString *)statement completion:(void (^_Nullable)(BOOL success))completion {
FLEXSQLResult *result = [self.databaseManager executeStatement:statement];
[FLEXAlert makeAlert:^(FLEXAlert *make) {
if (result.isError) {
make.title(@"Error");
}
make.message(result.message ?: @"<no output>");
make.button(@"Dismiss").cancelStyle().handler(^(NSArray<NSString *> *_) {
if (completion) {
completion(!result.isError);
}
});
} showFrom:self];
}
@end
@@ -14,6 +14,7 @@
#import "FLEXMutableListSection.h"
#import "NSArray+FLEX.h"
#import "FLEXAlert.h"
#import "FLEXMacros.h"
@interface FLEXTableListViewController ()
@property (nonatomic, readonly) id<FLEXDatabaseManager> dbm;
@@ -70,9 +71,10 @@
self.tables.selectionHandler = ^(FLEXTableListViewController *host, NSString *tableName) {
NSArray *rows = [host.dbm queryAllDataInTable:tableName];
NSArray *columns = [host.dbm queryAllColumnsOfTable:tableName];
UIViewController *resultsScreen = [FLEXTableContentViewController columns:columns rows:rows];
resultsScreen.title = tableName;
NSArray *rowIDs = [host.dbm queryRowIDsInTable:tableName];
UIViewController *resultsScreen = [FLEXTableContentViewController
columns:columns rows:rows rowIDs:rowIDs tableName:tableName database:host.dbm
];
[host.navigationController pushViewController:resultsScreen animated:YES];
};
@@ -100,7 +102,7 @@
[FLEXAlert showAlert:@"Message" message:result.message from:self];
} else {
UIViewController *resultsScreen = [FLEXTableContentViewController
columns:result.columns rows:result.rows
columns:result.columns rows:result.rows rowIDs:nil tableName:@"" database:nil
];
[self.navigationController pushViewController:resultsScreen animated:YES];
@@ -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;
+1 -1
View File
@@ -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];
});
});
@@ -358,44 +356,43 @@ typedef NS_ENUM(NSUInteger, FLEXFileBrowserSortAttribute) {
// Since our actions are outside of that protocol, we need to manually handle the action forwarding from the cells.
}
#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
- (void)openFileController:(NSString *)fullPath {
UIDocumentInteractionController *controller = [UIDocumentInteractionController new];
controller.URL = [NSURL fileURLWithPath:fullPath];
@@ -473,6 +470,9 @@ typedef NS_ENUM(NSUInteger, FLEXFileBrowserSortAttribute) {
} else {
// Share sheet for files
UIActivityViewController *shareSheet = [[UIActivityViewController alloc] initWithActivityItems:@[filePath] applicationActivities:nil];
if (UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad) {
shareSheet.popoverPresentationController.sourceView = sender;
}
[self presentViewController:shareSheet animated:true completion:nil];
}
}
@@ -34,6 +34,9 @@ extern NSString *const kFLEXKeychainClassKey;
/// Item description.
extern NSString *const kFLEXKeychainDescriptionKey;
/// Item group.
extern NSString *const kFLEXKeychainGroupKey;
/// Item label.
extern NSString *const kFLEXKeychainLabelKey;
@@ -15,6 +15,7 @@ NSString * const kFLEXKeychainAccountKey = @"acct";
NSString * const kFLEXKeychainCreatedAtKey = @"cdat";
NSString * const kFLEXKeychainClassKey = @"labl";
NSString * const kFLEXKeychainDescriptionKey = @"desc";
NSString * const kFLEXKeychainGroupKey = @"agrp";
NSString * const kFLEXKeychainLabelKey = @"labl";
NSString * const kFLEXKeychainLastModifiedKey = @"mdat";
NSString * const kFLEXKeychainWhereKey = @"svce";
@@ -170,7 +170,7 @@
- (NSString *)password {
if (self.passwordData.length) {
return [NSString 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) {
@@ -98,8 +99,9 @@
NSDictionary *item = self.section.filteredList[idx];
FLEXKeychainQuery *query = [FLEXKeychainQuery new];
query.service = item[kFLEXKeychainWhereKey];
query.account = item[kFLEXKeychainAccountKey];
query.service = [item[kFLEXKeychainWhereKey] description];
query.account = [item[kFLEXKeychainAccountKey] description];
query.accessGroup = [item[kFLEXKeychainGroupKey] description];
[query fetch:nil];
return query;
@@ -129,6 +131,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 +150,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 {
@@ -215,6 +233,7 @@
make.message(@"Service: ").message(query.service);
make.message(@"\nAccount: ").message(query.account);
make.message(@"\nPassword: ").message(query.password);
make.message(@"\nGroup: ").message(query.accessGroup);
make.button(@"Copy Service").handler(^(NSArray<NSString *> *strings) {
[UIPasteboard.generalPasteboard flex_copy:query.service];
@@ -303,14 +303,14 @@ static inline NSString * TBWildcardMap(NSString *token, NSString *candidate, TBW
if (options == TBWildcardOptionsAny) {
return [[bundles flex_flatmapped:^NSArray *(NSString *bundlePath, NSUInteger idx) {
return [self classNamesInImageAtPath:bundlePath];
}] 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;
}
@@ -84,7 +84,6 @@
switch (_appearance) {
default:
case UIKeyboardAppearanceDefault:
#if FLEX_AT_LEAST_IOS13_SDK
if (@available(iOS 13, *)) {
titleColor = UIColor.labelColor;
@@ -97,7 +96,6 @@
}
break;
}
#endif
case UIKeyboardAppearanceLight:
titleColor = UIColor.blackColor;
backgroundColor = lightColor;
@@ -61,6 +61,7 @@
searchBar.keyboardType = UIKeyboardTypeWebSearch;
searchBar.autocorrectionType = UITextAutocorrectionTypeNo;
if (@available(iOS 11, *)) {
searchBar.smartQuotesType = UITextSmartQuotesTypeNo;
searchBar.smartInsertDeleteType = UITextSmartInsertDeleteTypeNo;
}
@@ -101,7 +102,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 +131,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 +267,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;
}
@@ -106,7 +106,6 @@
switch (_appearance) {
case UIKeyboardAppearanceDefault:
#if FLEX_AT_LEAST_IOS13_SDK
if (@available(iOS 13, *)) {
borderColor = UIColor.systemBackgroundColor;
@@ -119,7 +118,6 @@
}
break;
}
#endif
case UIKeyboardAppearanceLight: {
borderColor = UIColor.clearColor;
backgroundColor = lightColor;
@@ -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,26 @@ static BOOL my_os_log_shim_enabled(void *addr) {
- (void)tableView:(UITableView *)tableView performAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender {
if (action == @selector(copy:)) {
// We usually only want to copy the log message itself, not any metadata associated with it.
UIPasteboard.generalPasteboard.string = self.logMessages.filteredList[indexPath.row].messageText;
UIPasteboard.generalPasteboard.string = self.logMessages.filteredList[indexPath.row].messageText ?: @"";
}
}
- (UIContextMenuConfiguration *)tableView:(UITableView *)tableView
contextMenuConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath
point:(CGPoint)point __IOS_AVAILABLE(13.0) {
weakify(self)
return [UIContextMenuConfiguration configurationWithIdentifier:nil previewProvider:nil
actionProvider:^UIMenu *(NSArray<UIMenuElement *> *suggestedActions) {
UIAction *copy = [UIAction actionWithTitle:@"Copy"
image:nil
identifier:@"Copy"
handler:^(UIAction *action) { strongify(self)
// We usually only want to copy the log message itself, not any metadata associated with it.
UIPasteboard.generalPasteboard.string = self.logMessages.filteredList[indexPath.row].messageText ?: @"";
}];
return [UIMenu menuWithTitle:@"" image:nil identifier:nil options:UIMenuOptionsDisplayInline children:@[copy]];
}
];
}
@end
+17 -3
View File
@@ -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.
+30 -25
View File
@@ -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;
}
+2 -2
View File
@@ -21,10 +21,10 @@ NS_ASSUME_NONNULL_BEGIN
/// The response cache uses an NSCache, so it may purge prior to hitting the limit when the app is under memory pressure.
@property (nonatomic) NSUInteger networkResponseCacheByteLimit;
/// Requests whose host ends with one of the 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
+4 -4
View File
@@ -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
+8 -4
View File
@@ -8,10 +8,6 @@
#import "FLEXExplorerToolbar.h"
#if !FLEX_AT_LEAST_IOS13_SDK
@class UIWindowScene;
#endif
NS_ASSUME_NONNULL_BEGIN
@interface FLEXManager : NSObject
@@ -25,6 +21,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));
+20 -7
View File
@@ -49,7 +49,7 @@
NSAssert(NSThread.isMainThread, @"You must use %@ from the main thread only.", NSStringFromClass([self class]));
if (!_explorerWindow) {
_explorerWindow = [[FLEXWindow alloc] initWithFrame:UIScreen.mainScreen.bounds];
_explorerWindow = [[FLEXWindow alloc] initWithFrame:FLEXUtility.appKeyWindow.bounds];
_explorerWindow.eventDelegate = self;
_explorerWindow.rootViewController = self.explorerViewController;
}
@@ -69,14 +69,12 @@
- (void)showExplorer {
UIWindow *flex = self.explorerWindow;
flex.hidden = NO;
#if FLEX_AT_LEAST_IOS13_SDK
if (@available(iOS 13.0, *)) {
// Only look for a new scene if we don't have one
if (!flex.windowScene) {
flex.windowScene = FLEXUtility.activeScene;
flex.windowScene = FLEXUtility.appKeyWindow.windowScene;
}
}
#endif
}
- (void)hideExplorer {
@@ -85,18 +83,33 @@
- (void)toggleExplorer {
if (self.explorerWindow.isHidden) {
[self showExplorer];
if (@available(iOS 13.0, *)) {
[self showExplorerFromScene:FLEXUtility.appKeyWindow.windowScene];
} else {
[self showExplorer];
}
} else {
[self hideExplorer];
}
}
- (void)dismissAnyPresentedTools:(void (^)(void))completion {
if (self.explorerViewController.presentedViewController) {
[self.explorerViewController dismissViewControllerAnimated:YES completion:completion];
} else if (completion) {
completion();
}
}
- (void)presentTool:(UINavigationController * _Nonnull (^)(void))future completion:(void (^)(void))completion {
[self showExplorer];
[self.explorerViewController presentTool:future completion:completion];
}
- (void)showExplorerFromScene:(UIWindowScene *)scene {
#if FLEX_AT_LEAST_IOS13_SDK
if (@available(iOS 13.0, *)) {
self.explorerWindow.windowScene = scene;
}
#endif
self.explorerWindow.hidden = NO;
}
+2 -1
View File
@@ -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;
@@ -390,8 +390,6 @@
}
}
#if FLEX_AT_LEAST_IOS13_SDK
- (UIContextMenuConfiguration *)tableView:(UITableView *)tableView contextMenuConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath point:(CGPoint)point __IOS_AVAILABLE(13.0) {
NSURLRequest *request = [self transactionAtIndexPath:indexPath].request;
return [UIContextMenuConfiguration
@@ -406,29 +404,27 @@
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]
];
}
];
}
#endif
- (FLEXNetworkTransaction *)transactionAtIndexPath:(NSIndexPath *)indexPath {
return self.searchController.isActive ? self.filteredNetworkTransactions[indexPath.row] : self.networkTransactions[indexPath.row];
}
+5 -5
View File
@@ -28,13 +28,13 @@ extern NSString *const kFLEXNetworkRecorderTransactionsClearedNotification;
/// with an "image", "video", or "audio" prefix.
@property (nonatomic) BOOL shouldCacheMediaResponses;
@property (nonatomic) NSMutableArray<NSString *> *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
+7 -7
View File
@@ -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;
}
+17 -17
View File
@@ -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];
}
@@ -211,8 +211,6 @@ typedef UIViewController *(^FLEXNetworkDetailRowSelectionFuture)(void);
}
}
#if FLEX_AT_LEAST_IOS13_SDK
- (UIContextMenuConfiguration *)tableView:(UITableView *)tableView contextMenuConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath point:(CGPoint)point __IOS_AVAILABLE(13.0) {
return [UIContextMenuConfiguration
configurationWithIdentifier:nil
@@ -236,8 +234,6 @@ typedef UIViewController *(^FLEXNetworkDetailRowSelectionFuture)(void);
];
}
#endif
#pragma mark - View Configuration
+ (NSAttributedString *)attributedTextForRow:(FLEXNetworkDetailRow *)row {
@@ -328,24 +324,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 +421,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]];
}
}
+41 -11
View File
@@ -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
@@ -64,24 +76,38 @@ static NSMutableDictionary<Class, Class> *classesToRegisteredSections = nil;
// shortcut section for NSObject.
//
// TODO: rename it to FLEXNSObjectShortcuts or something?
Class sectionClass = nil;
FLEXShortcutsSection *shortcutsSection = [FLEXShortcutsSection forObject:object];
NSArray *sections = @[shortcutsSection];
Class customSectionClass = nil;
Class cls = object_getClass(object);
do {
sectionClass = classesToRegisteredSections[(Class<NSCopying>)cls];
} while (!sectionClass && (cls = [cls superclass]));
customSectionClass = classesToRegisteredSections[(id<NSCopying>)cls];
} while (!customSectionClass && (cls = [cls superclass]));
if (!sectionClass) {
sectionClass = [FLEXShortcutsSection class];
if (customSectionClass) {
id customSection = [customSectionClass forObject:object];
BOOL isFLEXShortcutSection = [customSection respondsToSelector:@selector(isNewSection)];
// If the section "replaces" the default shortcuts section,
// only return that section. Otherwise, return both this
// section and the default shortcuts section.
if (isFLEXShortcutSection && ![customSection isNewSection]) {
sections = @[customSection];
} else {
// Custom section will go before shortcuts
sections = @[customSection, shortcutsSection];
}
}
return [FLEXObjectExplorerViewController
exploringObject:object
customSection:[sectionClass forObject:object]
customSections:sections
];
}
+ (void)registerExplorerSection:(Class)explorerClass forClass:(Class)objectClass {
classesToRegisteredSections[(Class<NSCopying>)objectClass] = explorerClass;
classesToRegisteredSections[(id<NSCopying>)objectClass] = explorerClass;
}
#pragma mark - FLEXGlobalsEntry
@@ -176,7 +202,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
@@ -29,6 +29,9 @@ NS_ASSUME_NONNULL_BEGIN
+ (instancetype)exploringObject:(id)objectOrClass;
/// No custom section unless you provide one.
+ (instancetype)exploringObject:(id)objectOrClass customSection:(nullable FLEXTableViewSection *)customSection;
/// No custom sections unless you provide some.
+ (instancetype)exploringObject:(id)objectOrClass
customSections:(nullable NSArray<FLEXTableViewSection *> *)customSections;
/// The object being explored, which may be an instance of a class or a class itself.
@property (nonatomic, readonly) id object;
@@ -30,7 +30,7 @@
#pragma mark - Private properties
@interface FLEXObjectExplorerViewController () <UIGestureRecognizerDelegate>
@property (nonatomic, readonly) FLEXSingleRowSection *descriptionSection;
@property (nonatomic, readonly) FLEXTableViewSection *customSection;
@property (nonatomic, readonly) NSArray<FLEXTableViewSection *> *customSections;
@property (nonatomic) NSIndexSet *customSectionVisibleIndexes;
@property (nonatomic, readonly) NSArray<NSString *> *observedNotifications;
@@ -46,23 +46,27 @@
}
+ (instancetype)exploringObject:(id)target customSection:(FLEXTableViewSection *)section {
return [self exploringObject:target customSections:@[section]];
}
+ (instancetype)exploringObject:(id)target customSections:(NSArray *)customSections {
return [[self alloc]
initWithObject:target
explorer:[FLEXObjectExplorer forObject:target]
customSection:section
customSections:customSections
];
}
- (id)initWithObject:(id)target
explorer:(__kindof FLEXObjectExplorer *)explorer
customSection:(FLEXTableViewSection *)customSection {
customSections:(NSArray<FLEXTableViewSection *> *)customSections {
NSParameterAssert(target);
self = [super initWithStyle:UITableViewStyleGrouped];
if (self) {
_object = target;
_explorer = explorer;
_customSection = customSection;
_customSections = customSections;
}
return self;
@@ -72,7 +76,8 @@
return @[
kFLEXDefaultsHidePropertyIvarsKey,
kFLEXDefaultsHidePropertyMethodsKey,
kFLEXDefaultsHideMethodOverridesKey,
kFLEXDefaultsHidePrivateMethodsKey,
kFLEXDefaultsShowMethodOverridesKey,
kFLEXDefaultsHideVariablePreviewsKey,
];
}
@@ -87,7 +92,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;
@@ -194,8 +199,10 @@
referencesSection
]];
if (self.customSection) {
[sections insertObject:self.customSection atIndex:0];
if (self.customSections) {
[sections insertObjects:self.customSections atIndexes:[NSIndexSet
indexSetWithIndexesInRange:NSMakeRange(0, self.customSections.count)
]];
}
if (self.descriptionSection) {
[sections insertObject:self.descriptionSection atIndex:0];
@@ -265,9 +272,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 +300,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 +312,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
];
@@ -189,8 +189,6 @@
cell.accessoryType = [self accessoryTypeForRow:row];
}
#if FLEX_AT_LEAST_IOS13_SDK
- (NSString *)menuSubtitleForRow:(NSInteger)row {
return [self.metadata[row] contextualSubtitleWithTarget:self.explorer.object];
}
@@ -232,6 +230,4 @@
return [self.metadata[row] copiableMetadataWithTarget:self.explorer.object];
}
#endif
@end
@@ -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
@@ -25,3 +25,7 @@
@interface FLEXShortcutsFactory (Blocks) @end
@interface FLEXShortcutsFactory (Foundation) @end
@interface FLEXShortcutsFactory (WebKit_Safari) @end
@interface FLEXShortcutsFactory (Pasteboard) @end
@@ -8,8 +8,12 @@
#import "FLEXShortcutsFactory+Defaults.h"
#import "FLEXShortcut.h"
#import "FLEXMacros.h"
#import "FLEXRuntimeUtility.h"
#import "NSArray+FLEX.h"
#import "NSObject+FLEX_Reflection.h"
#import "FLEXObjcInternal.h"
#import "Cocoa+FLEXShortcuts.h"
#pragma mark - UIApplication
@@ -59,6 +63,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 +146,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 +280,164 @@
FLEXRuntimeUtilityTryAddObjectProperty(2, abbreviationDictionary, NSTimeZone.flex_metaclass, NSDictionary);
self.append.classMethods(@[
@"timeZoneWithName:", @"timeZoneWithAbbreviation:", @"timeZoneForSecondsFromGMT:", @"", @"", @"",
@"timeZoneWithName:", @"timeZoneWithAbbreviation:", @"timeZoneForSecondsFromGMT:",
]).forClass(NSTimeZone.flex_metaclass);
self.append.classProperties(@[
@"defaultTimeZone", @"systemTimeZone", @"localTimeZone"
@"defaultTimeZone", @"systemTimeZone", @"localTimeZone",
]).forClass(NSTimeZone.class);
// UTF8String is not a real property under the hood
FLEXRuntimeUtilityTryAddNonatomicProperty(2, UTF8String, NSString.class, const char *, PropertyKey(ReadOnly));
self.append.properties(@[@"length"]).methods(@[@"characterAtIndex:"]).forClass(NSString.class);
self.append.methods(@[
@"writeToFile:atomically:", @"subdataWithRange:", @"isEqualToData:",
]).properties(@[
@"length", @"bytes",
]).forClass(NSData.class);
self.append.classMethods(@[
@"dataWithJSONObject:options:error:",
@"JSONObjectWithData:options:error:",
@"isValidJSONObject:",
]).forClass(NSJSONSerialization.class);
// NSArray
self.append.classMethods(@[
@"arrayWithObject:", @"arrayWithContentsOfFile:"
]).forClass(NSArray.flex_metaclass);
self.append.methods(@[
@"valueForKeyPath:", @"subarrayWithRange:",
@"arrayByAddingObject:", @"arrayByAddingObjectsFromArray:",
@"filteredArrayUsingPredicate:", @"subarrayWithRange:",
@"containsObject:", @"objectAtIndex:", @"indexOfObject:",
@"makeObjectsPerformSelector:", @"makeObjectsPerformSelector:withObject:",
@"sortedArrayUsingSelector:", @"reverseObjectEnumerator",
@"isEqualToArray:", @"mutableCopy",
]).forClass(NSArray.class);
// NSDictionary
self.append.methods(@[
@"objectForKey:", @"valueForKeyPath:",
@"isEqualToDictionary:", @"mutableCopy",
]).forClass(NSDictionary.class);
// NSSet
self.append.classMethods(@[
@"setWithObject:", @"setWithArray:"
]).forClass(NSSet.flex_metaclass);
self.append.methods(@[
@"allObjects", @"valueForKeyPath:", @"containsObject:",
@"setByAddingObject:", @"setByAddingObjectsFromArray:",
@"filteredSetUsingPredicate:", @"isSubsetOfSet:",
@"makeObjectsPerformSelector:", @"makeObjectsPerformSelector:withObject:",
@"reverseObjectEnumerator", @"isEqualToSet:", @"mutableCopy",
]).forClass(NSSet.class);
// NSMutableArray
self.prepend.methods(@[
@"addObject:", @"insertObject:atIndex:", @"addObjectsFromArray:",
@"removeObject:", @"removeObjectAtIndex:",
@"removeObjectsInArray:", @"removeAllObjects",
@"removeLastObject", @"filterUsingPredicate:",
@"sortUsingSelector:", @"copy",
]).forClass(NSMutableArray.class);
// NSMutableDictionary
self.prepend.methods(@[
@"setObject:forKey:", @"removeObjectForKey:",
@"removeAllObjects", @"removeObjectsForKeys:", @"copy",
]).forClass(NSMutableDictionary.class);
// NSMutableSet
self.prepend.methods(@[
@"addObject:", @"removeObject:", @"filterUsingPredicate:",
@"removeAllObjects", @"addObjectsFromArray:",
@"unionSet:", @"minusSet:", @"intersectSet:", @"copy"
]).forClass(NSMutableSet.class);
self.append.methods(@[@"nextObject", @"allObjects"]).forClass(NSEnumerator.class);
self.append.properties(@[@"flex_observers"]).forClass(NSNotificationCenter.class);
}
@end
#pragma mark - WebKit / Safari
@implementation FLEXShortcutsFactory (WebKit_Safari)
+ (void)load { FLEX_EXIT_IF_NO_CTORS()
Class WKWebView = NSClassFromString(@"WKWebView");
Class SafariVC = NSClassFromString(@"SFSafariViewController");
if (WKWebView) {
self.append.properties(@[
@"configuration", @"scrollView", @"title", @"URL",
@"customUserAgent", @"navigationDelegate"
]).methods(@[@"reload", @"stopLoading"]).forClass(WKWebView);
}
if (SafariVC) {
self.append.properties(@[
@"delegate"
]).forClass(SafariVC);
if (@available(iOS 10.0, *)) {
self.append.properties(@[
@"preferredBarTintColor", @"preferredControlTintColor"
]).forClass(SafariVC);
}
if (@available(iOS 11.0, *)) {
self.append.properties(@[
@"configuration", @"dismissButtonStyle"
]).forClass(SafariVC);
}
}
}
@end
#pragma mark - Pasteboard
@implementation FLEXShortcutsFactory (Pasteboard)
+ (void)load { FLEX_EXIT_IF_NO_CTORS()
self.append.properties(@[
@"name", @"numberOfItems", @"items",
@"string", @"image", @"color", @"URL",
]).forClass(UIPasteboard.class);
}
@end
@interface NSNotificationCenter (Observers)
@property (readonly) NSArray<NSString *> *flex_observers;
@end
@implementation NSNotificationCenter (Observers)
- (id)flex_observers {
NSString *debug = self.debugDescription;
NSArray<NSString *> *observers = [debug componentsSeparatedByString:@"\n"];
NSArray<NSArray<NSString *> *> *splitObservers = [observers flex_mapped:^id(NSString *entry, NSUInteger idx) {
return [entry componentsSeparatedByString:@","];
}];
NSArray *names = [splitObservers flex_mapped:^id(NSArray<NSString *> *entry, NSUInteger idx) {
return entry[0];
}];
NSArray *objects = [splitObservers flex_mapped:^id(NSArray<NSString *> *entry, NSUInteger idx) {
if (entry.count < 2) return NSNull.null;
NSScanner *scanner = [NSScanner scannerWithString:entry[1]];
unsigned long long objectPointerValue;
if ([scanner scanHexLongLong:&objectPointerValue]) {
void *objectPointer = (void *)objectPointerValue;
if (FLEXPointerIsValidObjcObject(objectPointer))
return (__bridge id)(void *)objectPointer;
}
return NSNull.null;
}];
return [NSArray flex_forEachUpTo:names.count map:^id(NSUInteger i) {
return @[names[i], objects[i]];
}];
}
@end
@@ -10,6 +10,8 @@
#import "FLEXObjectInfoSection.h"
@class FLEXProperty, FLEXIvar, FLEXMethod;
NS_ASSUME_NONNULL_BEGIN
/// An abstract base class for custom object "shortcuts" where every
/// row can possibly have some action. The section title is "Shortcuts".
///
@@ -25,11 +27,11 @@
@interface FLEXShortcutsSection : FLEXTableViewSection <FLEXObjectInfoSection>
/// Uses \c kFLEXDefaultCell
+ (instancetype)forObject:(id)objectOrClass rowTitles:(NSArray<NSString *> *)titles;
+ (instancetype)forObject:(id)objectOrClass rowTitles:(nullable NSArray<NSString *> *)titles;
/// Uses \c kFLEXDetailCell for non-empty subtitles, otherwise uses \c kFLEXDefaultCell
+ (instancetype)forObject:(id)objectOrClass
rowTitles:(NSArray<NSString *> *)titles
rowSubtitles:(NSArray<NSString *> *)subtitles;
rowTitles:(nullable NSArray<NSString *> *)titles
rowSubtitles:(nullable NSArray<NSString *> *)subtitles;
/// Uses \c kFLEXDefaultCell for rows that are given a title, otherwise
/// this uses \c kFLEXDetailCell for any other allowed object.
@@ -44,16 +46,15 @@
/// - a \c FLEXIvar
/// - a \c FLEXMethodBase (includes \c FLEXMethod of course)
/// Passing one of the latter 3 will provide a shortcut to that property/ivar/method.
/// @return \c nil if no rows are provided
+ (instancetype)forObject:(id)objectOrClass rows:(NSArray *)rows;
+ (instancetype)forObject:(id)objectOrClass rows:(nullable NSArray *)rows;
/// Same as \c forObject:rows: but the given rows are prepended
/// to the shortcuts already registered for the object's class.
/// \c forObject:rows: does not use the registered shortcuts at all.
+ (instancetype)forObject:(id)objectOrClass additionalRows:(NSArray *)rows;
+ (instancetype)forObject:(id)objectOrClass additionalRows:(nullable NSArray *)rows;
/// Calls into \c forObject:rows: using the registered shortcuts for the object's class.
/// @return \c nil if the object has no shortcuts registered at all
/// @return An empty section if the object has no shortcuts registered at all.
+ (instancetype)forObject:(id)objectOrClass;
/// Subclasses \e may override this to hide the disclosure indicator
@@ -72,10 +73,16 @@
/// Defaults to NO. Has no effect on static subtitles that are passed explicitly.
@property (nonatomic) BOOL cacheSubtitles;
/// Whether this shortcut section overrides the default section or not.
/// Subclasses should not override this method. To provide a second
/// section alongside the default shortcuts section, use \c forObject:rows:
/// @return \c NO if initialized with \c forObject: or \c forObject:additionalRows:
@property (nonatomic, readonly) BOOL isNewSection;
@end
@class FLEXShortcutsFactory;
typedef FLEXShortcutsFactory *(^FLEXShortcutsFactoryNames)(NSArray *names);
typedef FLEXShortcutsFactory *_Nonnull(^FLEXShortcutsFactoryNames)(NSArray *names);
typedef void (^FLEXShortcutsFactoryTarget)(Class targetClass);
/// The block properties below are to be used like SnapKit or Masonry.
@@ -123,3 +130,5 @@ typedef void (^FLEXShortcutsFactoryTarget)(Class targetClass);
@property (nonatomic, readonly) FLEXShortcutsFactoryTarget forClass;
@end
NS_ASSUME_NONNULL_END
@@ -33,6 +33,7 @@
@end
@implementation FLEXShortcutsSection
@synthesize isNewSection = _isNewSection;
#pragma mark Initialization
@@ -47,13 +48,13 @@
}
+ (instancetype)forObject:(id)objectOrClass rows:(NSArray *)rows {
return [[self alloc] initWithObject:objectOrClass rows:rows];
return [[self alloc] initWithObject:objectOrClass rows:rows isNewSection:YES];
}
+ (instancetype)forObject:(id)objectOrClass additionalRows:(NSArray *)toPrepend {
NSArray *rows = [FLEXShortcutsFactory shortcutsForObjectOrClass:objectOrClass];
NSArray *allRows = [toPrepend arrayByAddingObjectsFromArray:rows] ?: rows;
return [self forObject:objectOrClass rows:allRows];
return [[self alloc] initWithObject:objectOrClass rows:allRows isNewSection:NO];
}
+ (instancetype)forObject:(id)objectOrClass {
@@ -72,16 +73,18 @@
_object = object;
_allTitles = titles.copy;
_allSubtitles = subtitles.copy;
_isNewSection = YES;
_numberOfLines = 1;
}
return self;
}
- (id)initWithObject:object rows:(NSArray *)rows {
- (id)initWithObject:object rows:(NSArray *)rows isNewSection:(BOOL)newSection {
self = [super init];
if (self) {
_object = object;
_isNewSection = newSection;
_allShortcuts = [rows flex_mapped:^id(id obj, NSUInteger idx) {
return [FLEXShortcut shortcutFor:obj];
@@ -201,10 +204,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 +258,53 @@
}
@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 {
NSParameterAssert(objectOrClass);
NSMutableArray<id<FLEXRuntimeMetadata>> *shortcuts = [NSMutableArray new];
BOOL isClass = object_isClass(objectOrClass);
// The -class does not give you a metaclass, and we want a metaclass
@@ -325,30 +350,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 +443,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 +474,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;
}
]
]];
@@ -50,8 +50,6 @@
/// Return nil to use the default reuse identifier
- (NSString *)reuseIdentifierWithTarget:(id)object;
#if FLEX_AT_LEAST_IOS13_SDK
/// An array of actions to place in the first section of the context menu.
- (NSArray<UIAction *> *)additionalActionsWithTarget:(id)object sender:(UIViewController *)sender API_AVAILABLE(ios(13.0));
/// An array where every 2 elements are a key-value pair. The key is a description
@@ -60,8 +58,6 @@
/// Properties and ivars return the address of an object, if they hold one.
- (NSString *)contextualSubtitleWithTarget:(id)object;
#endif
@end
// Even if a property is readonly, it still may be editable
@@ -13,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"
@@ -128,18 +129,38 @@ FLEXObjectExplorerDefaultsImpl
- (NSString *)reuseIdentifierWithTarget:(id)object { return nil; }
#if FLEX_AT_LEAST_IOS13_SDK
- (NSArray<UIAction *> *)additionalActionsWithTarget:(id)object sender:(UIViewController *)sender __IOS_AVAILABLE(13.0) {
Class 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;
@@ -184,8 +205,6 @@ FLEXObjectExplorerDefaultsImpl
return nil;
}
#endif
@end
@@ -262,8 +281,6 @@ FLEXObjectExplorerDefaultsImpl
- (NSString *)reuseIdentifierWithTarget:(id)object { return nil; }
#if FLEX_AT_LEAST_IOS13_SDK
- (NSArray<UIAction *> *)additionalActionsWithTarget:(id)object sender:(UIViewController *)sender __IOS_AVAILABLE(13.0) {
Class ivarClass = self.typeEncoding.flex_typeClass;
@@ -314,8 +331,6 @@ FLEXObjectExplorerDefaultsImpl
return nil;
}
#endif
@end
@@ -360,8 +375,6 @@ FLEXObjectExplorerDefaultsImpl
- (NSString *)reuseIdentifierWithTarget:(id)object { return nil; }
#if FLEX_AT_LEAST_IOS13_SDK
- (NSArray<UIAction *> *)additionalActionsWithTarget:(id)object sender:(UIViewController *)sender __IOS_AVAILABLE(13.0) {
return nil;
}
@@ -378,8 +391,6 @@ FLEXObjectExplorerDefaultsImpl
return nil;
}
#endif
@end
@implementation FLEXMethod (UIKitHelpers)
@@ -457,8 +468,6 @@ FLEXObjectExplorerDefaultsImpl
- (NSString *)reuseIdentifierWithTarget:(id)object { return nil; }
#if FLEX_AT_LEAST_IOS13_SDK
- (NSArray<UIAction *> *)additionalActionsWithTarget:(id)object sender:(UIViewController *)sender __IOS_AVAILABLE(13.0) {
return nil;
}
@@ -476,8 +485,6 @@ FLEXObjectExplorerDefaultsImpl
return nil;
}
#endif
@end
@@ -569,8 +576,6 @@ FLEXObjectExplorerDefaultsImpl
return UITableViewCellAccessoryNone;
}
#if FLEX_AT_LEAST_IOS13_SDK
- (NSArray<UIAction *> *)additionalActionsWithTarget:(id)object sender:(UIViewController *)sender __IOS_AVAILABLE(13.0) {
return nil;
}
@@ -583,8 +588,6 @@ FLEXObjectExplorerDefaultsImpl
return nil;
}
#endif
@end
+7 -1
View File
@@ -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
+18 -2
View File
@@ -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 {
+1 -1
View File
@@ -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;
+1 -1
View File
@@ -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
@@ -27,7 +27,7 @@
@interface NSString (KeyPaths)
- (NSString *)stringByRemovingLastKeyPathComponent;
- (NSString *)stringByReplacingLastKeyPathComponent:(NSString *)replacement;
- (NSString *)flex_stringByRemovingLastKeyPathComponent;
- (NSString *)flex_stringByReplacingLastKeyPathComponent:(NSString *)replacement;
@end
@@ -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
@@ -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

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