Compare commits
130 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cd695ed106 | |||
| 69c1719159 | |||
| 4019518bf5 | |||
| 3fd8e7c77d | |||
| 99c3bcb8c5 | |||
| b587e96e70 | |||
| ef8f0a303e | |||
| cfb1e4caab | |||
| 2411c331cd | |||
| fd4b38f46d | |||
| 269e31894c | |||
| 2f2da50aed | |||
| d87779212c | |||
| 5db6a12c6e | |||
| 6d0f776102 | |||
| 6c83ddc2c7 | |||
| b510d24e13 | |||
| 6cdf2e61dc | |||
| 9b0ed83ff5 | |||
| dbe1b93f48 | |||
| 06444f1576 | |||
| 9bbf1d0d48 | |||
| 0562f15cd0 | |||
| eb63c91481 | |||
| f916174070 | |||
| 60e23e126e | |||
| afeff1b562 | |||
| 5acb33005b | |||
| 3446eff353 | |||
| ad1f1f579e | |||
| e03b5f7e5d | |||
| d010c82dd0 | |||
| 652d03c39a | |||
| 1d39669a52 | |||
| d558ca6852 | |||
| ad32ca0f05 | |||
| 67097982ea | |||
| 1342d3029c | |||
| 62dcef4644 | |||
| 7ee296143e | |||
| c6bac54597 | |||
| 5f7bce64ed | |||
| d2dde55bb1 | |||
| f1a0a5c5e5 | |||
| 0db073459e | |||
| 7c7ed9286f | |||
| aac88dd6c8 | |||
| d7376b75cd | |||
| 5b39b3ed03 | |||
| bbaa85bdbf | |||
| 34e27bc5d9 | |||
| 714307273e | |||
| 5242d3c5a1 | |||
| cf2e94a1d2 | |||
| 800acb4cad | |||
| 368ce64121 | |||
| 05f03090a9 | |||
| a8803781e8 | |||
| 170f74b297 | |||
| 0d0f2a3073 | |||
| d6bddf5199 | |||
| 258ec8697b | |||
| 8f9a6e88ec | |||
| c37270e6ac | |||
| ce10d45c29 | |||
| b2716c4b2e | |||
| ab135ba94e | |||
| bcc04f4113 | |||
| 470b3fa3b3 | |||
| e84dfeae5c | |||
| 44e86e59b7 | |||
| ac6d9cfa3f | |||
| 1a59711760 | |||
| b60ce7a057 | |||
| b4ac210bef | |||
| 9360c58975 | |||
| 43d9a460ce | |||
| 15fee5a8a5 | |||
| fad038392b | |||
| 558d65a0b0 | |||
| 077fca36c0 | |||
| 947769f6f9 | |||
| 7c480c5faf | |||
| fa6c72cb08 | |||
| 9af2926ec1 | |||
| 366a8266bd | |||
| 713fbac54a | |||
| 8198fba689 | |||
| 20e14a36c9 | |||
| 176e98518d | |||
| 31440056c1 | |||
| 6c7b39ed03 | |||
| 96989f7e0c | |||
| 22a23b3b12 | |||
| 90a855a289 | |||
| fd67373995 | |||
| 3b5e095f74 | |||
| c7850df186 | |||
| 6bbcc55cf6 | |||
| e63ea4bbff | |||
| 5a760fb1ac | |||
| e63f2ee3ad | |||
| 46c6dcb7e6 | |||
| bf42bbe27b | |||
| e89fec4b2d | |||
| 715bb92929 | |||
| 109074f98e | |||
| 45fbdb7914 | |||
| cb2e0789d8 | |||
| de1ca783b6 | |||
| 3a9c24b784 | |||
| b57a333fc9 | |||
| 288bf1343e | |||
| a0b1caed54 | |||
| 9282c61183 | |||
| ee6677ee08 | |||
| 3276eb3516 | |||
| a3fa7bbadc | |||
| 637074b354 | |||
| 547bfbaec0 | |||
| 4e1fcf4682 | |||
| 4d50fd2020 | |||
| 2c510c8ca1 | |||
| 8283e2a8e7 | |||
| 3f82631a95 | |||
| d77864494d | |||
| c5ed6d4ece | |||
| 9412f6eccf | |||
| d9aa102e77 | |||
| 6f80476135 |
@@ -0,0 +1,3 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [NSExceptional]
|
||||
@@ -0,0 +1,27 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Report a bug in FLEX
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
### Environment
|
||||
- Platform+version: **iOS 14** <!--- Change to match your platform and version -->
|
||||
- FLEX version: **9.9.9** <!--- Change to the version of FLEX you're using -->
|
||||
<!--- FLEXing / libFLEX users: please include FLEXing and libFLEX versions separately -->
|
||||
|
||||
### Bug Report
|
||||
|
||||
Here, you can provide a description of the bug. Some tips:
|
||||
|
||||
- Please do not paste an entire crash log. Upload the crash log to something like [ghostbin.co](https://ghostbin.co/) or another paste service. Alternatively, you can cut out the relevant stack trace and paste that inside a ` ```code block``` `
|
||||
- If the bug is more complex than "this button is broken" or a crash, consider including a sample project. For example, if your app's requests aren't showing up in the network history page.
|
||||
- Providing steps to reproduce is always helpful!
|
||||
- If you want to include a screenshot or GIF, consider modifying the default markdown for uploaded images to use this code to make the image smaller on desktop:
|
||||
```
|
||||
<img width="50%" src=your-image-url >
|
||||
```
|
||||
|
||||
This template is a suggestion. You may format your issue however you want, but generally you should at least include your iOS version and FLEX version.
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest a new feature for FLEX
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#import "FLEXFilteringTableViewController.h"
|
||||
#import "FLEXTableViewSection.h"
|
||||
#import "NSArray+FLEX.h"
|
||||
#import "FLEXMacros.h"
|
||||
|
||||
@interface FLEXFilteringTableViewController ()
|
||||
|
||||
@@ -73,7 +74,7 @@
|
||||
#pragma mark - Search
|
||||
|
||||
- (void)updateSearchResults:(NSString *)newText {
|
||||
NSArray *(^filter)() = ^NSArray *{
|
||||
NSArray *(^filter)(void) = ^NSArray *{
|
||||
self.filterText = newText;
|
||||
|
||||
// Sections will adjust data based on this property
|
||||
@@ -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, *)) {
|
||||
@@ -92,10 +96,17 @@
|
||||
- (void)dismissAnimated {
|
||||
// Tabs are only closed if the done button is pressed; this
|
||||
// allows you to leave a tab open by dragging down to dismiss
|
||||
[FLEXTabList.sharedList closeTab:self];
|
||||
if ([self.presentingViewController isKindOfClass:[FLEXExplorerViewController class]]) {
|
||||
[FLEXTabList.sharedList closeTab:self];
|
||||
}
|
||||
|
||||
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (BOOL)canShowToolbar {
|
||||
return self.topViewController.toolbarItems.count;
|
||||
}
|
||||
|
||||
- (void)addNavigationBarItemsToViewController:(UINavigationItem *)navigationItem {
|
||||
if (!self.presentingViewController) {
|
||||
return;
|
||||
@@ -145,8 +156,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 +180,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,13 +529,37 @@ 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 {
|
||||
[self.debounceTimer invalidate];
|
||||
NSString *text = searchController.searchBar.text;
|
||||
|
||||
void (^updateSearchResults)() = ^{
|
||||
void (^updateSearchResults)(void) = ^{
|
||||
if (self.searchResultsUpdater) {
|
||||
[self.searchResultsUpdater updateSearchResults:text];
|
||||
} else {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -12,7 +12,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FLEXDefaultEditorViewController : FLEXVariableEditorViewController
|
||||
|
||||
+ (instancetype)target:(NSUserDefaults *)defaults key:(NSString *)key commitHandler:(void(^_Nullable)())onCommit;
|
||||
+ (instancetype)target:(NSUserDefaults *)defaults key:(NSString *)key commitHandler:(void(^_Nullable)(void))onCommit;
|
||||
|
||||
+ (BOOL)canEditDefaultWithValue:(nullable id)currentValue;
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
@implementation FLEXDefaultEditorViewController
|
||||
|
||||
+ (instancetype)target:(NSUserDefaults *)defaults key:(NSString *)key commitHandler:(void(^_Nullable)())onCommit {
|
||||
+ (instancetype)target:(NSUserDefaults *)defaults key:(NSString *)key commitHandler:(void(^_Nullable)(void))onCommit {
|
||||
FLEXDefaultEditorViewController *editor = [self target:defaults data:key commitHandler:onCommit];
|
||||
editor.title = @"Edit Default";
|
||||
return editor;
|
||||
|
||||
@@ -15,9 +15,9 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
@interface FLEXFieldEditorViewController : FLEXVariableEditorViewController
|
||||
|
||||
/// @return nil if the property is readonly or if the type is unsupported
|
||||
+ (nullable instancetype)target:(id)target property:(FLEXProperty *)property commitHandler:(void(^_Nullable)())onCommit;
|
||||
+ (nullable instancetype)target:(id)target property:(FLEXProperty *)property commitHandler:(void(^_Nullable)(void))onCommit;
|
||||
/// @return nil if the ivar type is unsupported
|
||||
+ (nullable instancetype)target:(id)target ivar:(FLEXIvar *)ivar commitHandler:(void(^_Nullable)())onCommit;
|
||||
+ (nullable instancetype)target:(id)target ivar:(FLEXIvar *)ivar commitHandler:(void(^_Nullable)(void))onCommit;
|
||||
|
||||
/// Subclasses can change the button title via the \c title property
|
||||
@property (nonatomic, readonly) UIBarButtonItem *getterButton;
|
||||
|
||||
@@ -30,19 +30,14 @@
|
||||
|
||||
#pragma mark - Initialization
|
||||
|
||||
+ (instancetype)target:(id)target property:(nonnull FLEXProperty *)property commitHandler:(void(^_Nullable)())onCommit {
|
||||
id value = [property getValue:target];
|
||||
if (![self canEditProperty:property onObject:target currentValue:value]) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
+ (instancetype)target:(id)target property:(nonnull FLEXProperty *)property commitHandler:(void(^_Nullable)(void))onCommit {
|
||||
FLEXFieldEditorViewController *editor = [self target:target data:property commitHandler:onCommit];
|
||||
editor.title = [@"Property: " stringByAppendingString:property.name];
|
||||
editor.property = property;
|
||||
return editor;
|
||||
}
|
||||
|
||||
+ (instancetype)target:(id)target ivar:(nonnull FLEXIvar *)ivar commitHandler:(void(^_Nullable)())onCommit {
|
||||
+ (instancetype)target:(id)target ivar:(nonnull FLEXIvar *)ivar commitHandler:(void(^_Nullable)(void))onCommit {
|
||||
FLEXFieldEditorViewController *editor = [self target:target data:ivar commitHandler:onCommit];
|
||||
editor.title = [@"Ivar: " stringByAppendingString:ivar.name];
|
||||
editor.ivar = ivar;
|
||||
@@ -151,14 +146,4 @@
|
||||
}
|
||||
}
|
||||
|
||||
+ (BOOL)canEditProperty:(FLEXProperty *)property onObject:(id)object currentValue:(id)value {
|
||||
const FLEXTypeEncoding *typeEncoding = property.attributes.typeEncoding.UTF8String;
|
||||
BOOL canEditType = [FLEXArgumentInputViewFactory canEditFieldWithTypeEncoding:typeEncoding currentValue:value];
|
||||
return canEditType && [object respondsToSelector:property.likelySetter];
|
||||
}
|
||||
|
||||
+ (BOOL)canEditIvar:(Ivar)ivar currentValue:(id)value {
|
||||
return [FLEXArgumentInputViewFactory canEditFieldWithTypeEncoding:ivar_getTypeEncoding(ivar) currentValue:value];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -21,17 +21,17 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
@protected
|
||||
id _target;
|
||||
_Nullable id _data;
|
||||
void (^_Nullable _commitHandler)();
|
||||
void (^_Nullable _commitHandler)(void);
|
||||
}
|
||||
|
||||
/// @param target The target of the operation
|
||||
/// @param data The data associated with the operation
|
||||
/// @param onCommit An action to perform when the data changes
|
||||
+ (instancetype)target:(id)target data:(nullable id)data commitHandler:(void(^_Nullable)())onCommit;
|
||||
+ (instancetype)target:(id)target data:(nullable id)data commitHandler:(void(^_Nullable)(void))onCommit;
|
||||
/// @param target The target of the operation
|
||||
/// @param data The data associated with the operation
|
||||
/// @param onCommit An action to perform when the data changes
|
||||
- (id)initWithTarget:(id)target data:(nullable id)data commitHandler:(void(^_Nullable)())onCommit;
|
||||
- (id)initWithTarget:(id)target data:(nullable id)data commitHandler:(void(^_Nullable)(void))onCommit;
|
||||
|
||||
@property (nonatomic, readonly) id target;
|
||||
|
||||
|
||||
@@ -25,11 +25,11 @@
|
||||
|
||||
#pragma mark - Initialization
|
||||
|
||||
+ (instancetype)target:(id)target data:(nullable id)data commitHandler:(void(^_Nullable)())onCommit {
|
||||
+ (instancetype)target:(id)target data:(nullable id)data commitHandler:(void(^_Nullable)(void))onCommit {
|
||||
return [[self alloc] initWithTarget:target data:data commitHandler:onCommit];
|
||||
}
|
||||
|
||||
- (id)initWithTarget:(id)target data:(nullable id)data commitHandler:(void(^_Nullable)())onCommit {
|
||||
- (id)initWithTarget:(id)target data:(nullable id)data commitHandler:(void(^_Nullable)(void))onCommit {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_target = target;
|
||||
|
||||
@@ -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 {
|
||||
@@ -449,17 +460,12 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
initWithTarget:self action:@selector(handleToolbarDetailsTapGesture:)
|
||||
];
|
||||
[toolbar.selectedViewDescriptionContainer addGestureRecognizer:self.detailsTapGR];
|
||||
|
||||
// Swipe gestures for selecting deeper / higher views at a point
|
||||
UISwipeGestureRecognizer *leftSwipe = [[UISwipeGestureRecognizer alloc]
|
||||
UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc]
|
||||
initWithTarget:self action:@selector(handleChangeViewAtPointGesture:)
|
||||
];
|
||||
UISwipeGestureRecognizer *rightSwipe = [[UISwipeGestureRecognizer alloc]
|
||||
initWithTarget:self action:@selector(handleChangeViewAtPointGesture:)
|
||||
];
|
||||
leftSwipe.direction = UISwipeGestureRecognizerDirectionLeft;
|
||||
rightSwipe.direction = UISwipeGestureRecognizerDirectionRight;
|
||||
[toolbar.selectedViewDescriptionContainer addGestureRecognizer:leftSwipe];
|
||||
[toolbar.selectedViewDescriptionContainer addGestureRecognizer:rightSwipe];
|
||||
[toolbar.selectedViewDescriptionContainer addGestureRecognizer:panGesture];
|
||||
|
||||
// Long press gesture to present tabs manager
|
||||
[toolbar.globalsItem addGestureRecognizer:[[UILongPressGestureRecognizer alloc]
|
||||
@@ -598,19 +604,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 +913,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 +978,7 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
} else {
|
||||
return [FLEXHierarchyViewController delegate:self];
|
||||
}
|
||||
} completion:^{
|
||||
if (completion) {
|
||||
completion();
|
||||
}
|
||||
}];
|
||||
} completion:completion];
|
||||
}
|
||||
|
||||
- (void)toggleMenuTool {
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
[self dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (void)showRevertOrDismissAlert:(void(^)())revertBlock {
|
||||
- (void)showRevertOrDismissAlert:(void(^)(void))revertBlock {
|
||||
[self.tableView deselectRowAtIndexPath:self.tableView.indexPathForSelectedRow animated:YES];
|
||||
[self reloadData];
|
||||
[self.tableView reloadData];
|
||||
|
||||
@@ -80,10 +80,10 @@
|
||||
|
||||
- (void)closeTab:(UINavigationController *)tab {
|
||||
NSParameterAssert(tab);
|
||||
NSParameterAssert([self.openTabs containsObject:tab]);
|
||||
NSInteger idx = [self.openTabs indexOfObject:tab];
|
||||
|
||||
[self closeTabAtIndex:idx];
|
||||
if (idx != NSNotFound) {
|
||||
[self closeTabAtIndex:idx];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)closeTabAtIndex:(NSInteger)idx {
|
||||
|
||||
@@ -12,16 +12,11 @@
|
||||
#import <FLEX/CALayer+FLEX.h>
|
||||
#import <FLEX/UIFont+FLEX.h>
|
||||
#import <FLEX/UIGestureRecognizer+Blocks.h>
|
||||
#import <FLEX/UIView+FLEX_Layout.h>
|
||||
#import <FLEX/UIPasteboard+FLEX.h>
|
||||
#import <FLEX/UIMenu+FLEX.h>
|
||||
#import <FLEX/UITextField+Range.h>
|
||||
|
||||
#import <FLEX/NSObject+FLEX_Reflection.h>
|
||||
#import <FLEX/NSArray+FLEX.h>
|
||||
#import <FLEX/NSDictionary+ObjcRuntime.h>
|
||||
#import <FLEX/NSString+ObjcRuntime.h>
|
||||
#import <FLEX/NSString+FLEX.h>
|
||||
#import <FLEX/NSUserDefaults+FLEX.h>
|
||||
#import <FLEX/NSMapTable+FLEX_Subscripting.h>
|
||||
#import <FLEX/NSTimer+FLEX.h>
|
||||
|
||||
@@ -20,6 +20,5 @@
|
||||
#import <FLEX/FLEX-Categories.h>
|
||||
#import <FLEX/FLEX-ObjectExploring.h>
|
||||
|
||||
#import <FLEX/FLEXMacros.h>
|
||||
#import <FLEX/FLEXAlert.h>
|
||||
#import <FLEX/FLEXResources.h>
|
||||
|
||||
@@ -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"];
|
||||
@@ -225,7 +226,10 @@ static const NSInteger kFLEXLiveObjectsSortBySizeIndex = 2;
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
NSString *className = self.filteredClassNames[indexPath.row];
|
||||
UIViewController *instances = [FLEXObjectListViewController instancesOfClassWithName:className];
|
||||
UIViewController *instances = [FLEXObjectListViewController
|
||||
instancesOfClassWithName:className
|
||||
retained:YES
|
||||
];
|
||||
[self.navigationController pushViewController:instances animated:YES];
|
||||
}
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
|
||||
/// This will either return a list of the instances, or take you straight
|
||||
/// to the explorer itself if there is only one instance.
|
||||
+ (UIViewController *)instancesOfClassWithName:(NSString *)className;
|
||||
+ (UIViewController *)instancesOfClassWithName:(NSString *)className retained:(BOOL)retain;
|
||||
+ (instancetype)subclassesOfClassWithName:(NSString *)className;
|
||||
+ (instancetype)objectsWithReferencesToObject:(id)object;
|
||||
+ (instancetype)objectsWithReferencesToObject:(id)object retained:(BOOL)retain;
|
||||
|
||||
@end
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -111,45 +141,33 @@
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (UIViewController *)instancesOfClassWithName:(NSString *)className {
|
||||
const char *classNameCString = className.UTF8String;
|
||||
NSMutableArray *instances = [NSMutableArray new];
|
||||
[FLEXHeapEnumerator enumerateLiveObjectsUsingBlock:^(__unsafe_unretained id object, __unsafe_unretained Class actualClass) {
|
||||
if (strcmp(classNameCString, class_getName(actualClass)) == 0) {
|
||||
// Note: objects of certain classes crash when retain is called.
|
||||
// It is up to the user to avoid tapping into instance lists for these classes.
|
||||
// Ex. OS_dispatch_queue_specific_queue
|
||||
// In the future, we could provide some kind of warning for classes that are known to be problematic.
|
||||
if (malloc_size((__bridge const void *)(object)) > 0) {
|
||||
[instances addObject:object];
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
NSArray<FLEXObjectRef *> *references = [FLEXObjectRef referencingAll:instances];
|
||||
+ (UIViewController *)instancesOfClassWithName:(NSString *)className retained:(BOOL)retain {
|
||||
NSArray<FLEXObjectRef *> *references = [FLEXHeapEnumerator
|
||||
instancesOfClassWithName:className retained:retain
|
||||
];
|
||||
|
||||
if (references.count == 1) {
|
||||
return [FLEXObjectExplorerFactory
|
||||
explorerViewControllerForObject:references.firstObject.object
|
||||
explorerViewControllerForObject:references.firstObject.object
|
||||
];
|
||||
}
|
||||
|
||||
FLEXObjectListViewController *controller = [[self alloc] initWithReferences:references];
|
||||
controller.title = [NSString stringWithFormat:@"%@ (%lu)", className, (unsigned long)instances.count];
|
||||
controller.title = [NSString stringWithFormat:@"%@ (%@)", className, @(references.count)];
|
||||
return controller;
|
||||
}
|
||||
|
||||
+ (instancetype)subclassesOfClassWithName:(NSString *)className {
|
||||
NSArray<Class> *classes = FLEXGetAllSubclasses(NSClassFromString(className), NO);
|
||||
NSArray<FLEXObjectRef *> *references = [FLEXObjectRef referencingClasses:classes];
|
||||
NSArray<FLEXObjectRef *> *references = [FLEXHeapEnumerator subclassesOfClassWithName:className];
|
||||
FLEXObjectListViewController *controller = [[self alloc] initWithReferences:references];
|
||||
controller.title = [NSString stringWithFormat:@"Subclasses of %@ (%lu)",
|
||||
className, (unsigned long)classes.count
|
||||
controller.title = [NSString stringWithFormat:@"Subclasses of %@ (%@)",
|
||||
className, @(references.count)
|
||||
];
|
||||
|
||||
return controller;
|
||||
}
|
||||
|
||||
+ (instancetype)objectsWithReferencesToObject:(id)object {
|
||||
+ (instancetype)objectsWithReferencesToObject:(id)object retained:(BOOL)retain {
|
||||
static Class SwiftObjectClass = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
@@ -159,44 +177,17 @@
|
||||
}
|
||||
});
|
||||
|
||||
NSMutableArray<FLEXObjectRef *> *instances = [NSMutableArray new];
|
||||
[FLEXHeapEnumerator enumerateLiveObjectsUsingBlock:^(__unsafe_unretained id tryObject, __unsafe_unretained Class actualClass) {
|
||||
// Get all the ivars on the object. Start with the class and and travel up the inheritance chain.
|
||||
// Once we find a match, record it and move on to the next object. There's no reason to find multiple matches within the same object.
|
||||
Class tryClass = actualClass;
|
||||
while (tryClass) {
|
||||
unsigned int ivarCount = 0;
|
||||
Ivar *ivars = class_copyIvarList(tryClass, &ivarCount);
|
||||
NSArray<FLEXObjectRef *> *instances = [FLEXHeapEnumerator
|
||||
objectsWithReferencesToObject:object retained:retain
|
||||
];
|
||||
|
||||
for (unsigned int ivarIndex = 0; ivarIndex < ivarCount; ivarIndex++) {
|
||||
Ivar ivar = ivars[ivarIndex];
|
||||
NSString *typeEncoding = @(ivar_getTypeEncoding(ivar) ?: "");
|
||||
|
||||
if (typeEncoding.flex_typeIsObjectOrClass) {
|
||||
ptrdiff_t offset = ivar_getOffset(ivar);
|
||||
uintptr_t *fieldPointer = (__bridge void *)tryObject + offset;
|
||||
|
||||
if (*fieldPointer == (uintptr_t)(__bridge void *)object) {
|
||||
NSString *ivarName = @(ivar_getName(ivar) ?: "???");
|
||||
[instances addObject:[FLEXObjectRef referencing:tryObject ivar:ivarName]];
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 +222,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 +237,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;
|
||||
|
||||
@@ -10,10 +10,19 @@
|
||||
|
||||
@interface FLEXObjectRef : NSObject
|
||||
|
||||
+ (instancetype)referencing:(id)object;
|
||||
+ (instancetype)referencing:(id)object ivar:(NSString *)ivarName;
|
||||
/// Reference an object without affecting its lifespan or or emitting reference-counting operations.
|
||||
+ (instancetype)unretained:(__unsafe_unretained id)object;
|
||||
+ (instancetype)unretained:(__unsafe_unretained id)object ivar:(NSString *)ivarName;
|
||||
|
||||
+ (NSArray<FLEXObjectRef *> *)referencingAll:(NSArray *)objects;
|
||||
/// Reference an object and control its lifespan.
|
||||
+ (instancetype)retained:(id)object;
|
||||
+ (instancetype)retained:(id)object ivar:(NSString *)ivarName;
|
||||
|
||||
/// Reference an object and conditionally choose to retain it or not.
|
||||
+ (instancetype)referencing:(__unsafe_unretained id)object retained:(BOOL)retain;
|
||||
+ (instancetype)referencing:(__unsafe_unretained id)object ivar:(NSString *)ivarName retained:(BOOL)retain;
|
||||
|
||||
+ (NSArray<FLEXObjectRef *> *)referencingAll:(NSArray *)objects retained:(BOOL)retain;
|
||||
/// Classes do not have a summary, and the reference is just the class name.
|
||||
+ (NSArray<FLEXObjectRef *> *)referencingClasses:(NSArray<Class> *)classes;
|
||||
|
||||
@@ -22,6 +31,11 @@
|
||||
/// For instances, this is the result of -[FLEXRuntimeUtility summaryForObject:]
|
||||
/// For classes, there is no summary.
|
||||
@property (nonatomic, readonly) NSString *summary;
|
||||
@property (nonatomic, readonly) id object;
|
||||
@property (nonatomic, readonly, unsafe_unretained) id object;
|
||||
|
||||
/// Retains the referenced object if it is not already retained
|
||||
- (void)retainObject;
|
||||
/// Releases the referenced object if it is already retained
|
||||
- (void)releaseObject;
|
||||
|
||||
@end
|
||||
|
||||
@@ -10,44 +10,70 @@
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
#import "NSArray+FLEX.h"
|
||||
|
||||
@interface FLEXObjectRef ()
|
||||
@interface FLEXObjectRef () {
|
||||
/// Used to retain the object if desired
|
||||
id _retainer;
|
||||
}
|
||||
@property (nonatomic, readonly) BOOL wantsSummary;
|
||||
@end
|
||||
|
||||
@implementation FLEXObjectRef
|
||||
@synthesize summary = _summary;
|
||||
|
||||
+ (instancetype)referencing:(id)object {
|
||||
return [self referencing:object showSummary:YES];
|
||||
+ (instancetype)unretained:(__unsafe_unretained id)object {
|
||||
return [self referencing:object showSummary:YES retained:NO];
|
||||
}
|
||||
|
||||
+ (instancetype)referencing:(id)object showSummary:(BOOL)showSummary {
|
||||
return [[self alloc] initWithObject:object ivarName:nil showSummary:showSummary];
|
||||
+ (instancetype)unretained:(__unsafe_unretained id)object ivar:(NSString *)ivarName {
|
||||
return [[self alloc] initWithObject:object ivarName:ivarName showSummary:YES retained:NO];
|
||||
}
|
||||
|
||||
+ (instancetype)referencing:(id)object ivar:(NSString *)ivarName {
|
||||
return [[self alloc] initWithObject:object ivarName:ivarName showSummary:YES];
|
||||
+ (instancetype)retained:(id)object {
|
||||
return [self referencing:object showSummary:YES retained:YES];
|
||||
}
|
||||
|
||||
+ (NSArray<FLEXObjectRef *> *)referencingAll:(NSArray *)objects {
|
||||
+ (instancetype)retained:(id)object ivar:(NSString *)ivarName {
|
||||
return [[self alloc] initWithObject:object ivarName:ivarName showSummary:YES retained:YES];
|
||||
}
|
||||
|
||||
+ (instancetype)referencing:(__unsafe_unretained id)object retained:(BOOL)retain {
|
||||
return retain ? [self retained:object] : [self unretained:object];
|
||||
}
|
||||
|
||||
+ (instancetype)referencing:(__unsafe_unretained id)object ivar:(NSString *)ivarName retained:(BOOL)retain {
|
||||
return retain ? [self retained:object ivar:ivarName] : [self unretained:object ivar:ivarName];
|
||||
}
|
||||
|
||||
+ (instancetype)referencing:(__unsafe_unretained id)object showSummary:(BOOL)showSummary retained:(BOOL)retain {
|
||||
return [[self alloc] initWithObject:object ivarName:nil showSummary:showSummary retained:retain];
|
||||
}
|
||||
|
||||
+ (NSArray<FLEXObjectRef *> *)referencingAll:(NSArray *)objects retained:(BOOL)retain {
|
||||
return [objects flex_mapped:^id(id obj, NSUInteger idx) {
|
||||
return [self referencing:obj showSummary:YES];
|
||||
return [self referencing:obj showSummary:YES retained:retain];
|
||||
}];
|
||||
}
|
||||
|
||||
+ (NSArray<FLEXObjectRef *> *)referencingClasses:(NSArray<Class> *)classes {
|
||||
return [classes flex_mapped:^id(id obj, NSUInteger idx) {
|
||||
return [self referencing:obj showSummary:NO];
|
||||
return [self referencing:obj showSummary:NO retained:NO];
|
||||
}];
|
||||
}
|
||||
|
||||
- (id)initWithObject:(id)object ivarName:(NSString *)ivar showSummary:(BOOL)showSummary {
|
||||
- (id)initWithObject:(__unsafe_unretained id)object
|
||||
ivarName:(NSString *)ivar
|
||||
showSummary:(BOOL)showSummary
|
||||
retained:(BOOL)retain {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_object = object;
|
||||
_wantsSummary = showSummary;
|
||||
|
||||
if (retain) {
|
||||
_retainer = object;
|
||||
}
|
||||
|
||||
NSString *class = NSStringFromClass(object_getClass(object));
|
||||
NSString *class = [FLEXRuntimeUtility safeClassNameForObject:object];
|
||||
if (ivar) {
|
||||
_reference = [NSString stringWithFormat:@"%@ %@", class, ivar];
|
||||
} else if (showSummary) {
|
||||
@@ -73,4 +99,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (void)retainObject {
|
||||
if (!_retainer) {
|
||||
_retainer = _object;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)releaseObject {
|
||||
_retainer = nil;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -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];
|
||||
@@ -38,7 +38,7 @@
|
||||
self = [self initWithNibName:nil bundle:nil];
|
||||
if (self) {
|
||||
self.originalText = text;
|
||||
NSString *htmlString = [NSString stringWithFormat:@"<head><meta name='viewport' content='initial-scale=1.0'></head><body><pre>%@</pre></body>", [FLEXUtility stringByEscapingHTMLEntitiesInString:text]];
|
||||
NSString *htmlString = [NSString stringWithFormat:@"<head><style>:root{ color-scheme: light dark; }</style><meta name='viewport' content='initial-scale=1.0'></head><body><pre>%@</pre></body>", [FLEXUtility stringByEscapingHTMLEntitiesInString:text]];
|
||||
[self.webView loadHTMLString:htmlString baseURL:nil];
|
||||
}
|
||||
return self;
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -195,7 +195,7 @@ static inline NSString * TBWildcardMap(NSString *token, NSString *candidate, TBW
|
||||
"/System/Library/PrivateFrameworks/WebKitLegacy.framework/WebKitLegacy",
|
||||
RTLD_LAZY
|
||||
);
|
||||
void (*WebKitInitialize)() = dlsym(handle, "WebKitInitialize");
|
||||
void (*WebKitInitialize)(void) = dlsym(handle, "WebKitInitialize");
|
||||
if (WebKitInitialize) {
|
||||
NSAssert(NSThread.isMainThread,
|
||||
@"WebKitInitialize can only be called on the main thread"
|
||||
@@ -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;
|
||||
|
||||
@@ -10,9 +10,12 @@
|
||||
#import "FLEXKeyPathSearchController.h"
|
||||
#import "FLEXRuntimeBrowserToolbar.h"
|
||||
#import "UIGestureRecognizer+Blocks.h"
|
||||
#import "UIBarButtonItem+FLEX.h"
|
||||
#import "FLEXTableView.h"
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
#import "FLEXAlert.h"
|
||||
#import "FLEXRuntimeClient.h"
|
||||
#import <dlfcn.h>
|
||||
|
||||
@interface FLEXObjcRuntimeViewController () <FLEXKeyPathSearchControllerDelegate>
|
||||
|
||||
@@ -28,9 +31,26 @@
|
||||
- (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)
|
||||
]
|
||||
];
|
||||
|
||||
[self addToolbarItems:@[FLEXBarButtonItem(@"dlopen()", self, @selector(dlopenPressed:))]];
|
||||
|
||||
// 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,13 +77,61 @@
|
||||
[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 dlopen
|
||||
|
||||
/// Prompt user for dlopen shortcuts to choose from
|
||||
- (void)dlopenPressed:(id)sender {
|
||||
[FLEXAlert makeAlert:^(FLEXAlert *make) {
|
||||
make.title(@"Dynamically Open Library");
|
||||
make.message(@"Invoke dlopen() with the given path. Choose an option below.");
|
||||
|
||||
make.button(@"System Framework").handler(^(NSArray<NSString *> *_) {
|
||||
[self dlopenWithFormat:@"/System/Library/Frameworks/%@.framework/%@"];
|
||||
});
|
||||
make.button(@"System Private Framework").handler(^(NSArray<NSString *> *_) {
|
||||
[self dlopenWithFormat:@"/System/Library/PrivateFrameworks/%@.framework/%@"];
|
||||
});
|
||||
make.button(@"Arbitrary Binary").handler(^(NSArray<NSString *> *_) {
|
||||
[self dlopenWithFormat:nil];
|
||||
});
|
||||
|
||||
make.button(@"Cancel").cancelStyle();
|
||||
} showFrom:self];
|
||||
}
|
||||
|
||||
/// Prompt user for input and dlopen
|
||||
- (void)dlopenWithFormat:(NSString *)format {
|
||||
[FLEXAlert makeAlert:^(FLEXAlert *make) {
|
||||
make.title(@"Dynamically Open Library");
|
||||
if (format) {
|
||||
make.message(@"Pass in a framework name, such as CarKit or FrontBoard.");
|
||||
} else {
|
||||
make.message(@"Pass in an absolute path to a binary.");
|
||||
}
|
||||
|
||||
make.textField(format ? @"ARKit" : @"/System/Library/Frameworks/ARKit.framework/ARKit");
|
||||
|
||||
make.button(@"Cancel").cancelStyle();
|
||||
make.button(@"Open").destructiveStyle().handler(^(NSArray<NSString *> *strings) {
|
||||
NSString *path = strings[0];
|
||||
|
||||
if (path.length < 2) {
|
||||
[self dlopenInvalidPath];
|
||||
} else if (format) {
|
||||
path = [NSString stringWithFormat:format, path, path];
|
||||
}
|
||||
|
||||
dlopen(path.UTF8String, RTLD_NOW);
|
||||
});
|
||||
} showFrom:self];
|
||||
}
|
||||
|
||||
- (void)dlopenInvalidPath {
|
||||
[FLEXAlert makeAlert:^(FLEXAlert * _Nonnull make) {
|
||||
make.title(@"Path or Name Too Short");
|
||||
make.button(@"Dismiss").cancelStyle();
|
||||
} showFrom:self];
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -168,11 +168,10 @@ static uint8_t (*OSLogGetType)(void *);
|
||||
NSDate *date = [NSDate dateWithTimeIntervalSince1970:log_message->tv_gmt.tv_sec];
|
||||
|
||||
// Get log message text
|
||||
const char *messageText = OSLogCopyFormattedMessage(log_message);
|
||||
// https://github.com/limneos/oslog/issues/1
|
||||
if (entry->log_message.format && !(strcmp(entry->log_message.format, messageText))) {
|
||||
messageText = (char *)entry->log_message.format;
|
||||
}
|
||||
// https://github.com/FLEXTool/FLEX/issues/564
|
||||
const char *messageText = OSLogCopyFormattedMessage(log_message) ?: "";
|
||||
|
||||
// move messageText from stack to heap
|
||||
NSString *msg = [NSString stringWithUTF8String:messageText];
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
#import "FLEXManager.h"
|
||||
#import "FLEXGlobalsEntry.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@@ -14,7 +15,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
#pragma mark - Globals Screen Entries
|
||||
|
||||
/// Adds an entry at the bottom of the list of Global State items.
|
||||
/// Adds an entry at the top of the list of Global State items.
|
||||
/// Call this method before this view controller is displayed.
|
||||
/// @param entryName The string to be displayed in the cell.
|
||||
/// @param objectFutureBlock When you tap on the row, information about the object returned
|
||||
@@ -26,7 +27,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
/// you may want to use __weak references.
|
||||
- (void)registerGlobalEntryWithName:(NSString *)entryName objectFutureBlock:(id (^)(void))objectFutureBlock;
|
||||
|
||||
/// Adds an entry at the bottom of the list of Global State items.
|
||||
/// Adds an entry at the top of the list of Global State items.
|
||||
/// Call this method before this view controller is displayed.
|
||||
/// @param entryName The string to be displayed in the cell.
|
||||
/// @param viewControllerFutureBlock When you tap on the row, view controller returned
|
||||
@@ -34,10 +35,23 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
/// @note This method must be called from the main thread.
|
||||
/// The viewControllerFutureBlock will be invoked from the main thread and may not return nil.
|
||||
/// @note The passed block will be copied and retain for the duration of the application,
|
||||
/// you may want to use __weak references.
|
||||
/// you may want to use __weak references as needed.
|
||||
- (void)registerGlobalEntryWithName:(NSString *)entryName
|
||||
viewControllerFutureBlock:(UIViewController * (^)(void))viewControllerFutureBlock;
|
||||
|
||||
/// Adds an entry at the top of the list of Global State items.
|
||||
/// @param entryName The string to be displayed in the cell.
|
||||
/// @param rowSelectedAction When you tap on the row, this block will be invoked
|
||||
/// with the host table view view controller. Use it to deselect the row or present an alert.
|
||||
/// @note This method must be called from the main thread.
|
||||
/// The rowSelectedAction will be invoked from the main thread.
|
||||
/// @note The passed block will be copied and retain for the duration of the application,
|
||||
/// you may want to use __weak references as needed.
|
||||
- (void)registerGlobalEntryWithName:(NSString *)entryName action:(FLEXGlobalsEntryRowAction)rowSelectedAction;
|
||||
|
||||
/// Removes all registered global entries.
|
||||
- (void)clearGlobalEntries;
|
||||
|
||||
#pragma mark - Simulator Shortcuts
|
||||
|
||||
/// Simulator keyboard shortcuts are enabled by default.
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
#import "FLEXManager+Extensibility.h"
|
||||
#import "FLEXManager+Private.h"
|
||||
#import "FLEXNavigationController.h"
|
||||
#import "FLEXGlobalsEntry.h"
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
#import "FLEXKeyboardShortcutManager.h"
|
||||
#import "FLEXExplorerViewController.h"
|
||||
@@ -58,6 +57,23 @@
|
||||
[self.userGlobalEntries addObject:entry];
|
||||
}
|
||||
|
||||
- (void)registerGlobalEntryWithName:(NSString *)entryName action:(FLEXGlobalsEntryRowAction)rowSelectedAction {
|
||||
NSParameterAssert(entryName);
|
||||
NSParameterAssert(rowSelectedAction);
|
||||
NSAssert(NSThread.isMainThread, @"This method must be called from the main thread.");
|
||||
|
||||
entryName = entryName.copy;
|
||||
FLEXGlobalsEntry *entry = [FLEXGlobalsEntry entryWithNameFuture:^NSString * _Nonnull{
|
||||
return entryName;
|
||||
} action:rowSelectedAction];
|
||||
|
||||
[self.userGlobalEntries addObject:entry];
|
||||
}
|
||||
|
||||
- (void)clearGlobalEntries {
|
||||
[self.userGlobalEntries removeAllObjects];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Simulator Shortcuts
|
||||
|
||||
@@ -95,60 +111,49 @@
|
||||
[self registerDefaultSimulatorShortcutWithKey:@"f" modifiers:0 action:^{
|
||||
[self toggleExplorer];
|
||||
} description:@"Toggle FLEX toolbar"];
|
||||
|
||||
|
||||
[self registerDefaultSimulatorShortcutWithKey:@"g" modifiers:0 action:^{
|
||||
[self showExplorerIfNeeded];
|
||||
[self.explorerViewController toggleMenuTool];
|
||||
} description:@"Toggle FLEX globals menu"];
|
||||
|
||||
|
||||
[self registerDefaultSimulatorShortcutWithKey:@"v" modifiers:0 action:^{
|
||||
[self showExplorerIfNeeded];
|
||||
[self.explorerViewController toggleViewsTool];
|
||||
} description:@"Toggle view hierarchy menu"];
|
||||
|
||||
|
||||
[self registerDefaultSimulatorShortcutWithKey:@"s" modifiers:0 action:^{
|
||||
[self showExplorerIfNeeded];
|
||||
[self.explorerViewController toggleSelectTool];
|
||||
} description:@"Toggle select tool"];
|
||||
|
||||
|
||||
[self registerDefaultSimulatorShortcutWithKey:@"m" modifiers:0 action:^{
|
||||
[self showExplorerIfNeeded];
|
||||
[self.explorerViewController toggleMoveTool];
|
||||
} description:@"Toggle move tool"];
|
||||
|
||||
|
||||
[self registerDefaultSimulatorShortcutWithKey:@"n" modifiers:0 action:^{
|
||||
[self toggleTopViewControllerOfClass:[FLEXNetworkMITMViewController class]];
|
||||
} description:@"Toggle network history view"];
|
||||
|
||||
// 't' is for testing: quickly present an object explorer for debugging
|
||||
[self registerDefaultSimulatorShortcutWithKey:@"t" modifiers:0 action:^{
|
||||
[self showExplorerIfNeeded];
|
||||
|
||||
[self.explorerViewController toggleToolWithViewControllerProvider:^UINavigationController *{
|
||||
return [FLEXNavigationController withRootViewController:[FLEXObjectExplorerFactory
|
||||
explorerViewControllerForObject:NSBundle.mainBundle
|
||||
]];
|
||||
} completion:nil];
|
||||
} description:@"Present an object explorer for debugging"];
|
||||
|
||||
|
||||
[self registerDefaultSimulatorShortcutWithKey:UIKeyInputDownArrow modifiers:0 action:^{
|
||||
if (self.isHidden || ![self.explorerViewController handleDownArrowKeyPressed]) {
|
||||
[self tryScrollDown];
|
||||
}
|
||||
} description:@"Cycle view selection\n\t\tMove view down\n\t\tScroll down"];
|
||||
|
||||
|
||||
[self registerDefaultSimulatorShortcutWithKey:UIKeyInputUpArrow modifiers:0 action:^{
|
||||
if (self.isHidden || ![self.explorerViewController handleUpArrowKeyPressed]) {
|
||||
[self tryScrollUp];
|
||||
}
|
||||
} description:@"Cycle view selection\n\t\tMove view up\n\t\tScroll up"];
|
||||
|
||||
|
||||
[self registerDefaultSimulatorShortcutWithKey:UIKeyInputRightArrow modifiers:0 action:^{
|
||||
if (!self.isHidden) {
|
||||
[self.explorerViewController handleRightArrowKeyPressed];
|
||||
}
|
||||
} description:@"Move selected view right"];
|
||||
|
||||
|
||||
[self registerDefaultSimulatorShortcutWithKey:UIKeyInputLeftArrow modifiers:0 action:^{
|
||||
if (self.isHidden) {
|
||||
[self tryGoBack];
|
||||
@@ -156,15 +161,15 @@
|
||||
[self.explorerViewController handleLeftArrowKeyPressed];
|
||||
}
|
||||
} description:@"Move selected view left"];
|
||||
|
||||
|
||||
[self registerDefaultSimulatorShortcutWithKey:@"?" modifiers:0 action:^{
|
||||
[self toggleTopViewControllerOfClass:[FLEXKeyboardHelpViewController class]];
|
||||
} description:@"Toggle (this) help menu"];
|
||||
|
||||
|
||||
[self registerDefaultSimulatorShortcutWithKey:UIKeyInputEscape modifiers:0 action:^{
|
||||
[[self.topViewController presentingViewController] dismissViewControllerAnimated:YES completion:nil];
|
||||
} description:@"End editing text\n\t\tDismiss top view controller"];
|
||||
|
||||
|
||||
[self registerDefaultSimulatorShortcutWithKey:@"o" modifiers:UIKeyModifierCommand|UIKeyModifierShift action:^{
|
||||
[self toggleTopViewControllerOfClass:[FLEXFileBrowserController class]];
|
||||
} description:@"Toggle file browser menu"];
|
||||
@@ -183,7 +188,7 @@
|
||||
if (@available(iOS 11, *)) {
|
||||
return scrollView.adjustedContentInset;
|
||||
}
|
||||
|
||||
|
||||
return scrollView.contentInset;
|
||||
}
|
||||
|
||||
|
||||
@@ -21,10 +21,10 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
/// The response cache uses an NSCache, so it may purge prior to hitting the limit when the app is under memory pressure.
|
||||
@property (nonatomic) NSUInteger networkResponseCacheByteLimit;
|
||||
|
||||
/// Requests whose host ends with one of the blacklisted entries in this array will be not be recorded (eg. google.com).
|
||||
/// Requests whose host ends with one of the excluded entries in this array will be not be recorded (eg. google.com).
|
||||
/// Wildcard or subdomain entries are not required (eg. google.com will match any subdomain under google.com).
|
||||
/// Useful to remove requests that are typically noisy, such as analytics requests that you aren't interested in tracking.
|
||||
@property (nonatomic) NSMutableArray<NSString *> *networkRequestHostBlacklist;
|
||||
@property (nonatomic) NSMutableArray<NSString *> *networkRequestHostDenylist;
|
||||
|
||||
/// Sets custom viewer for specific content type.
|
||||
/// @param contentType Mime type like application/json
|
||||
|
||||
@@ -48,12 +48,12 @@
|
||||
FLEXNetworkRecorder.defaultRecorder.responseCacheByteLimit = networkResponseCacheByteLimit;
|
||||
}
|
||||
|
||||
- (NSMutableArray<NSString *> *)networkRequestHostBlacklist {
|
||||
return FLEXNetworkRecorder.defaultRecorder.hostBlacklist;
|
||||
- (NSMutableArray<NSString *> *)networkRequestHostDenylist {
|
||||
return FLEXNetworkRecorder.defaultRecorder.hostDenylist;
|
||||
}
|
||||
|
||||
- (void)setNetworkRequestHostBlacklist:(NSMutableArray<NSString *> *)networkRequestHostBlacklist {
|
||||
FLEXNetworkRecorder.defaultRecorder.hostBlacklist = networkRequestHostBlacklist;
|
||||
- (void)setNetworkRequestHostDenylist:(NSMutableArray<NSString *> *)networkRequestHostDenylist {
|
||||
FLEXNetworkRecorder.defaultRecorder.hostDenylist = networkRequestHostDenylist;
|
||||
}
|
||||
|
||||
- (void)setCustomViewerForContentType:(NSString *)contentType
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// FLEXHTTPTransactionDetailController.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 2/10/15.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@class FLEXHTTPTransaction;
|
||||
|
||||
@interface FLEXHTTPTransactionDetailController : UITableViewController
|
||||
|
||||
+ (instancetype)withTransaction:(FLEXHTTPTransaction *)transaction;
|
||||
|
||||
@end
|
||||
+48
-47
@@ -7,7 +7,7 @@
|
||||
//
|
||||
|
||||
#import "FLEXColor.h"
|
||||
#import "FLEXNetworkTransactionDetailController.h"
|
||||
#import "FLEXHTTPTransactionDetailController.h"
|
||||
#import "FLEXNetworkCurlLogger.h"
|
||||
#import "FLEXNetworkRecorder.h"
|
||||
#import "FLEXNetworkTransaction.h"
|
||||
@@ -22,64 +22,63 @@
|
||||
typedef UIViewController *(^FLEXNetworkDetailRowSelectionFuture)(void);
|
||||
|
||||
@interface FLEXNetworkDetailRow : NSObject
|
||||
|
||||
@property (nonatomic, copy) NSString *title;
|
||||
@property (nonatomic, copy) NSString *detailText;
|
||||
@property (nonatomic, copy) FLEXNetworkDetailRowSelectionFuture selectionFuture;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXNetworkDetailRow
|
||||
|
||||
@end
|
||||
|
||||
@interface FLEXNetworkDetailSection : NSObject
|
||||
|
||||
@property (nonatomic, copy) NSString *title;
|
||||
@property (nonatomic, copy) NSArray<FLEXNetworkDetailRow *> *rows;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXNetworkDetailSection
|
||||
|
||||
@end
|
||||
|
||||
@interface FLEXNetworkTransactionDetailController ()
|
||||
@interface FLEXHTTPTransactionDetailController ()
|
||||
|
||||
@property (nonatomic, readonly) FLEXHTTPTransaction *transaction;
|
||||
@property (nonatomic, copy) NSArray<FLEXNetworkDetailSection *> *sections;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXNetworkTransactionDetailController
|
||||
@implementation FLEXHTTPTransactionDetailController
|
||||
|
||||
+ (instancetype)withTransaction:(FLEXHTTPTransaction *)transaction {
|
||||
FLEXHTTPTransactionDetailController *controller = [self new];
|
||||
controller.transaction = transaction;
|
||||
return controller;
|
||||
}
|
||||
|
||||
- (instancetype)initWithStyle:(UITableViewStyle)style {
|
||||
// Force grouped style
|
||||
self = [super initWithStyle:UITableViewStyleGrouped];
|
||||
if (self) {
|
||||
[NSNotificationCenter.defaultCenter addObserver:self
|
||||
selector:@selector(handleTransactionUpdatedNotification:)
|
||||
name:kFLEXNetworkRecorderTransactionUpdatedNotification
|
||||
object:nil
|
||||
];
|
||||
self.toolbarItems = @[
|
||||
UIBarButtonItem.flex_flexibleSpace,
|
||||
[UIBarButtonItem
|
||||
flex_itemWithTitle:@"Copy curl"
|
||||
target:self
|
||||
action:@selector(copyButtonPressed:)
|
||||
]
|
||||
];
|
||||
}
|
||||
return self;
|
||||
return [super initWithStyle:UITableViewStyleGrouped];
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
[NSNotificationCenter.defaultCenter addObserver:self
|
||||
selector:@selector(handleTransactionUpdatedNotification:)
|
||||
name:kFLEXNetworkRecorderTransactionUpdatedNotification
|
||||
object:nil
|
||||
];
|
||||
self.toolbarItems = @[
|
||||
UIBarButtonItem.flex_flexibleSpace,
|
||||
[UIBarButtonItem
|
||||
flex_itemWithTitle:@"Copy curl"
|
||||
target:self
|
||||
action:@selector(copyButtonPressed:)
|
||||
]
|
||||
];
|
||||
|
||||
[self.tableView registerClass:[FLEXMultilineTableViewCell class] forCellReuseIdentifier:kFLEXMultilineCell];
|
||||
}
|
||||
|
||||
- (void)setTransaction:(FLEXNetworkTransaction *)transaction {
|
||||
- (void)setTransaction:(FLEXHTTPTransaction *)transaction {
|
||||
if (![_transaction isEqual:transaction]) {
|
||||
_transaction = transaction;
|
||||
self.title = [transaction.request.URL lastPathComponent];
|
||||
@@ -189,6 +188,12 @@ typedef UIViewController *(^FLEXNetworkDetailRowSelectionFuture)(void);
|
||||
];
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)sectionIndexTitlesForTableView:(UITableView *)tableView {
|
||||
return [NSArray flex_forEachUpTo:self.sections.count map:^id(NSUInteger i) {
|
||||
return @"⦁";
|
||||
}];
|
||||
}
|
||||
|
||||
- (FLEXNetworkDetailRow *)rowModelAtIndexPath:(NSIndexPath *)indexPath {
|
||||
FLEXNetworkDetailSection *sectionModel = self.sections[indexPath.section];
|
||||
return sectionModel.rows[indexPath.row];
|
||||
@@ -211,8 +216,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 +239,6 @@ typedef UIViewController *(^FLEXNetworkDetailRowSelectionFuture)(void);
|
||||
];
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#pragma mark - View Configuration
|
||||
|
||||
+ (NSAttributedString *)attributedTextForRow:(FLEXNetworkDetailRow *)row {
|
||||
@@ -257,7 +258,7 @@ typedef UIViewController *(^FLEXNetworkDetailRowSelectionFuture)(void);
|
||||
|
||||
#pragma mark - Table Data Generation
|
||||
|
||||
+ (FLEXNetworkDetailSection *)generalSectionForTransaction:(FLEXNetworkTransaction *)transaction {
|
||||
+ (FLEXNetworkDetailSection *)generalSectionForTransaction:(FLEXHTTPTransaction *)transaction {
|
||||
NSMutableArray<FLEXNetworkDetailRow *> *rows = [NSMutableArray new];
|
||||
|
||||
FLEXNetworkDetailRow *requestURLRow = [FLEXNetworkDetailRow new];
|
||||
@@ -328,24 +329,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");
|
||||
@@ -412,7 +412,7 @@ typedef UIViewController *(^FLEXNetworkDetailRowSelectionFuture)(void);
|
||||
return generalSection;
|
||||
}
|
||||
|
||||
+ (FLEXNetworkDetailSection *)requestHeadersSectionForTransaction:(FLEXNetworkTransaction *)transaction {
|
||||
+ (FLEXNetworkDetailSection *)requestHeadersSectionForTransaction:(FLEXHTTPTransaction *)transaction {
|
||||
FLEXNetworkDetailSection *requestHeadersSection = [FLEXNetworkDetailSection new];
|
||||
requestHeadersSection.title = @"Request Headers";
|
||||
requestHeadersSection.rows = [self networkDetailRowsFromDictionary:transaction.request.allHTTPHeaderFields];
|
||||
@@ -420,20 +420,21 @@ typedef UIViewController *(^FLEXNetworkDetailRowSelectionFuture)(void);
|
||||
return requestHeadersSection;
|
||||
}
|
||||
|
||||
+ (FLEXNetworkDetailSection *)postBodySectionForTransaction:(FLEXNetworkTransaction *)transaction {
|
||||
+ (FLEXNetworkDetailSection *)postBodySectionForTransaction:(FLEXHTTPTransaction *)transaction {
|
||||
FLEXNetworkDetailSection *postBodySection = [FLEXNetworkDetailSection new];
|
||||
postBodySection.title = @"Request Body Parameters";
|
||||
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]];
|
||||
}
|
||||
}
|
||||
return postBodySection;
|
||||
}
|
||||
|
||||
+ (FLEXNetworkDetailSection *)queryParametersSectionForTransaction:(FLEXNetworkTransaction *)transaction {
|
||||
+ (FLEXNetworkDetailSection *)queryParametersSectionForTransaction:(FLEXHTTPTransaction *)transaction {
|
||||
NSArray<NSURLQueryItem *> *queries = [FLEXUtility itemsFromQueryString:transaction.request.URL.query];
|
||||
FLEXNetworkDetailSection *querySection = [FLEXNetworkDetailSection new];
|
||||
querySection.title = @"Query Parameters";
|
||||
@@ -442,7 +443,7 @@ typedef UIViewController *(^FLEXNetworkDetailRowSelectionFuture)(void);
|
||||
return querySection;
|
||||
}
|
||||
|
||||
+ (FLEXNetworkDetailSection *)responseHeadersSectionForTransaction:(FLEXNetworkTransaction *)transaction {
|
||||
+ (FLEXNetworkDetailSection *)responseHeadersSectionForTransaction:(FLEXHTTPTransaction *)transaction {
|
||||
FLEXNetworkDetailSection *responseHeadersSection = [FLEXNetworkDetailSection new];
|
||||
responseHeadersSection.title = @"Response Headers";
|
||||
if ([transaction.response isKindOfClass:[NSHTTPURLResponse class]]) {
|
||||
@@ -520,7 +521,7 @@ typedef UIViewController *(^FLEXNetworkDetailRowSelectionFuture)(void);
|
||||
return detailViewController;
|
||||
}
|
||||
|
||||
+ (NSData *)postBodyDataForTransaction:(FLEXNetworkTransaction *)transaction {
|
||||
+ (NSData *)postBodyDataForTransaction:(FLEXHTTPTransaction *)transaction {
|
||||
NSData *bodyData = transaction.cachedRequestBody;
|
||||
if (bodyData.length > 0) {
|
||||
NSString *contentEncoding = [transaction.request valueForHTTPHeaderField:@"Content-Encoding"];
|
||||
@@ -0,0 +1,34 @@
|
||||
//
|
||||
// FLEXMITMDataSource.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 8/22/21.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FLEXMITMDataSource<__covariant TransactionType> : NSObject
|
||||
|
||||
+ (instancetype)dataSourceWithProvider:(NSArray<TransactionType> *(^)(void))future;
|
||||
|
||||
@property (nonatomic, readonly) NSArray<TransactionType> *transactions;
|
||||
@property (nonatomic, readonly) NSArray<TransactionType> *allTransactions;
|
||||
/// Equal to \c allTransactions if not filtered
|
||||
@property (nonatomic, readonly) NSArray<TransactionType> *filteredTransactions;
|
||||
|
||||
/// Use this instead of either of the other two as it updates based on whether we have a filter or not
|
||||
@property (nonatomic) NSInteger bytesReceived;
|
||||
@property (nonatomic) NSInteger totalBytesReceived;
|
||||
/// Equal to \c totalBytesReceived if not filtered
|
||||
@property (nonatomic) NSInteger filteredBytesReceived;
|
||||
|
||||
- (void)reloadByteCounts;
|
||||
- (void)reloadData:(void (^_Nullable)(FLEXMITMDataSource *dataSource))completion;
|
||||
- (void)filter:(NSString *)searchString completion:(void(^_Nullable)(FLEXMITMDataSource *dataSource))completion;
|
||||
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,102 @@
|
||||
//
|
||||
// FLEXMITMDataSource.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 8/22/21.
|
||||
//
|
||||
|
||||
#import "FLEXMITMDataSource.h"
|
||||
#import "FLEXNetworkTransaction.h"
|
||||
#import "FLEXUtility.h"
|
||||
|
||||
@interface FLEXMITMDataSource ()
|
||||
@property (nonatomic, readonly) NSArray *(^dataProvider)(void);
|
||||
@property (nonatomic) NSString *filterString;
|
||||
@end
|
||||
|
||||
@implementation FLEXMITMDataSource
|
||||
|
||||
+ (instancetype)dataSourceWithProvider:(NSArray<id> *(^)(void))future {
|
||||
FLEXMITMDataSource *ds = [self new];
|
||||
ds->_dataProvider = future;
|
||||
[ds reloadData:nil];
|
||||
|
||||
return ds;
|
||||
}
|
||||
|
||||
- (NSArray *)transactions {
|
||||
return _filteredTransactions;
|
||||
}
|
||||
|
||||
- (NSInteger)bytesReceived {
|
||||
return _filteredBytesReceived;
|
||||
}
|
||||
|
||||
- (void)reloadByteCounts {
|
||||
[self updateBytesReceived];
|
||||
[self updateFilteredBytesReceived];
|
||||
}
|
||||
|
||||
- (void)reloadData:(void (^)(FLEXMITMDataSource *dataSource))completion {
|
||||
self.allTransactions = self.dataProvider();
|
||||
[self filter:self.filterString completion:completion];
|
||||
}
|
||||
|
||||
- (void)filter:(NSString *)searchString completion:(void (^)(FLEXMITMDataSource *dataSource))completion {
|
||||
self.filterString = searchString;
|
||||
|
||||
if (!searchString.length) {
|
||||
self.filteredTransactions = self.allTransactions;
|
||||
if (completion) completion(self);
|
||||
} else {
|
||||
[self onBackgroundQueue:^NSArray *{
|
||||
return [self.allTransactions flex_filtered:^BOOL(FLEXNetworkTransaction *entry, NSUInteger idx) {
|
||||
return [entry matchesQuery:searchString];
|
||||
}];
|
||||
} thenOnMainQueue:^(NSArray *filteredNetworkTransactions) {
|
||||
if ([self.filterString isEqual:searchString]) {
|
||||
self.filteredTransactions = filteredNetworkTransactions;
|
||||
if (completion) completion(self);
|
||||
}
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setAllTransactions:(NSArray *)transactions {
|
||||
_allTransactions = transactions;
|
||||
[self updateBytesReceived];
|
||||
}
|
||||
|
||||
- (void)setFilteredTransactions:(NSArray *)filteredTransactions {
|
||||
_filteredTransactions = filteredTransactions;
|
||||
[self updateFilteredBytesReceived];
|
||||
}
|
||||
|
||||
- (void)updateBytesReceived {
|
||||
NSInteger bytesReceived = 0;
|
||||
for (FLEXNetworkTransaction *transaction in self.transactions) {
|
||||
bytesReceived += transaction.receivedDataLength;
|
||||
}
|
||||
|
||||
self.bytesReceived = bytesReceived;
|
||||
}
|
||||
|
||||
- (void)updateFilteredBytesReceived {
|
||||
NSInteger filteredBytesReceived = 0;
|
||||
for (FLEXNetworkTransaction *transaction in self.filteredTransactions) {
|
||||
filteredBytesReceived += transaction.receivedDataLength;
|
||||
}
|
||||
|
||||
self.filteredBytesReceived = filteredBytesReceived;
|
||||
}
|
||||
|
||||
- (void)onBackgroundQueue:(NSArray *(^)(void))backgroundBlock thenOnMainQueue:(void(^)(NSArray *))mainBlock {
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
NSArray *items = backgroundBlock();
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
mainBlock(items);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -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;
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#import "FLEXTableViewController.h"
|
||||
#import "FLEXGlobalsEntry.h"
|
||||
|
||||
/// The main screen for the network observer, which displays a list of network transactions.
|
||||
@interface FLEXNetworkMITMViewController : FLEXTableViewController <FLEXGlobalsEntry>
|
||||
|
||||
@end
|
||||
|
||||
@@ -8,29 +8,36 @@
|
||||
|
||||
#import "FLEXColor.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXMITMDataSource.h"
|
||||
#import "FLEXNetworkMITMViewController.h"
|
||||
#import "FLEXNetworkTransaction.h"
|
||||
#import "FLEXNetworkRecorder.h"
|
||||
#import "FLEXNetworkObserver.h"
|
||||
#import "FLEXNetworkTransactionCell.h"
|
||||
#import "FLEXNetworkTransactionDetailController.h"
|
||||
#import "FLEXHTTPTransactionDetailController.h"
|
||||
#import "FLEXNetworkSettingsController.h"
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
#import "FLEXGlobalsViewController.h"
|
||||
#import "FLEXWebViewController.h"
|
||||
#import "UIBarButtonItem+FLEX.h"
|
||||
#import "FLEXResources.h"
|
||||
|
||||
typedef NS_ENUM(NSUInteger, FLEXNetworkObserverMode) {
|
||||
FLEXNetworkObserverModeREST = 0,
|
||||
FLEXNetworkObserverModeWebsockets,
|
||||
};
|
||||
|
||||
@interface FLEXNetworkMITMViewController ()
|
||||
|
||||
/// Backing model
|
||||
@property (nonatomic, copy) NSArray<FLEXNetworkTransaction *> *networkTransactions;
|
||||
@property (nonatomic) long long bytesReceived;
|
||||
@property (nonatomic, copy) NSArray<FLEXNetworkTransaction *> *filteredNetworkTransactions;
|
||||
@property (nonatomic) long long filteredBytesReceived;
|
||||
|
||||
@property (nonatomic) BOOL rowInsertInProgress;
|
||||
@property (nonatomic) BOOL isPresentingSearch;
|
||||
@property (nonatomic) BOOL updateInProgress;
|
||||
@property (nonatomic) BOOL pendingReload;
|
||||
|
||||
@property (nonatomic, readonly) FLEXNetworkObserverMode mode;
|
||||
|
||||
@property (nonatomic, readonly) FLEXMITMDataSource<FLEXNetworkTransaction *> *dataSource;
|
||||
@property (nonatomic, readonly) FLEXMITMDataSource<FLEXHTTPTransaction *> *HTTPDataSource;
|
||||
@property (nonatomic, readonly) FLEXMITMDataSource<FLEXWebsocketTransaction *> *websocketDataSource;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXNetworkMITMViewController
|
||||
@@ -47,6 +54,18 @@
|
||||
self.showsSearchBar = YES;
|
||||
self.showSearchBarInitially = NO;
|
||||
|
||||
_HTTPDataSource = [FLEXMITMDataSource dataSourceWithProvider:^NSArray * {
|
||||
return FLEXNetworkRecorder.defaultRecorder.HTTPTransactions;
|
||||
}];
|
||||
|
||||
if (@available(iOS 13.0, *)) {
|
||||
self.searchController.searchBar.showsScopeBar = YES;
|
||||
self.searchController.searchBar.scopeButtonTitles = @[@"REST", @"Websockets"];
|
||||
_websocketDataSource = [FLEXMITMDataSource dataSourceWithProvider:^NSArray * {
|
||||
return FLEXNetworkRecorder.defaultRecorder.websocketTransactions;
|
||||
}];
|
||||
}
|
||||
|
||||
[self addToolbarItems:@[
|
||||
[UIBarButtonItem
|
||||
flex_itemWithImage:FLEXResources.gearIcon
|
||||
@@ -61,14 +80,14 @@
|
||||
]];
|
||||
|
||||
[self.tableView
|
||||
registerClass:[FLEXNetworkTransactionCell class]
|
||||
forCellReuseIdentifier:kFLEXNetworkTransactionCellIdentifier
|
||||
registerClass:FLEXNetworkTransactionCell.class
|
||||
forCellReuseIdentifier:FLEXNetworkTransactionCell.reuseID
|
||||
];
|
||||
self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
|
||||
self.tableView.rowHeight = FLEXNetworkTransactionCell.preferredCellHeight;
|
||||
|
||||
[self registerForNotifications];
|
||||
[self updateTransactions];
|
||||
[self updateTransactions:nil];
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated {
|
||||
@@ -137,44 +156,36 @@
|
||||
[self dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Transactions
|
||||
|
||||
- (void)updateTransactions {
|
||||
self.networkTransactions = [FLEXNetworkRecorder.defaultRecorder networkTransactions];
|
||||
- (FLEXNetworkObserverMode)mode {
|
||||
return self.searchController.searchBar.selectedScopeButtonIndex;
|
||||
}
|
||||
|
||||
- (void)setNetworkTransactions:(NSArray<FLEXNetworkTransaction *> *)networkTransactions {
|
||||
if (![_networkTransactions isEqual:networkTransactions]) {
|
||||
_networkTransactions = networkTransactions;
|
||||
[self updateBytesReceived];
|
||||
[self updateFilteredBytesReceived];
|
||||
- (FLEXMITMDataSource<FLEXNetworkTransaction *> *)dataSource {
|
||||
switch (self.mode) {
|
||||
case FLEXNetworkObserverModeREST:
|
||||
return self.HTTPDataSource;
|
||||
case FLEXNetworkObserverModeWebsockets:
|
||||
return self.websocketDataSource;
|
||||
|
||||
default:
|
||||
@throw NSInternalInconsistencyException;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)updateBytesReceived {
|
||||
long long bytesReceived = 0;
|
||||
for (FLEXNetworkTransaction *transaction in self.networkTransactions) {
|
||||
bytesReceived += transaction.receivedDataLength;
|
||||
}
|
||||
self.bytesReceived = bytesReceived;
|
||||
[self updateFirstSectionHeader];
|
||||
- (void)updateTransactions:(void(^)(void))callback {
|
||||
id completion = ^(FLEXMITMDataSource *dataSource) {
|
||||
// Update byte count
|
||||
[self updateFirstSectionHeader];
|
||||
if (callback && dataSource == self.dataSource) callback();
|
||||
};
|
||||
|
||||
[self.HTTPDataSource reloadData:completion];
|
||||
[self.websocketDataSource reloadData:completion];
|
||||
}
|
||||
|
||||
- (void)setFilteredNetworkTransactions:(NSArray<FLEXNetworkTransaction *> *)networkTransactions {
|
||||
if (![_filteredNetworkTransactions isEqual:networkTransactions]) {
|
||||
_filteredNetworkTransactions = networkTransactions;
|
||||
[self updateFilteredBytesReceived];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)updateFilteredBytesReceived {
|
||||
long long filteredBytesReceived = 0;
|
||||
for (FLEXNetworkTransaction *transaction in self.filteredNetworkTransactions) {
|
||||
filteredBytesReceived += transaction.receivedDataLength;
|
||||
}
|
||||
self.filteredBytesReceived = filteredBytesReceived;
|
||||
[self updateFirstSectionHeader];
|
||||
}
|
||||
|
||||
#pragma mark Header
|
||||
|
||||
@@ -191,11 +202,11 @@
|
||||
long long bytesReceived = 0;
|
||||
NSInteger totalRequests = 0;
|
||||
if (self.searchController.isActive) {
|
||||
bytesReceived = self.filteredBytesReceived;
|
||||
totalRequests = self.filteredNetworkTransactions.count;
|
||||
bytesReceived = self.dataSource.filteredBytesReceived;
|
||||
totalRequests = self.dataSource.transactions.count;
|
||||
} else {
|
||||
bytesReceived = self.bytesReceived;
|
||||
totalRequests = self.networkTransactions.count;
|
||||
bytesReceived = self.dataSource.bytesReceived;
|
||||
totalRequests = self.dataSource.transactions.count;
|
||||
}
|
||||
|
||||
NSString *byteCountText = [NSByteCountFormatter
|
||||
@@ -253,7 +264,7 @@
|
||||
- (void)tryUpdateTransactions {
|
||||
// Don't do any view updating if we aren't in the view hierarchy
|
||||
if (!self.viewIfLoaded.window) {
|
||||
[self updateTransactions];
|
||||
[self updateTransactions:nil];
|
||||
self.pendingReload = YES;
|
||||
return;
|
||||
}
|
||||
@@ -261,57 +272,71 @@
|
||||
// Let the previous row insert animation finish before starting a new one to avoid stomping.
|
||||
// We'll try calling the method again when the insertion completes,
|
||||
// and we properly no-op if there haven't been changes.
|
||||
if (self.rowInsertInProgress) {
|
||||
if (self.updateInProgress) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.searchController.isActive) {
|
||||
[self updateTransactions];
|
||||
[self updateSearchResults:self.searchText];
|
||||
return;
|
||||
}
|
||||
self.updateInProgress = YES;
|
||||
|
||||
NSInteger existingRowCount = self.networkTransactions.count;
|
||||
[self updateTransactions];
|
||||
NSInteger newRowCount = self.networkTransactions.count;
|
||||
NSInteger addedRowCount = newRowCount - existingRowCount;
|
||||
|
||||
if (addedRowCount != 0 && !self.isPresentingSearch) {
|
||||
// Insert animation if we're at the top.
|
||||
if (self.tableView.contentOffset.y <= 0.0 && addedRowCount > 0) {
|
||||
[CATransaction begin];
|
||||
|
||||
self.rowInsertInProgress = YES;
|
||||
[CATransaction setCompletionBlock:^{
|
||||
self.rowInsertInProgress = NO;
|
||||
[self tryUpdateTransactions];
|
||||
}];
|
||||
|
||||
NSMutableArray<NSIndexPath *> *indexPathsToReload = [NSMutableArray new];
|
||||
for (NSInteger row = 0; row < addedRowCount; row++) {
|
||||
[indexPathsToReload addObject:[NSIndexPath indexPathForRow:row inSection:0]];
|
||||
}
|
||||
[self.tableView insertRowsAtIndexPaths:indexPathsToReload withRowAnimation:UITableViewRowAnimationAutomatic];
|
||||
|
||||
[CATransaction commit];
|
||||
} else {
|
||||
// Maintain the user's position if they've scrolled down.
|
||||
CGSize existingContentSize = self.tableView.contentSize;
|
||||
[self.tableView reloadData];
|
||||
CGFloat contentHeightChange = self.tableView.contentSize.height - existingContentSize.height;
|
||||
self.tableView.contentOffset = CGPointMake(self.tableView.contentOffset.x, self.tableView.contentOffset.y + contentHeightChange);
|
||||
// Get state before update
|
||||
NSString *currentFilter = self.searchText;
|
||||
FLEXNetworkObserverMode currentMode = self.mode;
|
||||
NSInteger existingRowCount = self.dataSource.transactions.count;
|
||||
|
||||
[self updateTransactions:^{
|
||||
// Compare to state after update
|
||||
NSString *newFilter = self.searchText;
|
||||
FLEXNetworkObserverMode newMode = self.mode;
|
||||
NSInteger newRowCount = self.dataSource.transactions.count;
|
||||
NSInteger rowCountDiff = newRowCount - existingRowCount;
|
||||
|
||||
// Abort if the observation mode changed, or if the search field text changed
|
||||
if (newMode != currentMode || ![currentFilter isEqualToString:newFilter]) {
|
||||
self.updateInProgress = NO;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (rowCountDiff) {
|
||||
// Insert animation if we're at the top.
|
||||
if (self.tableView.contentOffset.y <= 0.0 && rowCountDiff > 0) {
|
||||
[CATransaction begin];
|
||||
|
||||
[CATransaction setCompletionBlock:^{
|
||||
self.updateInProgress = NO;
|
||||
// This isn't an infinite loop, it won't run a third time
|
||||
// if there were no new transactions the second time
|
||||
[self tryUpdateTransactions];
|
||||
}];
|
||||
|
||||
NSMutableArray<NSIndexPath *> *indexPathsToReload = [NSMutableArray new];
|
||||
for (NSInteger row = 0; row < rowCountDiff; row++) {
|
||||
[indexPathsToReload addObject:[NSIndexPath indexPathForRow:row inSection:0]];
|
||||
}
|
||||
|
||||
[self.tableView insertRowsAtIndexPaths:indexPathsToReload withRowAnimation:UITableViewRowAnimationAutomatic];
|
||||
[CATransaction commit];
|
||||
} else {
|
||||
// Maintain the user's position if they've scrolled down.
|
||||
CGSize existingContentSize = self.tableView.contentSize;
|
||||
[self.tableView reloadData];
|
||||
CGFloat contentHeightChange = self.tableView.contentSize.height - existingContentSize.height;
|
||||
self.tableView.contentOffset = CGPointMake(self.tableView.contentOffset.x, self.tableView.contentOffset.y + contentHeightChange);
|
||||
self.updateInProgress = NO;
|
||||
}
|
||||
} else {
|
||||
self.updateInProgress = NO;
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)handleTransactionUpdatedNotification:(NSNotification *)notification {
|
||||
[self updateBytesReceived];
|
||||
[self updateFilteredBytesReceived];
|
||||
[self.HTTPDataSource reloadByteCounts];
|
||||
[self.websocketDataSource reloadByteCounts];
|
||||
|
||||
FLEXNetworkTransaction *transaction = notification.userInfo[kFLEXNetworkRecorderUserInfoTransactionKey];
|
||||
|
||||
// Update both the main table view and search table view if needed.
|
||||
for (FLEXNetworkTransactionCell *cell in [self.tableView visibleCells]) {
|
||||
for (FLEXNetworkTransactionCell *cell in self.tableView.visibleCells) {
|
||||
if ([cell.transaction isEqual:transaction]) {
|
||||
// Using -[UITableView reloadRowsAtIndexPaths:withRowAnimation:] is overkill here and kicks off a lot of
|
||||
// work that can make the table view somewhat unresponsive when lots of updates are streaming in.
|
||||
@@ -320,12 +345,14 @@
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
[self updateFirstSectionHeader];
|
||||
}
|
||||
|
||||
- (void)handleTransactionsClearedNotification:(NSNotification *)notification {
|
||||
[self updateTransactions];
|
||||
[self.tableView reloadData];
|
||||
[self updateTransactions:^{
|
||||
[self.tableView reloadData];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)handleNetworkObserverEnabledStateChangedNotification:(NSNotification *)notification {
|
||||
@@ -337,7 +364,7 @@
|
||||
#pragma mark - Table view data source
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
|
||||
return self.searchController.isActive ? self.filteredNetworkTransactions.count : self.networkTransactions.count;
|
||||
return self.dataSource.transactions.count;
|
||||
}
|
||||
|
||||
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
|
||||
@@ -352,7 +379,11 @@
|
||||
}
|
||||
|
||||
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
FLEXNetworkTransactionCell *cell = [tableView dequeueReusableCellWithIdentifier:kFLEXNetworkTransactionCellIdentifier forIndexPath:indexPath];
|
||||
FLEXNetworkTransactionCell *cell = [tableView
|
||||
dequeueReusableCellWithIdentifier:FLEXNetworkTransactionCell.reuseID
|
||||
forIndexPath:indexPath
|
||||
];
|
||||
|
||||
cell.transaction = [self transactionAtIndexPath:indexPath];
|
||||
|
||||
// Since we insert from the top, assign background colors bottom up to keep them consistent for each transaction.
|
||||
@@ -367,9 +398,33 @@
|
||||
}
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
FLEXNetworkTransactionDetailController *detailViewController = [FLEXNetworkTransactionDetailController new];
|
||||
detailViewController.transaction = [self transactionAtIndexPath:indexPath];
|
||||
[self.navigationController pushViewController:detailViewController animated:YES];
|
||||
switch (self.mode) {
|
||||
case FLEXNetworkObserverModeREST: {
|
||||
FLEXHTTPTransaction *transaction = [self HTTPTransactionAtIndexPath:indexPath];
|
||||
UIViewController *details = [FLEXHTTPTransactionDetailController withTransaction:transaction];
|
||||
[self.navigationController pushViewController:details animated:YES];
|
||||
break;
|
||||
}
|
||||
|
||||
case FLEXNetworkObserverModeWebsockets: {
|
||||
if (@available(iOS 13.0, *)) { // This check will never fail
|
||||
FLEXWebsocketTransaction *transaction = [self websocketTransactionAtIndexPath:indexPath];
|
||||
|
||||
UIViewController *details = nil;
|
||||
if (transaction.message.type == NSURLSessionWebSocketMessageTypeData) {
|
||||
details = [FLEXObjectExplorerFactory explorerViewControllerForObject:transaction.message.data];
|
||||
} else {
|
||||
details = [[FLEXWebViewController alloc] initWithText:transaction.message.string];
|
||||
}
|
||||
|
||||
[self.navigationController pushViewController:details animated:YES];
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
@throw NSInternalInconsistencyException;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -385,84 +440,83 @@
|
||||
|
||||
- (void)tableView:(UITableView *)tableView performAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender {
|
||||
if (action == @selector(copy:)) {
|
||||
NSURLRequest *request = [self transactionAtIndexPath:indexPath].request;
|
||||
UIPasteboard.generalPasteboard.string = request.URL.absoluteString ?: @"";
|
||||
UIPasteboard.generalPasteboard.string = [self transactionAtIndexPath:indexPath].copyString;
|
||||
}
|
||||
}
|
||||
|
||||
#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;
|
||||
|
||||
FLEXNetworkTransaction *transaction = [self transactionAtIndexPath:indexPath];
|
||||
|
||||
return [UIContextMenuConfiguration
|
||||
configurationWithIdentifier:nil
|
||||
previewProvider:nil
|
||||
actionProvider:^UIMenu *(NSArray<UIMenuElement *> *suggestedActions) {
|
||||
UIAction *copy = [UIAction
|
||||
actionWithTitle:@"Copy"
|
||||
actionWithTitle:@"Copy URL"
|
||||
image:nil
|
||||
identifier:nil
|
||||
handler:^(__kindof UIAction *action) {
|
||||
UIPasteboard.generalPasteboard.string = request.URL.absoluteString ?: @"";
|
||||
}
|
||||
];
|
||||
UIAction *blacklist = [UIAction
|
||||
actionWithTitle:[NSString stringWithFormat:@"Blacklist '%@'", 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];
|
||||
[self tryUpdateTransactions];
|
||||
UIPasteboard.generalPasteboard.string = transaction.copyString;
|
||||
}
|
||||
];
|
||||
|
||||
NSArray *children = @[copy];
|
||||
if (self.mode == FLEXNetworkObserverModeREST) {
|
||||
NSURLRequest *request = [self HTTPTransactionAtIndexPath:indexPath].request;
|
||||
UIAction *denylist = [UIAction
|
||||
actionWithTitle:[NSString stringWithFormat:@"Exclude '%@'", request.URL.host]
|
||||
image:nil
|
||||
identifier:nil
|
||||
handler:^(__kindof UIAction *action) {
|
||||
NSMutableArray *denylist = FLEXNetworkRecorder.defaultRecorder.hostDenylist;
|
||||
[denylist addObject:request.URL.host];
|
||||
[FLEXNetworkRecorder.defaultRecorder clearExcludedTransactions];
|
||||
[FLEXNetworkRecorder.defaultRecorder synchronizeDenylist];
|
||||
[self tryUpdateTransactions];
|
||||
}
|
||||
];
|
||||
|
||||
children = [children arrayByAddingObject:denylist];
|
||||
}
|
||||
return [UIMenu
|
||||
menuWithTitle:@"" image:nil identifier:nil
|
||||
options:UIMenuOptionsDisplayInline
|
||||
children:@[copy, blacklist]
|
||||
children:children
|
||||
];
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
- (FLEXNetworkTransaction *)transactionAtIndexPath:(NSIndexPath *)indexPath {
|
||||
return self.searchController.isActive ? self.filteredNetworkTransactions[indexPath.row] : self.networkTransactions[indexPath.row];
|
||||
return self.dataSource.transactions[indexPath.row];
|
||||
}
|
||||
|
||||
- (FLEXHTTPTransaction *)HTTPTransactionAtIndexPath:(NSIndexPath *)indexPath {
|
||||
return self.HTTPDataSource.transactions[indexPath.row];
|
||||
}
|
||||
|
||||
- (FLEXWebsocketTransaction *)websocketTransactionAtIndexPath:(NSIndexPath *)indexPath {
|
||||
return self.websocketDataSource.transactions[indexPath.row];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Search Bar
|
||||
|
||||
- (void)updateSearchResults:(NSString *)searchString {
|
||||
if (!searchString.length) {
|
||||
self.filteredNetworkTransactions = self.networkTransactions;
|
||||
[self.tableView reloadData];
|
||||
} else {
|
||||
[self onBackgroundQueue:^NSArray *{
|
||||
return [self.networkTransactions flex_filtered:^BOOL(FLEXNetworkTransaction *entry, NSUInteger idx) {
|
||||
return [entry.request.URL.absoluteString localizedCaseInsensitiveContainsString:searchString];
|
||||
}];
|
||||
} thenOnMainQueue:^(NSArray *filteredNetworkTransactions) {
|
||||
if ([self.searchText isEqual:searchString]) {
|
||||
self.filteredNetworkTransactions = filteredNetworkTransactions;
|
||||
[self.tableView reloadData];
|
||||
}
|
||||
}];
|
||||
}
|
||||
id callback = ^(FLEXMITMDataSource *dataSource) {
|
||||
if (self.dataSource == dataSource) {
|
||||
[self.tableView reloadData];
|
||||
}
|
||||
};
|
||||
|
||||
[self.HTTPDataSource filter:searchString completion:callback];
|
||||
[self.websocketDataSource filter:searchString completion:callback];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark UISearchControllerDelegate
|
||||
|
||||
- (void)willPresentSearchController:(UISearchController *)searchController {
|
||||
self.isPresentingSearch = YES;
|
||||
}
|
||||
|
||||
- (void)didPresentSearchController:(UISearchController *)searchController {
|
||||
self.isPresentingSearch = NO;
|
||||
- (void)searchBar:(UISearchBar *)searchBar selectedScopeButtonIndexDidChange:(NSInteger)selectedScope {
|
||||
[self updateFirstSectionHeader];
|
||||
[self.tableView reloadData];
|
||||
}
|
||||
|
||||
- (void)willDismissSearchController:(UISearchController *)searchController {
|
||||
|
||||
@@ -14,7 +14,7 @@ extern NSString *const kFLEXNetworkRecorderTransactionUpdatedNotification;
|
||||
extern NSString *const kFLEXNetworkRecorderUserInfoTransactionKey;
|
||||
extern NSString *const kFLEXNetworkRecorderTransactionsClearedNotification;
|
||||
|
||||
@class FLEXNetworkTransaction;
|
||||
@class FLEXNetworkTransaction, FLEXHTTPTransaction, FLEXWebsocketTransaction;
|
||||
|
||||
@interface FLEXNetworkRecorder : NSObject
|
||||
|
||||
@@ -28,28 +28,30 @@ 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
|
||||
#pragma mark Accessing recorded network activity
|
||||
|
||||
/// Array of FLEXNetworkTransaction objects ordered by start time with the newest first.
|
||||
- (NSArray<FLEXNetworkTransaction *> *)networkTransactions;
|
||||
/// Array of FLEXHTTPTransaction objects ordered by start time with the newest first.
|
||||
@property (nonatomic, readonly) NSArray<FLEXHTTPTransaction *> *HTTPTransactions;
|
||||
/// Array of FLEXWebsocketTransaction objects ordered by start time with the newest first.
|
||||
@property (nonatomic, readonly) NSArray<FLEXWebsocketTransaction *> *websocketTransactions API_AVAILABLE(ios(13.0));
|
||||
|
||||
/// The full response data IFF it hasn't been purged due to memory pressure.
|
||||
- (NSData *)cachedResponseBodyForTransaction:(FLEXNetworkTransaction *)transaction;
|
||||
- (NSData *)cachedResponseBodyForTransaction:(FLEXHTTPTransaction *)transaction;
|
||||
|
||||
/// Dumps all network transactions and cached response bodies.
|
||||
- (void)clearRecordedActivity;
|
||||
|
||||
|
||||
// Recording network activity
|
||||
#pragma mark Recording network activity
|
||||
|
||||
/// Call when app is about to send HTTP request.
|
||||
- (void)recordRequestWillBeSentWithRequestID:(NSString *)requestID
|
||||
@@ -72,4 +74,12 @@ extern NSString *const kFLEXNetworkRecorderTransactionsClearedNotification;
|
||||
/// This string can be set to anything useful about the API used to make the request.
|
||||
- (void)recordMechanism:(NSString *)mechanism forRequestID:(NSString *)requestID;
|
||||
|
||||
- (void)recordWebsocketMessageSend:(NSURLSessionWebSocketMessage *)message
|
||||
task:(NSURLSessionWebSocketTask *)task API_AVAILABLE(ios(13.0));
|
||||
- (void)recordWebsocketMessageSendCompletion:(NSURLSessionWebSocketMessage *)message
|
||||
error:(NSError *)error API_AVAILABLE(ios(13.0));
|
||||
|
||||
- (void)recordWebsocketMessageReceived:(NSURLSessionWebSocketMessage *)message
|
||||
task:(NSURLSessionWebSocketTask *)task API_AVAILABLE(ios(13.0));
|
||||
|
||||
@end
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXResources.h"
|
||||
#import "NSUserDefaults+FLEX.h"
|
||||
#import "OSCache.h"
|
||||
|
||||
NSString *const kFLEXNetworkRecorderNewTransactionNotification = @"kFLEXNetworkRecorderNewTransactionNotification";
|
||||
NSString *const kFLEXNetworkRecorderTransactionUpdatedNotification = @"kFLEXNetworkRecorderTransactionUpdatedNotification";
|
||||
@@ -22,9 +23,10 @@ NSString *const kFLEXNetworkRecorderResponseCacheLimitDefaultsKey = @"com.flex.r
|
||||
|
||||
@interface FLEXNetworkRecorder ()
|
||||
|
||||
@property (nonatomic) NSCache *responseCache;
|
||||
@property (nonatomic) NSMutableArray<FLEXNetworkTransaction *> *orderedTransactions;
|
||||
@property (nonatomic) NSMutableDictionary<NSString *, FLEXNetworkTransaction *> *requestIDsToTransactions;
|
||||
@property (nonatomic) OSCache *restCache;
|
||||
@property (nonatomic) NSMutableArray<FLEXHTTPTransaction *> *orderedHTTPTransactions;
|
||||
@property (nonatomic) NSMutableArray<FLEXWebsocketTransaction *> *orderedWSTransactions;
|
||||
@property (nonatomic) NSMutableDictionary<NSString *, FLEXHTTPTransaction *> *requestIDsToHTTPTransactions;
|
||||
@property (nonatomic) dispatch_queue_t queue;
|
||||
|
||||
@end
|
||||
@@ -34,18 +36,19 @@ NSString *const kFLEXNetworkRecorderResponseCacheLimitDefaultsKey = @"com.flex.r
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
self.responseCache = [NSCache new];
|
||||
self.restCache = [OSCache new];
|
||||
NSUInteger responseCacheLimit = [[NSUserDefaults.standardUserDefaults
|
||||
objectForKey:kFLEXNetworkRecorderResponseCacheLimitDefaultsKey] unsignedIntegerValue
|
||||
];
|
||||
|
||||
// Default to 25 MB max. The cache will purge earlier if there is memory pressure.
|
||||
self.responseCache.totalCostLimit = responseCacheLimit ?: 25 * 1024 * 1024;
|
||||
[self.responseCache setTotalCostLimit:responseCacheLimit];
|
||||
self.restCache.totalCostLimit = responseCacheLimit ?: 25 * 1024 * 1024;
|
||||
[self.restCache setTotalCostLimit:responseCacheLimit];
|
||||
|
||||
self.orderedTransactions = [NSMutableArray new];
|
||||
self.requestIDsToTransactions = [NSMutableDictionary new];
|
||||
self.hostBlacklist = NSUserDefaults.standardUserDefaults.flex_networkHostBlacklist.mutableCopy;
|
||||
self.orderedWSTransactions = [NSMutableArray new];
|
||||
self.orderedHTTPTransactions = [NSMutableArray new];
|
||||
self.requestIDsToHTTPTransactions = [NSMutableDictionary new];
|
||||
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);
|
||||
@@ -67,46 +70,47 @@ NSString *const kFLEXNetworkRecorderResponseCacheLimitDefaultsKey = @"com.flex.r
|
||||
#pragma mark - Public Data Access
|
||||
|
||||
- (NSUInteger)responseCacheByteLimit {
|
||||
return self.responseCache.totalCostLimit;
|
||||
return self.restCache.totalCostLimit;
|
||||
}
|
||||
|
||||
- (void)setResponseCacheByteLimit:(NSUInteger)responseCacheByteLimit {
|
||||
self.responseCache.totalCostLimit = responseCacheByteLimit;
|
||||
self.restCache.totalCostLimit = responseCacheByteLimit;
|
||||
[NSUserDefaults.standardUserDefaults
|
||||
setObject:@(responseCacheByteLimit)
|
||||
forKey:kFLEXNetworkRecorderResponseCacheLimitDefaultsKey
|
||||
];
|
||||
}
|
||||
|
||||
- (NSArray<FLEXNetworkTransaction *> *)networkTransactions {
|
||||
__block NSArray<FLEXNetworkTransaction *> *transactions = nil;
|
||||
dispatch_sync(self.queue, ^{
|
||||
transactions = self.orderedTransactions.copy;
|
||||
});
|
||||
return transactions;
|
||||
- (NSArray<FLEXHTTPTransaction *> *)HTTPTransactions {
|
||||
return self.orderedHTTPTransactions.copy;
|
||||
}
|
||||
|
||||
- (NSData *)cachedResponseBodyForTransaction:(FLEXNetworkTransaction *)transaction {
|
||||
return [self.responseCache objectForKey:transaction.requestID];
|
||||
- (NSArray<FLEXWebsocketTransaction *> *)websocketTransactions {
|
||||
return self.orderedWSTransactions.copy;
|
||||
}
|
||||
|
||||
- (NSData *)cachedResponseBodyForTransaction:(FLEXHTTPTransaction *)transaction {
|
||||
return [self.restCache objectForKey:transaction.requestID];
|
||||
}
|
||||
|
||||
- (void)clearRecordedActivity {
|
||||
dispatch_async(self.queue, ^{
|
||||
[self.responseCache removeAllObjects];
|
||||
[self.orderedTransactions removeAllObjects];
|
||||
[self.requestIDsToTransactions removeAllObjects];
|
||||
[self.restCache removeAllObjects];
|
||||
[self.orderedWSTransactions removeAllObjects];
|
||||
[self.orderedHTTPTransactions removeAllObjects];
|
||||
[self.requestIDsToHTTPTransactions removeAllObjects];
|
||||
|
||||
[self notify:kFLEXNetworkRecorderTransactionsClearedNotification transaction:nil];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)clearBlacklistedTransactions {
|
||||
- (void)clearExcludedTransactions {
|
||||
dispatch_sync(self.queue, ^{
|
||||
self.orderedTransactions = ({
|
||||
[self.orderedTransactions flex_filtered:^BOOL(FLEXNetworkTransaction *ta, NSUInteger idx) {
|
||||
self.orderedHTTPTransactions = ({
|
||||
[self.orderedHTTPTransactions flex_filtered:^BOOL(FLEXHTTPTransaction *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 +121,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,28 +130,23 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
// Before async block to stay accurate
|
||||
NSDate *startDate = [NSDate date];
|
||||
FLEXHTTPTransaction *transaction = [FLEXHTTPTransaction request:request identifier:requestID];
|
||||
|
||||
// Before async block to keep times accurate
|
||||
if (redirectResponse) {
|
||||
[self recordResponseReceivedWithRequestID:requestID response:redirectResponse];
|
||||
[self recordLoadingFinishedWithRequestID:requestID responseBody:nil];
|
||||
}
|
||||
|
||||
dispatch_async(self.queue, ^{
|
||||
FLEXNetworkTransaction *transaction = [FLEXNetworkTransaction new];
|
||||
transaction.requestID = requestID;
|
||||
transaction.request = request;
|
||||
transaction.startTime = startDate;
|
||||
|
||||
[self.orderedTransactions insertObject:transaction atIndex:0];
|
||||
[self.requestIDsToTransactions setObject:transaction forKey:requestID];
|
||||
[self.orderedHTTPTransactions insertObject:transaction atIndex:0];
|
||||
[self.requestIDsToHTTPTransactions setObject:transaction forKey:requestID];
|
||||
transaction.transactionState = FLEXNetworkTransactionStateAwaitingResponse;
|
||||
|
||||
[self postNewTransactionNotificationWithTransaction:transaction];
|
||||
@@ -159,7 +158,7 @@ NSString *const kFLEXNetworkRecorderResponseCacheLimitDefaultsKey = @"com.flex.r
|
||||
NSDate *responseDate = [NSDate date];
|
||||
|
||||
dispatch_async(self.queue, ^{
|
||||
FLEXNetworkTransaction *transaction = self.requestIDsToTransactions[requestID];
|
||||
FLEXHTTPTransaction *transaction = self.requestIDsToHTTPTransactions[requestID];
|
||||
if (!transaction) {
|
||||
return;
|
||||
}
|
||||
@@ -174,7 +173,7 @@ NSString *const kFLEXNetworkRecorderResponseCacheLimitDefaultsKey = @"com.flex.r
|
||||
|
||||
- (void)recordDataReceivedWithRequestID:(NSString *)requestID dataLength:(int64_t)dataLength {
|
||||
dispatch_async(self.queue, ^{
|
||||
FLEXNetworkTransaction *transaction = self.requestIDsToTransactions[requestID];
|
||||
FLEXHTTPTransaction *transaction = self.requestIDsToHTTPTransactions[requestID];
|
||||
if (!transaction) {
|
||||
return;
|
||||
}
|
||||
@@ -188,7 +187,7 @@ NSString *const kFLEXNetworkRecorderResponseCacheLimitDefaultsKey = @"com.flex.r
|
||||
NSDate *finishedDate = [NSDate date];
|
||||
|
||||
dispatch_async(self.queue, ^{
|
||||
FLEXNetworkTransaction *transaction = self.requestIDsToTransactions[requestID];
|
||||
FLEXHTTPTransaction *transaction = self.requestIDsToHTTPTransactions[requestID];
|
||||
if (!transaction) {
|
||||
return;
|
||||
}
|
||||
@@ -205,7 +204,7 @@ NSString *const kFLEXNetworkRecorderResponseCacheLimitDefaultsKey = @"com.flex.r
|
||||
}
|
||||
|
||||
if (shouldCache) {
|
||||
[self.responseCache setObject:responseBody forKey:requestID cost:responseBody.length];
|
||||
[self.restCache setObject:responseBody forKey:requestID cost:responseBody.length];
|
||||
}
|
||||
|
||||
NSString *mimeType = transaction.response.MIMEType;
|
||||
@@ -213,32 +212,32 @@ NSString *const kFLEXNetworkRecorderResponseCacheLimitDefaultsKey = @"com.flex.r
|
||||
// Thumbnail image previews on a separate background queue
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
NSInteger maxPixelDimension = UIScreen.mainScreen.scale * 32.0;
|
||||
transaction.responseThumbnail = [FLEXUtility
|
||||
transaction.thumbnail = [FLEXUtility
|
||||
thumbnailedImageWithMaxPixelDimension:maxPixelDimension
|
||||
fromImageData:responseBody
|
||||
];
|
||||
[self postUpdateNotificationForTransaction:transaction];
|
||||
});
|
||||
} else if ([mimeType isEqual:@"application/json"]) {
|
||||
transaction.responseThumbnail = FLEXResources.jsonIcon;
|
||||
transaction.thumbnail = FLEXResources.jsonIcon;
|
||||
} else if ([mimeType isEqual:@"text/plain"]){
|
||||
transaction.responseThumbnail = FLEXResources.textPlainIcon;
|
||||
transaction.thumbnail = FLEXResources.textPlainIcon;
|
||||
} else if ([mimeType isEqual:@"text/html"]) {
|
||||
transaction.responseThumbnail = FLEXResources.htmlIcon;
|
||||
transaction.thumbnail = FLEXResources.htmlIcon;
|
||||
} else if ([mimeType isEqual:@"application/x-plist"]) {
|
||||
transaction.responseThumbnail = FLEXResources.plistIcon;
|
||||
transaction.thumbnail = FLEXResources.plistIcon;
|
||||
} else if ([mimeType isEqual:@"application/octet-stream"] || [mimeType isEqual:@"application/binary"]) {
|
||||
transaction.responseThumbnail = FLEXResources.binaryIcon;
|
||||
transaction.thumbnail = FLEXResources.binaryIcon;
|
||||
} else if ([mimeType containsString:@"javascript"]) {
|
||||
transaction.responseThumbnail = FLEXResources.jsIcon;
|
||||
transaction.thumbnail = FLEXResources.jsIcon;
|
||||
} else if ([mimeType containsString:@"xml"]) {
|
||||
transaction.responseThumbnail = FLEXResources.xmlIcon;
|
||||
transaction.thumbnail = FLEXResources.xmlIcon;
|
||||
} else if ([mimeType hasPrefix:@"audio"]) {
|
||||
transaction.responseThumbnail = FLEXResources.audioIcon;
|
||||
transaction.thumbnail = FLEXResources.audioIcon;
|
||||
} else if ([mimeType hasPrefix:@"video"]) {
|
||||
transaction.responseThumbnail = FLEXResources.videoIcon;
|
||||
transaction.thumbnail = FLEXResources.videoIcon;
|
||||
} else if ([mimeType hasPrefix:@"text"]) {
|
||||
transaction.responseThumbnail = FLEXResources.textIcon;
|
||||
transaction.thumbnail = FLEXResources.textIcon;
|
||||
}
|
||||
|
||||
[self postUpdateNotificationForTransaction:transaction];
|
||||
@@ -247,7 +246,7 @@ NSString *const kFLEXNetworkRecorderResponseCacheLimitDefaultsKey = @"com.flex.r
|
||||
|
||||
- (void)recordLoadingFailedWithRequestID:(NSString *)requestID error:(NSError *)error {
|
||||
dispatch_async(self.queue, ^{
|
||||
FLEXNetworkTransaction *transaction = self.requestIDsToTransactions[requestID];
|
||||
FLEXHTTPTransaction *transaction = self.requestIDsToHTTPTransactions[requestID];
|
||||
if (!transaction) {
|
||||
return;
|
||||
}
|
||||
@@ -262,7 +261,7 @@ NSString *const kFLEXNetworkRecorderResponseCacheLimitDefaultsKey = @"com.flex.r
|
||||
|
||||
- (void)recordMechanism:(NSString *)mechanism forRequestID:(NSString *)requestID {
|
||||
dispatch_async(self.queue, ^{
|
||||
FLEXNetworkTransaction *transaction = self.requestIDsToTransactions[requestID];
|
||||
FLEXHTTPTransaction *transaction = self.requestIDsToHTTPTransactions[requestID];
|
||||
if (!transaction) {
|
||||
return;
|
||||
}
|
||||
@@ -272,6 +271,42 @@ NSString *const kFLEXNetworkRecorderResponseCacheLimitDefaultsKey = @"com.flex.r
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark - Websocket Events
|
||||
|
||||
- (void)recordWebsocketMessageSend:(NSURLSessionWebSocketMessage *)message task:(NSURLSessionWebSocketTask *)task {
|
||||
dispatch_async(self.queue, ^{
|
||||
FLEXWebsocketTransaction *send = [FLEXWebsocketTransaction
|
||||
withMessage:message task:task direction:FLEXWebsocketOutgoing
|
||||
];
|
||||
|
||||
[self.orderedWSTransactions addObject:send];
|
||||
[self postNewTransactionNotificationWithTransaction:send];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)recordWebsocketMessageSendCompletion:(NSURLSessionWebSocketMessage *)message error:(NSError *)error {
|
||||
dispatch_async(self.queue, ^{
|
||||
FLEXWebsocketTransaction *send = [self.orderedWSTransactions flex_firstWhere:^BOOL(FLEXWebsocketTransaction *t) {
|
||||
return t.message == message;
|
||||
}];
|
||||
send.error = error;
|
||||
send.transactionState = error ? FLEXNetworkTransactionStateFailed : FLEXNetworkTransactionStateFinished;
|
||||
|
||||
[self postUpdateNotificationForTransaction:send];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)recordWebsocketMessageReceived:(NSURLSessionWebSocketMessage *)message task:(NSURLSessionWebSocketTask *)task {
|
||||
dispatch_async(self.queue, ^{
|
||||
FLEXWebsocketTransaction *receive = [FLEXWebsocketTransaction
|
||||
withMessage:message task:task direction:FLEXWebsocketIncoming
|
||||
];
|
||||
|
||||
[self.orderedWSTransactions addObject:receive];
|
||||
[self postNewTransactionNotificationWithTransaction:receive];
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark Notification Posting
|
||||
|
||||
- (void)postNewTransactionNotificationWithTransaction:(FLEXNetworkTransaction *)transaction {
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -17,28 +17,91 @@ typedef NS_ENUM(NSInteger, FLEXNetworkTransactionState) {
|
||||
FLEXNetworkTransactionStateFailed
|
||||
};
|
||||
|
||||
typedef NS_ENUM(NSUInteger, FLEXWebsocketMessageDirection) {
|
||||
FLEXWebsocketIncoming = 1,
|
||||
FLEXWebsocketOutgoing,
|
||||
};
|
||||
|
||||
/// The shared base class for all types of network transactions.
|
||||
/// Subclasses should implement the descriptions and details properties, and assign a thumbnail.
|
||||
@interface FLEXNetworkTransaction : NSObject
|
||||
|
||||
@property (nonatomic, copy) NSString *requestID;
|
||||
+ (instancetype)withStartTime:(NSDate *)startTime;
|
||||
|
||||
@property (nonatomic) NSURLRequest *request;
|
||||
+ (NSString *)readableStringFromTransactionState:(FLEXNetworkTransactionState)state;
|
||||
|
||||
@property (nonatomic) NSError *error;
|
||||
/// Subclasses can override to provide error state based on response data as well
|
||||
@property (nonatomic, readonly) BOOL displayAsError;
|
||||
@property (nonatomic, readonly) NSDate *startTime;
|
||||
|
||||
@property (nonatomic) FLEXNetworkTransactionState transactionState;
|
||||
@property (nonatomic) int64_t receivedDataLength;
|
||||
/// A small thumbnail to preview the type of/the response
|
||||
@property (nonatomic) UIImage *thumbnail;
|
||||
|
||||
/// The most prominent line of the cell. Typically a URL endpoint or other distinguishing attribute.
|
||||
/// This line turns red when the transaction indicates an error.
|
||||
@property (nonatomic, readonly) NSString *primaryDescription;
|
||||
/// Something less important, such as a blob of data or the URL's domain.
|
||||
@property (nonatomic, readonly) NSString *secondaryDescription;
|
||||
/// Minor details to display at the bottom of the cell, such as a timestamp, HTTP method, or status.
|
||||
@property (nonatomic, readonly) NSString *tertiaryDescription;
|
||||
|
||||
/// Subclasses should implement for when the transaction is complete
|
||||
@property (nonatomic, readonly) NSArray<NSString *> *details;
|
||||
|
||||
/// The string to copy when the user selects the "copy" action
|
||||
@property (nonatomic, readonly) NSString *copyString;
|
||||
|
||||
/// Whether or not this request should show up when the user searches for a given string
|
||||
- (BOOL)matchesQuery:(NSString *)filterString;
|
||||
|
||||
@end
|
||||
|
||||
/// The shared base class for all NSURL-API-related transactions.
|
||||
/// Descriptions are generated by this class using the URL provided by subclasses.
|
||||
@interface FLEXURLTransaction : FLEXNetworkTransaction
|
||||
|
||||
+ (instancetype)withRequest:(NSURLRequest *)request startTime:(NSDate *)startTime;
|
||||
|
||||
@property (nonatomic, readonly) NSURLRequest *request;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface FLEXHTTPTransaction : FLEXURLTransaction
|
||||
|
||||
+ (instancetype)request:(NSURLRequest *)request identifier:(NSString *)requestID;
|
||||
|
||||
@property (nonatomic, readonly) NSString *requestID;
|
||||
@property (nonatomic) NSURLResponse *response;
|
||||
@property (nonatomic, copy) NSString *requestMechanism;
|
||||
@property (nonatomic) FLEXNetworkTransactionState transactionState;
|
||||
@property (nonatomic) NSError *error;
|
||||
|
||||
@property (nonatomic) NSDate *startTime;
|
||||
@property (nonatomic) NSTimeInterval latency;
|
||||
@property (nonatomic) NSTimeInterval duration;
|
||||
|
||||
@property (nonatomic) int64_t receivedDataLength;
|
||||
|
||||
/// Only applicable for image downloads. A small thumbnail to preview the full response.
|
||||
@property (nonatomic) UIImage *responseThumbnail;
|
||||
|
||||
/// Populated lazily. Handles both normal HTTPBody data and HTTPBodyStreams.
|
||||
@property (nonatomic, readonly) NSData *cachedRequestBody;
|
||||
|
||||
+ (NSString *)readableStringFromTransactionState:(FLEXNetworkTransactionState)state;
|
||||
@end
|
||||
|
||||
|
||||
@interface FLEXWebsocketTransaction : FLEXURLTransaction
|
||||
|
||||
+ (instancetype)withMessage:(NSURLSessionWebSocketMessage *)message
|
||||
task:(NSURLSessionWebSocketTask *)task
|
||||
direction:(FLEXWebsocketMessageDirection)direction API_AVAILABLE(ios(13.0));
|
||||
|
||||
+ (instancetype)withMessage:(NSURLSessionWebSocketMessage *)message
|
||||
task:(NSURLSessionWebSocketTask *)task
|
||||
direction:(FLEXWebsocketMessageDirection)direction
|
||||
startTime:(NSDate *)started API_AVAILABLE(ios(13.0));
|
||||
|
||||
//@property (nonatomic, readonly) NSURLSessionWebSocketTask *task;
|
||||
@property (nonatomic, readonly) NSURLSessionWebSocketMessage *message API_AVAILABLE(ios(13.0));
|
||||
@property (nonatomic, readonly) FLEXWebsocketMessageDirection direction API_AVAILABLE(ios(13.0));
|
||||
|
||||
@property (nonatomic, readonly) int64_t dataLength API_AVAILABLE(ios(13.0));
|
||||
|
||||
@end
|
||||
|
||||
@@ -7,23 +7,191 @@
|
||||
//
|
||||
|
||||
#import "FLEXNetworkTransaction.h"
|
||||
#import "FLEXResources.h"
|
||||
#import "FLEXUtility.h"
|
||||
|
||||
@interface FLEXNetworkTransaction ()
|
||||
|
||||
@property (nonatomic, readwrite) NSData *cachedRequestBody;
|
||||
@interface FLEXNetworkTransaction () {
|
||||
@protected
|
||||
|
||||
NSString *_primaryDescription;
|
||||
NSString *_secondaryDescription;
|
||||
NSString *_tertiaryDescription;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXNetworkTransaction
|
||||
|
||||
+ (NSString *)readableStringFromTransactionState:(FLEXNetworkTransactionState)state {
|
||||
NSString *readableString = nil;
|
||||
switch (state) {
|
||||
case FLEXNetworkTransactionStateUnstarted:
|
||||
readableString = @"Unstarted";
|
||||
break;
|
||||
|
||||
case FLEXNetworkTransactionStateAwaitingResponse:
|
||||
readableString = @"Awaiting Response";
|
||||
break;
|
||||
|
||||
case FLEXNetworkTransactionStateReceivingData:
|
||||
readableString = @"Receiving Data";
|
||||
break;
|
||||
|
||||
case FLEXNetworkTransactionStateFinished:
|
||||
readableString = @"Finished";
|
||||
break;
|
||||
|
||||
case FLEXNetworkTransactionStateFailed:
|
||||
readableString = @"Failed";
|
||||
break;
|
||||
}
|
||||
return readableString;
|
||||
}
|
||||
|
||||
+ (instancetype)withStartTime:(NSDate *)startTime {
|
||||
FLEXNetworkTransaction *transaction = [self new];
|
||||
transaction->_startTime = startTime;
|
||||
return transaction;
|
||||
}
|
||||
|
||||
- (BOOL)displayAsError {
|
||||
return _error != nil;
|
||||
}
|
||||
|
||||
- (NSString *)copyString {
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (BOOL)matchesQuery:(NSString *)filterString {
|
||||
return NO;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface FLEXURLTransaction ()
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXURLTransaction
|
||||
|
||||
+ (instancetype)withRequest:(NSURLRequest *)request startTime:(NSDate *)startTime {
|
||||
FLEXURLTransaction *transaction = [self withStartTime:startTime];
|
||||
transaction->_request = request;
|
||||
return transaction;
|
||||
}
|
||||
|
||||
- (NSString *)timestampStringFromRequestDate:(NSDate *)date {
|
||||
static NSDateFormatter *dateFormatter = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
dateFormatter = [NSDateFormatter new];
|
||||
dateFormatter.dateFormat = @"HH:mm:ss";
|
||||
});
|
||||
|
||||
return [dateFormatter stringFromDate:date];
|
||||
}
|
||||
|
||||
- (NSString *)primaryDescription {
|
||||
if (!_primaryDescription) {
|
||||
NSString *name = self.request.URL.lastPathComponent;
|
||||
if (!name.length) {
|
||||
name = @"/";
|
||||
}
|
||||
|
||||
if (_request.URL.query) {
|
||||
name = [name stringByAppendingFormat:@"?%@", self.request.URL.query];
|
||||
}
|
||||
|
||||
_primaryDescription = name;
|
||||
}
|
||||
|
||||
return _primaryDescription;
|
||||
}
|
||||
|
||||
- (NSString *)secondaryDescription {
|
||||
if (!_secondaryDescription) {
|
||||
NSMutableArray<NSString *> *mutablePathComponents = self.request.URL.pathComponents.mutableCopy;
|
||||
if (mutablePathComponents.count > 0) {
|
||||
[mutablePathComponents removeLastObject];
|
||||
}
|
||||
|
||||
NSString *path = self.request.URL.host;
|
||||
for (NSString *pathComponent in mutablePathComponents) {
|
||||
path = [path stringByAppendingPathComponent:pathComponent];
|
||||
}
|
||||
|
||||
_secondaryDescription = path;
|
||||
}
|
||||
|
||||
return _secondaryDescription;
|
||||
}
|
||||
|
||||
- (NSString *)tertiaryDescription {
|
||||
if (!_tertiaryDescription) {
|
||||
NSMutableArray<NSString *> *detailComponents = [NSMutableArray new];
|
||||
|
||||
NSString *timestamp = [self timestampStringFromRequestDate:self.startTime];
|
||||
if (timestamp.length > 0) {
|
||||
[detailComponents addObject:timestamp];
|
||||
}
|
||||
|
||||
// Omit method for GET (assumed as default)
|
||||
NSString *httpMethod = self.request.HTTPMethod;
|
||||
if (httpMethod.length > 0) {
|
||||
[detailComponents addObject:httpMethod];
|
||||
}
|
||||
|
||||
if (self.transactionState == FLEXNetworkTransactionStateFinished || self.transactionState == FLEXNetworkTransactionStateFailed) {
|
||||
[detailComponents addObjectsFromArray:self.details];
|
||||
} else {
|
||||
// Unstarted, Awaiting Response, Receiving Data, etc.
|
||||
NSString *state = [self.class readableStringFromTransactionState:self.transactionState];
|
||||
[detailComponents addObject:state];
|
||||
}
|
||||
|
||||
_tertiaryDescription = [detailComponents componentsJoinedByString:@" ・ "];
|
||||
}
|
||||
|
||||
return _tertiaryDescription;
|
||||
}
|
||||
|
||||
- (void)setTransactionState:(FLEXNetworkTransactionState)transactionState {
|
||||
super.transactionState = transactionState;
|
||||
// Reset bottom description
|
||||
_tertiaryDescription = nil;
|
||||
}
|
||||
|
||||
- (NSString *)copyString {
|
||||
return self.request.URL.absoluteString;
|
||||
}
|
||||
|
||||
- (BOOL)matchesQuery:(NSString *)filterString {
|
||||
return [self.request.URL.absoluteString localizedCaseInsensitiveContainsString:filterString];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface FLEXHTTPTransaction ()
|
||||
@property (nonatomic, readwrite) NSData *cachedRequestBody;
|
||||
@end
|
||||
|
||||
@implementation FLEXHTTPTransaction
|
||||
|
||||
+ (instancetype)request:(NSURLRequest *)request identifier:(NSString *)requestID {
|
||||
FLEXHTTPTransaction *httpt = [self withRequest:request startTime:NSDate.date];
|
||||
httpt->_requestID = requestID;
|
||||
return httpt;
|
||||
}
|
||||
|
||||
- (NSString *)description {
|
||||
NSString *description = [super description];
|
||||
|
||||
|
||||
description = [description stringByAppendingFormat:@" id = %@;", self.requestID];
|
||||
description = [description stringByAppendingFormat:@" url = %@;", self.request.URL];
|
||||
description = [description stringByAppendingFormat:@" duration = %f;", self.duration];
|
||||
description = [description stringByAppendingFormat:@" receivedDataLength = %lld", self.receivedDataLength];
|
||||
|
||||
|
||||
return description;
|
||||
}
|
||||
|
||||
@@ -49,30 +217,89 @@
|
||||
return _cachedRequestBody;
|
||||
}
|
||||
|
||||
+ (NSString *)readableStringFromTransactionState:(FLEXNetworkTransactionState)state {
|
||||
NSString *readableString = nil;
|
||||
switch (state) {
|
||||
case FLEXNetworkTransactionStateUnstarted:
|
||||
readableString = @"Unstarted";
|
||||
break;
|
||||
|
||||
case FLEXNetworkTransactionStateAwaitingResponse:
|
||||
readableString = @"Awaiting Response";
|
||||
break;
|
||||
|
||||
case FLEXNetworkTransactionStateReceivingData:
|
||||
readableString = @"Receiving Data";
|
||||
break;
|
||||
|
||||
case FLEXNetworkTransactionStateFinished:
|
||||
readableString = @"Finished";
|
||||
break;
|
||||
|
||||
case FLEXNetworkTransactionStateFailed:
|
||||
readableString = @"Failed";
|
||||
break;
|
||||
- (NSArray *)detailString {
|
||||
NSMutableArray<NSString *> *detailComponents = [NSMutableArray new];
|
||||
|
||||
NSString *statusCodeString = [FLEXUtility statusCodeStringFromURLResponse:self.response];
|
||||
if (statusCodeString.length > 0) {
|
||||
[detailComponents addObject:statusCodeString];
|
||||
}
|
||||
return readableString;
|
||||
|
||||
if (self.receivedDataLength > 0) {
|
||||
NSString *responseSize = [NSByteCountFormatter
|
||||
stringFromByteCount:self.receivedDataLength
|
||||
countStyle:NSByteCountFormatterCountStyleBinary
|
||||
];
|
||||
[detailComponents addObject:responseSize];
|
||||
}
|
||||
|
||||
NSString *totalDuration = [FLEXUtility stringFromRequestDuration:self.duration];
|
||||
NSString *latency = [FLEXUtility stringFromRequestDuration:self.latency];
|
||||
NSString *duration = [NSString stringWithFormat:@"%@ (%@)", totalDuration, latency];
|
||||
[detailComponents addObject:duration];
|
||||
|
||||
return detailComponents;
|
||||
}
|
||||
|
||||
- (BOOL)displayAsError {
|
||||
return [FLEXUtility isErrorStatusCodeFromURLResponse:self.response] || super.displayAsError;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation FLEXWebsocketTransaction
|
||||
|
||||
+ (instancetype)withMessage:(NSURLSessionWebSocketMessage *)message
|
||||
task:(NSURLSessionWebSocketTask *)task
|
||||
direction:(FLEXWebsocketMessageDirection)direction
|
||||
startTime:(NSDate *)started {
|
||||
FLEXWebsocketTransaction *wst = [self withRequest:task.originalRequest startTime:started];
|
||||
wst->_message = message;
|
||||
wst->_direction = direction;
|
||||
|
||||
// Populate receivedDataLength
|
||||
if (direction == FLEXWebsocketIncoming) {
|
||||
wst.receivedDataLength = wst.dataLength;
|
||||
wst.transactionState = FLEXNetworkTransactionStateFinished;
|
||||
}
|
||||
|
||||
// Populate thumbnail image
|
||||
if (message.type == NSURLSessionWebSocketMessageTypeData) {
|
||||
wst.thumbnail = FLEXResources.binaryIcon;
|
||||
} else {
|
||||
wst.thumbnail = FLEXResources.textIcon;
|
||||
}
|
||||
|
||||
return wst;
|
||||
}
|
||||
|
||||
+ (instancetype)withMessage:(NSURLSessionWebSocketMessage *)message
|
||||
task:(NSURLSessionWebSocketTask *)task
|
||||
direction:(FLEXWebsocketMessageDirection)direction {
|
||||
return [self withMessage:message task:task direction:direction startTime:NSDate.date];
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)details API_AVAILABLE(ios(13.0)) {
|
||||
return @[
|
||||
self.direction == FLEXWebsocketOutgoing ? @"SENT →" : @"→ RECEIVED",
|
||||
[NSByteCountFormatter
|
||||
stringFromByteCount:self.dataLength
|
||||
countStyle:NSByteCountFormatterCountStyleBinary
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
- (int64_t)dataLength {
|
||||
if (self.message) {
|
||||
if (self.message.type == NSURLSessionWebSocketMessageTypeString) {
|
||||
return self.message.string.length;
|
||||
}
|
||||
|
||||
return self.message.data.length;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -8,14 +8,13 @@
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
extern NSString * const kFLEXNetworkTransactionCellIdentifier;
|
||||
|
||||
@class FLEXNetworkTransaction;
|
||||
|
||||
@interface FLEXNetworkTransactionCell : UITableViewCell
|
||||
|
||||
@property (nonatomic) FLEXNetworkTransaction *transaction;
|
||||
|
||||
+ (CGFloat)preferredCellHeight;
|
||||
@property (nonatomic, readonly, class) NSString *reuseID;
|
||||
@property (nonatomic, readonly, class) CGFloat preferredCellHeight;
|
||||
|
||||
@end
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXResources.h"
|
||||
|
||||
NSString *const kFLEXNetworkTransactionCellIdentifier = @"kFLEXNetworkTransactionCellIdentifier";
|
||||
NSString * const kFLEXNetworkTransactionCellIdentifier = @"kFLEXNetworkTransactionCellIdentifier";
|
||||
|
||||
@interface FLEXNetworkTransactionCell ()
|
||||
|
||||
@@ -69,7 +69,7 @@ NSString *const kFLEXNetworkTransactionCellIdentifier = @"kFLEXNetworkTransactio
|
||||
|
||||
CGFloat thumbnailOriginY = round((self.contentView.bounds.size.height - kImageDimension) / 2.0);
|
||||
self.thumbnailImageView.frame = CGRectMake(kLeftPadding, thumbnailOriginY, kImageDimension, kImageDimension);
|
||||
self.thumbnailImageView.image = self.transaction.responseThumbnail;
|
||||
self.thumbnailImageView.image = self.transaction.thumbnail;
|
||||
|
||||
CGFloat textOriginX = CGRectGetMaxX(self.thumbnailImageView.frame) + kLeftPadding;
|
||||
CGFloat availableTextWidth = self.contentView.bounds.size.width - textOriginX;
|
||||
@@ -77,7 +77,7 @@ NSString *const kFLEXNetworkTransactionCellIdentifier = @"kFLEXNetworkTransactio
|
||||
self.nameLabel.text = [self nameLabelText];
|
||||
CGSize nameLabelPreferredSize = [self.nameLabel sizeThatFits:CGSizeMake(availableTextWidth, CGFLOAT_MAX)];
|
||||
self.nameLabel.frame = CGRectMake(textOriginX, kVerticalPadding, availableTextWidth, nameLabelPreferredSize.height);
|
||||
self.nameLabel.textColor = (self.transaction.error || [FLEXUtility isErrorStatusCodeFromURLResponse:self.transaction.response]) ? UIColor.redColor : FLEXColor.primaryTextColor;
|
||||
self.nameLabel.textColor = self.transaction.displayAsError ? UIColor.redColor : FLEXColor.primaryTextColor;
|
||||
|
||||
self.pathLabel.text = [self pathLabelText];
|
||||
CGSize pathLabelPreferredSize = [self.pathLabel sizeThatFits:CGSizeMake(availableTextWidth, CGFLOAT_MAX)];
|
||||
@@ -93,81 +93,23 @@ NSString *const kFLEXNetworkTransactionCellIdentifier = @"kFLEXNetworkTransactio
|
||||
}
|
||||
|
||||
- (NSString *)nameLabelText {
|
||||
NSURL *url = self.transaction.request.URL;
|
||||
NSString *name = [url lastPathComponent];
|
||||
if (name.length == 0) {
|
||||
name = @"/";
|
||||
}
|
||||
NSString *query = [url query];
|
||||
if (query) {
|
||||
name = [name stringByAppendingFormat:@"?%@", query];
|
||||
}
|
||||
return name;
|
||||
return self.transaction.primaryDescription;
|
||||
}
|
||||
|
||||
- (NSString *)pathLabelText {
|
||||
NSURL *url = self.transaction.request.URL;
|
||||
NSMutableArray<NSString *> *mutablePathComponents = url.pathComponents.mutableCopy;
|
||||
if (mutablePathComponents.count > 0) {
|
||||
[mutablePathComponents removeLastObject];
|
||||
}
|
||||
NSString *path = [url host];
|
||||
for (NSString *pathComponent in mutablePathComponents) {
|
||||
path = [path stringByAppendingPathComponent:pathComponent];
|
||||
}
|
||||
return path;
|
||||
return self.transaction.secondaryDescription;
|
||||
}
|
||||
|
||||
- (NSString *)transactionDetailsLabelText {
|
||||
NSMutableArray<NSString *> *detailComponents = [NSMutableArray new];
|
||||
|
||||
NSString *timestamp = [[self class] timestampStringFromRequestDate:self.transaction.startTime];
|
||||
if (timestamp.length > 0) {
|
||||
[detailComponents addObject:timestamp];
|
||||
}
|
||||
|
||||
// Omit method for GET (assumed as default)
|
||||
NSString *httpMethod = self.transaction.request.HTTPMethod;
|
||||
if (httpMethod.length > 0) {
|
||||
[detailComponents addObject:httpMethod];
|
||||
}
|
||||
|
||||
if (self.transaction.transactionState == FLEXNetworkTransactionStateFinished || self.transaction.transactionState == FLEXNetworkTransactionStateFailed) {
|
||||
NSString *statusCodeString = [FLEXUtility statusCodeStringFromURLResponse:self.transaction.response];
|
||||
if (statusCodeString.length > 0) {
|
||||
[detailComponents addObject:statusCodeString];
|
||||
}
|
||||
|
||||
if (self.transaction.receivedDataLength > 0) {
|
||||
NSString *responseSize = [NSByteCountFormatter stringFromByteCount:self.transaction.receivedDataLength countStyle:NSByteCountFormatterCountStyleBinary];
|
||||
[detailComponents addObject:responseSize];
|
||||
}
|
||||
|
||||
NSString *totalDuration = [FLEXUtility stringFromRequestDuration:self.transaction.duration];
|
||||
NSString *latency = [FLEXUtility stringFromRequestDuration:self.transaction.latency];
|
||||
NSString *duration = [NSString stringWithFormat:@"%@ (%@)", totalDuration, latency];
|
||||
[detailComponents addObject:duration];
|
||||
} else {
|
||||
// Unstarted, Awaiting Response, Receiving Data, etc.
|
||||
NSString *state = [FLEXNetworkTransaction readableStringFromTransactionState:self.transaction.transactionState];
|
||||
[detailComponents addObject:state];
|
||||
}
|
||||
|
||||
return [detailComponents componentsJoinedByString:@" ・ "];
|
||||
}
|
||||
|
||||
+ (NSString *)timestampStringFromRequestDate:(NSDate *)date {
|
||||
static NSDateFormatter *dateFormatter = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
dateFormatter = [NSDateFormatter new];
|
||||
dateFormatter.dateFormat = @"HH:mm:ss";
|
||||
});
|
||||
return [dateFormatter stringFromDate:date];
|
||||
return self.transaction.tertiaryDescription;
|
||||
}
|
||||
|
||||
+ (CGFloat)preferredCellHeight {
|
||||
return 65.0;
|
||||
}
|
||||
|
||||
+ (NSString *)reuseID {
|
||||
return kFLEXNetworkTransactionCellIdentifier;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
//
|
||||
// FLEXNetworkTransactionDetailController.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 2/10/15.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@class FLEXNetworkTransaction;
|
||||
|
||||
@interface FLEXNetworkTransactionDetailController : UITableViewController
|
||||
|
||||
@property (nonatomic) FLEXNetworkTransaction *transaction;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,20 @@
|
||||
OSCache
|
||||
version 1.2.1, Decembet 18th, 2015
|
||||
|
||||
Copyright (C) 2014 Charcoal Design
|
||||
|
||||
This software is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose,
|
||||
including commercial applications, and to alter it and redistribute it
|
||||
freely, subject to the following restrictions:
|
||||
|
||||
1. The origin of this software must not be misrepresented; you must not
|
||||
claim that you wrote the original software. If you use this software
|
||||
in a product, an acknowledgment in the product documentation would be
|
||||
appreciated but is not required.
|
||||
2. Altered source versions must be plainly marked as such, and must not be
|
||||
misrepresented as being the original software.
|
||||
3. This notice may not be removed or altered from any source distribution.
|
||||
@@ -0,0 +1,57 @@
|
||||
//
|
||||
// OSCache.h
|
||||
//
|
||||
// Version 1.2.1
|
||||
//
|
||||
// Created by Nick Lockwood on 01/01/2014.
|
||||
// Copyright (C) 2014 Charcoal Design
|
||||
//
|
||||
// Distributed under the permissive zlib License
|
||||
// Get the latest version from here:
|
||||
//
|
||||
// https://github.com/nicklockwood/OSCache
|
||||
//
|
||||
// This software is provided 'as-is', without any express or implied
|
||||
// warranty. In no event will the authors be held liable for any damages
|
||||
// arising from the use of this software.
|
||||
//
|
||||
// Permission is granted to anyone to use this software for any purpose,
|
||||
// including commercial applications, and to alter it and redistribute it
|
||||
// freely, subject to the following restrictions:
|
||||
//
|
||||
// 1. The origin of this software must not be misrepresented; you must not
|
||||
// claim that you wrote the original software. If you use this software
|
||||
// in a product, an acknowledgment in the product documentation would be
|
||||
// appreciated but is not required.
|
||||
//
|
||||
// 2. Altered source versions must be plainly marked as such, and must not be
|
||||
// misrepresented as being the original software.
|
||||
//
|
||||
// 3. This notice may not be removed or altered from any source distribution.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface OSCache <KeyType, ObjectType> : NSCache <NSFastEnumeration>
|
||||
|
||||
@property (nonatomic, readonly) NSUInteger count;
|
||||
@property (nonatomic, readonly) NSUInteger totalCost;
|
||||
|
||||
- (id)objectForKeyedSubscript:(KeyType <NSCopying>)key;
|
||||
- (void)setObject:(ObjectType)obj forKeyedSubscript:(KeyType <NSCopying>)key;
|
||||
- (void)enumerateKeysAndObjectsUsingBlock:(void (^)(KeyType key, ObjectType obj, BOOL *stop))block;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@protocol OSCacheDelegate <NSCacheDelegate>
|
||||
@optional
|
||||
|
||||
- (BOOL)cache:(OSCache *)cache shouldEvictObject:(id)entry;
|
||||
- (void)cache:(OSCache *)cache willEvictObject:(id)entry;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,409 @@
|
||||
//
|
||||
// OSCache.m
|
||||
//
|
||||
// Version 1.2.1
|
||||
//
|
||||
// Created by Nick Lockwood on 01/01/2014.
|
||||
// Copyright (C) 2014 Charcoal Design
|
||||
//
|
||||
// Distributed under the permissive zlib License
|
||||
// Get the latest version from here:
|
||||
//
|
||||
// https://github.com/nicklockwood/OSCache
|
||||
//
|
||||
// This software is provided 'as-is', without any express or implied
|
||||
// warranty. In no event will the authors be held liable for any damages
|
||||
// arising from the use of this software.
|
||||
//
|
||||
// Permission is granted to anyone to use this software for any purpose,
|
||||
// including commercial applications, and to alter it and redistribute it
|
||||
// freely, subject to the following restrictions:
|
||||
//
|
||||
// 1. The origin of this software must not be misrepresented; you must not
|
||||
// claim that you wrote the original software. If you use this software
|
||||
// in a product, an acknowledgment in the product documentation would be
|
||||
// appreciated but is not required.
|
||||
//
|
||||
// 2. Altered source versions must be plainly marked as such, and must not be
|
||||
// misrepresented as being the original software.
|
||||
//
|
||||
// 3. This notice may not be removed or altered from any source distribution.
|
||||
//
|
||||
|
||||
#import "OSCache.h"
|
||||
#import <TargetConditionals.h>
|
||||
#if TARGET_OS_IPHONE
|
||||
#import <UIKit/UIKit.h>
|
||||
#endif
|
||||
|
||||
|
||||
#import <Availability.h>
|
||||
#if !__has_feature(objc_arc)
|
||||
#error This class requires automatic reference counting
|
||||
#endif
|
||||
|
||||
|
||||
#pragma GCC diagnostic ignored "-Wobjc-missing-property-synthesis"
|
||||
#pragma GCC diagnostic ignored "-Wdirect-ivar-access"
|
||||
#pragma GCC diagnostic ignored "-Wgnu"
|
||||
|
||||
|
||||
@interface OSCacheEntry : NSObject
|
||||
|
||||
@property (nonatomic, strong) NSObject *object;
|
||||
@property (nonatomic, assign) NSUInteger cost;
|
||||
@property (nonatomic, assign) NSInteger sequenceNumber;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation OSCacheEntry
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface OSCache_Private : NSObject
|
||||
|
||||
@property (nonatomic, unsafe_unretained) id<OSCacheDelegate> delegate;
|
||||
@property (nonatomic, assign) NSUInteger countLimit;
|
||||
@property (nonatomic, assign) NSUInteger totalCostLimit;
|
||||
@property (nonatomic, copy) NSString *name;
|
||||
|
||||
@property (nonatomic, strong) NSMutableDictionary *cache;
|
||||
@property (nonatomic, assign) NSUInteger totalCost;
|
||||
@property (nonatomic, assign) NSInteger sequenceNumber;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation OSCache_Private
|
||||
{
|
||||
BOOL _delegateRespondsToWillEvictObject;
|
||||
BOOL _delegateRespondsToShouldEvictObject;
|
||||
BOOL _currentlyCleaning;
|
||||
NSMutableArray *_entryPool;
|
||||
NSLock *_lock;
|
||||
}
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
if ((self = [super init]))
|
||||
{
|
||||
//create storage
|
||||
_cache = [[NSMutableDictionary alloc] init];
|
||||
_entryPool = [[NSMutableArray alloc] init];
|
||||
_lock = [[NSLock alloc] init];
|
||||
_totalCost = 0;
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
|
||||
//clean up in the event of a memory warning
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(cleanUpAllObjects) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
|
||||
|
||||
#endif
|
||||
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
- (void)setDelegate:(id<OSCacheDelegate>)delegate
|
||||
{
|
||||
_delegate = delegate;
|
||||
_delegateRespondsToShouldEvictObject = [delegate respondsToSelector:@selector(cache:shouldEvictObject:)];
|
||||
_delegateRespondsToWillEvictObject = [delegate respondsToSelector:@selector(cache:willEvictObject:)];
|
||||
}
|
||||
|
||||
- (void)setCountLimit:(NSUInteger)countLimit
|
||||
{
|
||||
[_lock lock];
|
||||
_countLimit = countLimit;
|
||||
[_lock unlock];
|
||||
[self cleanUp:NO];
|
||||
}
|
||||
|
||||
- (void)setTotalCostLimit:(NSUInteger)totalCostLimit
|
||||
{
|
||||
[_lock lock];
|
||||
_totalCostLimit = totalCostLimit;
|
||||
[_lock unlock];
|
||||
[self cleanUp:NO];
|
||||
}
|
||||
|
||||
- (NSUInteger)count
|
||||
{
|
||||
return [_cache count];
|
||||
}
|
||||
|
||||
- (void)cleanUp:(BOOL)keepEntries
|
||||
{
|
||||
[_lock lock];
|
||||
NSUInteger maxCount = _countLimit ?: INT_MAX;
|
||||
NSUInteger maxCost = _totalCostLimit ?: INT_MAX;
|
||||
NSUInteger totalCount = _cache.count;
|
||||
NSMutableArray *keys = [_cache.allKeys mutableCopy];
|
||||
while (totalCount > maxCount || _totalCost > maxCost)
|
||||
{
|
||||
NSInteger lowestSequenceNumber = INT_MAX;
|
||||
OSCacheEntry *lowestEntry = nil;
|
||||
id lowestKey = nil;
|
||||
|
||||
//remove oldest items until within limit
|
||||
for (id key in keys)
|
||||
{
|
||||
OSCacheEntry *entry = _cache[key];
|
||||
if (entry.sequenceNumber < lowestSequenceNumber)
|
||||
{
|
||||
lowestSequenceNumber = entry.sequenceNumber;
|
||||
lowestEntry = entry;
|
||||
lowestKey = key;
|
||||
}
|
||||
}
|
||||
|
||||
if (lowestKey)
|
||||
{
|
||||
[keys removeObject:lowestKey];
|
||||
if (!_delegateRespondsToShouldEvictObject ||
|
||||
[_delegate cache:(OSCache *)self shouldEvictObject:lowestEntry.object])
|
||||
{
|
||||
if (_delegateRespondsToWillEvictObject)
|
||||
{
|
||||
_currentlyCleaning = YES;
|
||||
[self.delegate cache:(OSCache *)self willEvictObject:lowestEntry.object];
|
||||
_currentlyCleaning = NO;
|
||||
}
|
||||
[_cache removeObjectForKey:lowestKey];
|
||||
_totalCost -= lowestEntry.cost;
|
||||
totalCount --;
|
||||
if (keepEntries)
|
||||
{
|
||||
[_entryPool addObject:lowestEntry];
|
||||
lowestEntry.object = nil;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
[_lock unlock];
|
||||
}
|
||||
|
||||
- (void)cleanUpAllObjects
|
||||
{
|
||||
[_lock lock];
|
||||
if (_delegateRespondsToShouldEvictObject || _delegateRespondsToWillEvictObject)
|
||||
{
|
||||
NSArray *keys = [_cache allKeys];
|
||||
if (_delegateRespondsToShouldEvictObject)
|
||||
{
|
||||
//sort, oldest first (in case we want to use that information in our eviction test)
|
||||
keys = [keys sortedArrayUsingComparator:^NSComparisonResult(id key1, id key2) {
|
||||
OSCacheEntry *entry1 = self->_cache[key1];
|
||||
OSCacheEntry *entry2 = self->_cache[key2];
|
||||
return (NSComparisonResult)MIN(1, MAX(-1, entry1.sequenceNumber - entry2.sequenceNumber));
|
||||
}];
|
||||
}
|
||||
|
||||
//remove all items individually
|
||||
for (id key in keys)
|
||||
{
|
||||
OSCacheEntry *entry = _cache[key];
|
||||
if (!_delegateRespondsToShouldEvictObject || [_delegate cache:(OSCache *)self shouldEvictObject:entry.object])
|
||||
{
|
||||
if (_delegateRespondsToWillEvictObject)
|
||||
{
|
||||
_currentlyCleaning = YES;
|
||||
[_delegate cache:(OSCache *)self willEvictObject:entry.object];
|
||||
_currentlyCleaning = NO;
|
||||
}
|
||||
[_cache removeObjectForKey:key];
|
||||
_totalCost -= entry.cost;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_totalCost = 0;
|
||||
[_cache removeAllObjects];
|
||||
_sequenceNumber = 0;
|
||||
}
|
||||
[_lock unlock];
|
||||
}
|
||||
|
||||
- (void)resequence
|
||||
{
|
||||
//sort, oldest first
|
||||
NSArray *entries = [[_cache allValues] sortedArrayUsingComparator:^NSComparisonResult(OSCacheEntry *entry1, OSCacheEntry *entry2) {
|
||||
return (NSComparisonResult)MIN(1, MAX(-1, entry1.sequenceNumber - entry2.sequenceNumber));
|
||||
}];
|
||||
|
||||
//renumber items
|
||||
NSInteger index = 0;
|
||||
for (OSCacheEntry *entry in entries)
|
||||
{
|
||||
entry.sequenceNumber = index++;
|
||||
}
|
||||
}
|
||||
|
||||
- (id)objectForKey:(id)key
|
||||
{
|
||||
[_lock lock];
|
||||
OSCacheEntry *entry = _cache[key];
|
||||
entry.sequenceNumber = _sequenceNumber++;
|
||||
if (_sequenceNumber < 0)
|
||||
{
|
||||
[self resequence];
|
||||
}
|
||||
id object = entry.object;
|
||||
[_lock unlock];
|
||||
return object;
|
||||
}
|
||||
|
||||
- (id)objectForKeyedSubscript:(id<NSCopying>)key
|
||||
{
|
||||
return [self objectForKey:key];
|
||||
}
|
||||
|
||||
- (void)setObject:(id)obj forKey:(id)key
|
||||
{
|
||||
[self setObject:obj forKey:key cost:0];
|
||||
}
|
||||
|
||||
- (void)setObject:(id)obj forKeyedSubscript:(id<NSCopying>)key
|
||||
{
|
||||
[self setObject:obj forKey:key cost:0];
|
||||
}
|
||||
|
||||
- (void)setObject:(id)obj forKey:(id)key cost:(NSUInteger)g
|
||||
{
|
||||
if (!obj)
|
||||
{
|
||||
[self removeObjectForKey:key];
|
||||
return;
|
||||
}
|
||||
NSAssert(!_currentlyCleaning, @"It is not possible to modify cache from within the implementation of this delegate method.");
|
||||
[_lock lock];
|
||||
_totalCost -= [_cache[key] cost];
|
||||
_totalCost += g;
|
||||
OSCacheEntry *entry = _cache[key];
|
||||
if (!entry) {
|
||||
entry = [[OSCacheEntry alloc] init];
|
||||
_cache[key] = entry;
|
||||
}
|
||||
entry.object = obj;
|
||||
entry.cost = g;
|
||||
entry.sequenceNumber = _sequenceNumber++;
|
||||
if (_sequenceNumber < 0)
|
||||
{
|
||||
[self resequence];
|
||||
}
|
||||
[_lock unlock];
|
||||
[self cleanUp:YES];
|
||||
}
|
||||
|
||||
- (void)removeObjectForKey:(id)key
|
||||
{
|
||||
NSAssert(!_currentlyCleaning, @"It is not possible to modify cache from within the implementation of this delegate method.");
|
||||
[_lock lock];
|
||||
OSCacheEntry *entry = _cache[key];
|
||||
if (entry) {
|
||||
_totalCost -= entry.cost;
|
||||
entry.object = nil;
|
||||
[_entryPool addObject:entry];
|
||||
[_cache removeObjectForKey:key];
|
||||
}
|
||||
[_lock unlock];
|
||||
}
|
||||
|
||||
- (void)removeAllObjects
|
||||
{
|
||||
NSAssert(!_currentlyCleaning, @"It is not possible to modify cache from within the implementation of this delegate method.");
|
||||
[_lock lock];
|
||||
_totalCost = 0;
|
||||
_sequenceNumber = 0;
|
||||
for (OSCacheEntry *entry in _cache.allValues)
|
||||
{
|
||||
entry.object = nil;
|
||||
[_entryPool addObject:entry];
|
||||
}
|
||||
[_cache removeAllObjects];
|
||||
[_lock unlock];
|
||||
}
|
||||
|
||||
- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state
|
||||
objects:(id __unsafe_unretained [])buffer
|
||||
count:(NSUInteger)len
|
||||
{
|
||||
[_lock lock];
|
||||
NSUInteger count = [_cache countByEnumeratingWithState:state objects:buffer count:len];
|
||||
[_lock unlock];
|
||||
return count;
|
||||
}
|
||||
|
||||
- (void)enumerateKeysAndObjectsUsingBlock:(void (^)(id key, id obj, BOOL *stop))block
|
||||
{
|
||||
if (block)
|
||||
{
|
||||
[_lock lock];
|
||||
[_cache enumerateKeysAndObjectsUsingBlock:^(id key, OSCacheEntry *entry, BOOL *stop) {
|
||||
block(key, entry.object, stop);
|
||||
}];
|
||||
[_lock unlock];
|
||||
}
|
||||
}
|
||||
|
||||
//handle unimplemented methods
|
||||
|
||||
- (BOOL)isKindOfClass:(Class)aClass
|
||||
{
|
||||
//pretend that we're an NSCache if anyone asks
|
||||
if (aClass == [OSCache class] || aClass == [NSCache class])
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
return [super isKindOfClass:aClass];
|
||||
}
|
||||
|
||||
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector
|
||||
{
|
||||
//protect against calls to unimplemented NSCache methods
|
||||
NSMethodSignature *signature = [super methodSignatureForSelector:selector];
|
||||
if (!signature)
|
||||
{
|
||||
signature = [NSCache instanceMethodSignatureForSelector:selector];
|
||||
}
|
||||
return signature;
|
||||
}
|
||||
|
||||
- (void)forwardInvocation:(NSInvocation *)invocation
|
||||
{
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wnonnull"
|
||||
|
||||
[invocation invokeWithTarget:nil];
|
||||
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation OSCache
|
||||
|
||||
+ (instancetype)allocWithZone:(struct _NSZone *)zone
|
||||
{
|
||||
return (OSCache *)[OSCache_Private allocWithZone:zone];
|
||||
}
|
||||
|
||||
- (id)objectForKeyedSubscript:(__unused id<NSCopying>)key { return nil; }
|
||||
- (void)setObject:(__unused id)obj forKeyedSubscript:(__unused id<NSCopying>)key {}
|
||||
- (void)enumerateKeysAndObjectsUsingBlock:(__unused void (^)(id, id, BOOL *))block { }
|
||||
- (NSUInteger)countByEnumeratingWithState:(__unused NSFastEnumerationState *)state
|
||||
objects:(__unused __unsafe_unretained id [])buffer
|
||||
count:(__unused NSUInteger)len { return 0; }
|
||||
|
||||
@end
|
||||
@@ -11,10 +11,14 @@
|
||||
// See the LICENSE file distributed with this work for the terms under
|
||||
// which Square, Inc. licenses this file to you.
|
||||
//
|
||||
// Heavily modified and added to by Tanner Bennett and various other contributors.
|
||||
// git blame details these modifications.
|
||||
//
|
||||
|
||||
#import "FLEXNetworkObserver.h"
|
||||
#import "FLEXNetworkRecorder.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "NSUserDefaults+FLEX.h"
|
||||
#import "NSObject+FLEX_Reflection.h"
|
||||
#import "FLEXMethod.h"
|
||||
|
||||
@@ -24,7 +28,6 @@
|
||||
#include <dlfcn.h>
|
||||
|
||||
NSString *const kFLEXNetworkObserverEnabledStateChangedNotification = @"kFLEXNetworkObserverEnabledStateChangedNotification";
|
||||
static NSString *const kFLEXNetworkObserverEnabledDefaultsKey = @"com.flex.FLEXNetworkObserver.enableOnLaunch";
|
||||
|
||||
typedef void (^NSURLSessionAsyncCompletion)(id fileURLOrData, NSURLResponse *response, NSError *error);
|
||||
typedef NSURLSessionTask * (^NSURLSessionNewTaskMethod)(NSURLSession *, id, NSURLSessionAsyncCompletion);
|
||||
@@ -68,6 +71,15 @@ didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask delegate:(id<NSUR
|
||||
|
||||
- (void)URLSessionTaskWillResume:(NSURLSessionTask *)task;
|
||||
|
||||
- (void)websocketTask:(NSURLSessionWebSocketTask *)task
|
||||
sendMessagage:(NSURLSessionWebSocketMessage *)message API_AVAILABLE(ios(13.0));
|
||||
- (void)websocketTaskMessageSendCompletion:(NSURLSessionWebSocketMessage *)message
|
||||
error:(NSError *)error API_AVAILABLE(ios(13.0));
|
||||
|
||||
- (void)websocketTask:(NSURLSessionWebSocketTask *)task
|
||||
receiveMessagage:(NSURLSessionWebSocketMessage *)message
|
||||
error:(NSError *)error API_AVAILABLE(ios(13.0));
|
||||
|
||||
@end
|
||||
|
||||
@interface FLEXNetworkObserver ()
|
||||
@@ -84,12 +96,12 @@ didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask delegate:(id<NSUR
|
||||
+ (void)setEnabled:(BOOL)enabled {
|
||||
BOOL previouslyEnabled = [self isEnabled];
|
||||
|
||||
[NSUserDefaults.standardUserDefaults setBool:enabled forKey:kFLEXNetworkObserverEnabledDefaultsKey];
|
||||
NSUserDefaults.standardUserDefaults.flex_networkObserverEnabled = enabled;
|
||||
|
||||
if (enabled) {
|
||||
// Inject if needed. This injection is protected with a dispatch_once, so we're ok calling it multiple times.
|
||||
// By doing the injection lazily, we keep the impact of the tool lower when this feature isn't enabled.
|
||||
[self injectIntoAllNSURLConnectionDelegateClasses];
|
||||
[self injectIntoAllNSURLThings];
|
||||
}
|
||||
|
||||
if (previouslyEnabled != enabled) {
|
||||
@@ -98,14 +110,14 @@ didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask delegate:(id<NSUR
|
||||
}
|
||||
|
||||
+ (BOOL)isEnabled {
|
||||
return [[NSUserDefaults.standardUserDefaults objectForKey:kFLEXNetworkObserverEnabledDefaultsKey] boolValue];
|
||||
return NSUserDefaults.standardUserDefaults.flex_networkObserverEnabled;
|
||||
}
|
||||
|
||||
+ (void)load {
|
||||
// We don't want to do the swizzling from +load because not all the classes may be loaded at this point.
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if ([self isEnabled]) {
|
||||
[self injectIntoAllNSURLConnectionDelegateClasses];
|
||||
[self injectIntoAllNSURLThings];
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -128,9 +140,11 @@ didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask delegate:(id<NSUR
|
||||
#pragma mark Delegate Injection Convenience Methods
|
||||
|
||||
/// All swizzled delegate methods should make use of this guard.
|
||||
/// This will prevent duplicated sniffing when the original implementation calls up to a superclass implementation which we've also swizzled.
|
||||
/// The superclass implementation (and implementations in classes above that) will be executed without interference if called from the original implementation.
|
||||
+ (void)sniffWithoutDuplicationForObject:(NSObject *)object selector:(SEL)selector sniffingBlock:(void (^)(void))sniffingBlock originalImplementationBlock:(void (^)(void))originalImplementationBlock {
|
||||
/// This will prevent duplicated sniffing when the original implementation calls up to a superclass
|
||||
/// implementation which we've also swizzled. The superclass implementation (and implementations in
|
||||
/// classes above that) will be executed without interference if called from the original implementation.
|
||||
+ (void)sniffWithoutDuplicationForObject:(NSObject *)object selector:(SEL)selector
|
||||
sniffingBlock:(void (^)(void))sniffingBlock originalImplementationBlock:(void (^)(void))originalImplementationBlock {
|
||||
// If we don't have an object to detect nested calls on, just run the original implementation and bail.
|
||||
// This case can happen if someone besides the URL loading system calls the delegate methods directly.
|
||||
// See https://github.com/Flipboard/FLEX/issues/61 for an example.
|
||||
@@ -154,7 +168,7 @@ didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask delegate:(id<NSUR
|
||||
|
||||
#pragma mark - Delegate Injection
|
||||
|
||||
+ (void)injectIntoAllNSURLConnectionDelegateClasses {
|
||||
+ (void)injectIntoAllNSURLThings {
|
||||
// Only allow swizzling once.
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
@@ -224,6 +238,15 @@ didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask delegate:(id<NSUR
|
||||
|
||||
[self injectIntoNSURLSessionAsyncDataAndDownloadTaskMethods];
|
||||
[self injectIntoNSURLSessionAsyncUploadTaskMethods];
|
||||
|
||||
if (@available(iOS 13.0, *)) {
|
||||
Class websocketTask = NSClassFromString(@"__NSURLSessionWebSocketTask");
|
||||
[self injectWebsocketSendMessage:websocketTask];
|
||||
[self injectWebsocketReceiveMessage:websocketTask];
|
||||
websocketTask = [NSURLSessionWebSocketTask class];
|
||||
[self injectWebsocketSendMessage:websocketTask];
|
||||
[self injectWebsocketReceiveMessage:websocketTask];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1266,7 +1289,76 @@ didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask delegate:(id<NSUR
|
||||
implementationBlock:implementationBlock
|
||||
undefinedBlock:undefinedBlock
|
||||
];
|
||||
}
|
||||
|
||||
+ (void)injectWebsocketSendMessage:(Class)cls API_AVAILABLE(ios(13.0)) {
|
||||
SEL selector = @selector(sendMessage:completionHandler:);
|
||||
SEL swizzledSelector = [FLEXUtility swizzledSelectorForSelector:selector];
|
||||
|
||||
typedef void (^SendMessageBlock)(
|
||||
NSURLSessionWebSocketTask *slf,
|
||||
NSURLSessionWebSocketMessage *message,
|
||||
void (^completion)(NSError *error)
|
||||
);
|
||||
|
||||
SendMessageBlock implementationBlock = ^(
|
||||
NSURLSessionWebSocketTask *slf,
|
||||
NSURLSessionWebSocketMessage *message,
|
||||
void (^completion)(NSError *error)
|
||||
) {
|
||||
[FLEXNetworkObserver.sharedObserver
|
||||
websocketTask:slf sendMessagage:message
|
||||
];
|
||||
completion = ^(NSError *error) {
|
||||
[FLEXNetworkObserver.sharedObserver
|
||||
websocketTaskMessageSendCompletion:message
|
||||
error:error
|
||||
];
|
||||
};
|
||||
|
||||
((void(*)(id, SEL, id, id))objc_msgSend)(
|
||||
slf, swizzledSelector, message, completion
|
||||
);
|
||||
};
|
||||
|
||||
[FLEXUtility replaceImplementationOfKnownSelector:selector
|
||||
onClass:cls
|
||||
withBlock:implementationBlock
|
||||
swizzledSelector:swizzledSelector
|
||||
];
|
||||
}
|
||||
|
||||
+ (void)injectWebsocketReceiveMessage:(Class)cls API_AVAILABLE(ios(13.0)) {
|
||||
SEL selector = @selector(receiveMessageWithCompletionHandler:);
|
||||
SEL swizzledSelector = [FLEXUtility swizzledSelectorForSelector:selector];
|
||||
|
||||
typedef void (^SendMessageBlock)(
|
||||
NSURLSessionWebSocketTask *slf,
|
||||
void (^completion)(NSURLSessionWebSocketMessage *message, NSError *error)
|
||||
);
|
||||
|
||||
SendMessageBlock implementationBlock = ^(
|
||||
NSURLSessionWebSocketTask *slf,
|
||||
void (^completion)(NSURLSessionWebSocketMessage *message, NSError *error)
|
||||
) {
|
||||
id completionHook = ^(NSURLSessionWebSocketMessage *message, NSError *error) {
|
||||
[FLEXNetworkObserver.sharedObserver
|
||||
websocketTask:slf receiveMessagage:message error:error
|
||||
];
|
||||
completion(message, error);
|
||||
};
|
||||
|
||||
((void(*)(id, SEL, id))objc_msgSend)(
|
||||
slf, swizzledSelector, completionHook
|
||||
);
|
||||
|
||||
};
|
||||
|
||||
[FLEXUtility replaceImplementationOfKnownSelector:selector
|
||||
onClass:cls
|
||||
withBlock:implementationBlock
|
||||
swizzledSelector:swizzledSelector
|
||||
];
|
||||
}
|
||||
|
||||
static char const * const kFLEXRequestIDKey = "kFLEXRequestIDKey";
|
||||
@@ -1589,4 +1681,35 @@ didFinishDownloadingToURL:(NSURL *)location data:(NSData *)data
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)websocketTask:(NSURLSessionWebSocketTask *)task
|
||||
sendMessagage:(NSURLSessionWebSocketMessage *)message {
|
||||
[self performBlock:^{
|
||||
// NSString *requestID = [[self class] requestIDForConnectionOrTask:task];
|
||||
[FLEXNetworkRecorder.defaultRecorder recordWebsocketMessageSend:message task:task];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)websocketTaskMessageSendCompletion:(NSURLSessionWebSocketMessage *)message
|
||||
error:(NSError *)error {
|
||||
[self performBlock:^{
|
||||
[FLEXNetworkRecorder.defaultRecorder
|
||||
recordWebsocketMessageSendCompletion:message
|
||||
error:error
|
||||
];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)websocketTask:(NSURLSessionWebSocketTask *)task
|
||||
receiveMessagage:(NSURLSessionWebSocketMessage *)message
|
||||
error:(NSError *)error {
|
||||
[self performBlock:^{
|
||||
if (!error && message) {
|
||||
[FLEXNetworkRecorder.defaultRecorder
|
||||
recordWebsocketMessageReceived:message
|
||||
task:task
|
||||
];
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -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;
|
||||
@@ -178,6 +183,7 @@
|
||||
referencesSection.selectionAction = ^(UIViewController *host) {
|
||||
UIViewController *references = [FLEXObjectListViewController
|
||||
objectsWithReferencesToObject:explorer.object
|
||||
retained:NO
|
||||
];
|
||||
[host.navigationController pushViewController:references animated:YES];
|
||||
};
|
||||
@@ -194,8 +200,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 +273,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 +301,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 +313,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;
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
viewer:^UIViewController *(id obj) {
|
||||
return [FLEXObjectListViewController
|
||||
instancesOfClassWithName:NSStringFromClass(obj)
|
||||
retained:NO
|
||||
];
|
||||
}
|
||||
accessoryType:^UITableViewCellAccessoryType(id obj) {
|
||||
|
||||
@@ -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,9 @@
|
||||
@interface FLEXShortcutsFactory (Blocks) @end
|
||||
|
||||
@interface FLEXShortcutsFactory (Foundation) @end
|
||||
|
||||
@interface FLEXShortcutsFactory (WebKit_Safari) @end
|
||||
|
||||
@interface FLEXShortcutsFactory (Pasteboard) @end
|
||||
|
||||
@interface FLEXShortcutsFactory (FirebaseFirestore) @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,177 @@
|
||||
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
|
||||
|
||||
#pragma mark - Firebase Firestore
|
||||
|
||||
@implementation FLEXShortcutsFactory (FirebaseFirestore)
|
||||
|
||||
+ (void)load { FLEX_EXIT_IF_NO_CTORS()
|
||||
Class FIRDocumentSnap = NSClassFromString(@"FIRDocumentSnapshot");
|
||||
if (FIRDocumentSnap) {
|
||||
FLEXRuntimeUtilityTryAddObjectProperty(2, data, FIRDocumentSnap, NSDictionary, PropertyKey(ReadOnly));
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -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
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user