Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 691ca1132e | |||
| 5e005db93b | |||
| 23fc5ea533 | |||
| 09300aa62e | |||
| 7c5b2308f0 | |||
| b0a9cd2707 | |||
| b921bde7fb | |||
| 8b65f2a8a7 | |||
| 59d22e2c01 | |||
| 6f7969ab18 | |||
| 1d9d6e80e3 | |||
| 56e5d7ddf9 | |||
| 8542a74fb9 | |||
| 1a8759615a | |||
| 8300a30ca3 |
@@ -1,3 +0,0 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: [NSExceptional]
|
||||
+5
-2
@@ -20,5 +20,8 @@ DerivedData
|
||||
/Example/Pods
|
||||
Podfile.lock
|
||||
IDEWorkspaceChecks.plist
|
||||
*.xcworkspace
|
||||
.build
|
||||
|
||||
#tvOS specific
|
||||
libflex_*
|
||||
flexinjected/packages/*
|
||||
layout/Library/Frameworks/*.framework
|
||||
Vendored
-5
@@ -1,5 +0,0 @@
|
||||
{
|
||||
"search.exclude": {
|
||||
"Classes/Headers": true
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,6 @@
|
||||
#import "FLEXFilteringTableViewController.h"
|
||||
#import "FLEXTableViewSection.h"
|
||||
#import "NSArray+FLEX.h"
|
||||
#import "FLEXMacros.h"
|
||||
|
||||
@interface FLEXFilteringTableViewController ()
|
||||
|
||||
@@ -74,7 +73,7 @@
|
||||
#pragma mark - Search
|
||||
|
||||
- (void)updateSearchResults:(NSString *)newText {
|
||||
NSArray *(^filter)(void) = ^NSArray *{
|
||||
NSArray *(^filter)() = ^NSArray *{
|
||||
self.filterText = newText;
|
||||
|
||||
// Sections will adjust data based on this property
|
||||
@@ -188,6 +187,8 @@
|
||||
[self.filterDelegate.sections[indexPath.section] didPressInfoButtonAction:indexPath.row](self);
|
||||
}
|
||||
|
||||
#if FLEX_AT_LEAST_IOS13_SDK
|
||||
#if !TARGET_OS_TV
|
||||
- (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];
|
||||
@@ -205,5 +206,7 @@
|
||||
|
||||
return nil;
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@end
|
||||
|
||||
@@ -16,13 +16,4 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@end
|
||||
|
||||
@interface UINavigationController (FLEXObjectExploring)
|
||||
|
||||
/// Push an object explorer view controller onto the navigation stack
|
||||
- (void)pushExplorerForObject:(id)object;
|
||||
/// Push an object explorer view controller onto the navigation stack
|
||||
- (void)pushExplorerForObject:(id)object animated:(BOOL)animated;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -8,8 +8,9 @@
|
||||
|
||||
#import "FLEXNavigationController.h"
|
||||
#import "FLEXExplorerViewController.h"
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
#import "FLEXTabList.h"
|
||||
#import "FLEXColor.h"
|
||||
#import "NSObject+FLEX_Reflection.h"
|
||||
|
||||
@interface UINavigationController (Private) <UIGestureRecognizerDelegate>
|
||||
- (void)_gestureRecognizedInteractiveHide:(UIGestureRecognizer *)sender;
|
||||
@@ -21,7 +22,6 @@
|
||||
@interface FLEXNavigationController ()
|
||||
@property (nonatomic, readonly) BOOL toolbarWasHidden;
|
||||
@property (nonatomic) BOOL waitingToAddTab;
|
||||
@property (nonatomic, readonly) BOOL canShowToolbar;
|
||||
@property (nonatomic) BOOL didSetupPendingDismissButtons;
|
||||
@property (nonatomic) UISwipeGestureRecognizer *navigationBarSwipeGesture;
|
||||
@end
|
||||
@@ -29,8 +29,7 @@
|
||||
@implementation FLEXNavigationController
|
||||
|
||||
+ (instancetype)withRootViewController:(UIViewController *)rootVC {
|
||||
FLEXNavigationController *nav = [[self alloc] initWithRootViewController:rootVC];
|
||||
return nav;
|
||||
return [[self alloc] initWithRootViewController:rootVC];
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
@@ -51,8 +50,10 @@
|
||||
if (@available(iOS 13, *)) {
|
||||
switch (self.modalPresentationStyle) {
|
||||
case UIModalPresentationAutomatic:
|
||||
#if !TARGET_OS_TV
|
||||
case UIModalPresentationPageSheet:
|
||||
case UIModalPresentationFormSheet:
|
||||
#endif
|
||||
break;
|
||||
|
||||
default:
|
||||
@@ -67,17 +68,6 @@
|
||||
- (void)viewWillAppear:(BOOL)animated {
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
if (@available(iOS 15.0, *)) {
|
||||
UISheetPresentationController *presenter = self.sheetPresentationController;
|
||||
presenter.detents = @[
|
||||
UISheetPresentationControllerDetent.mediumDetent,
|
||||
UISheetPresentationControllerDetent.largeDetent,
|
||||
];
|
||||
presenter.prefersScrollingExpandsWhenScrolledToEdge = NO;
|
||||
presenter.selectedDetentIdentifier = UISheetPresentationControllerDetentIdentifierLarge;
|
||||
presenter.largestUndimmedDetentIdentifier = UISheetPresentationControllerDetentIdentifierLarge;
|
||||
}
|
||||
|
||||
if (self.beingPresented && !self.didSetupPendingDismissButtons) {
|
||||
for (UIViewController *vc in self.viewControllers) {
|
||||
[self addNavigationBarItemsToViewController:vc.navigationItem];
|
||||
@@ -85,6 +75,14 @@
|
||||
|
||||
self.didSetupPendingDismissButtons = YES;
|
||||
}
|
||||
#if TARGET_OS_TV
|
||||
if ([self darkMode]){
|
||||
self.view.backgroundColor = [UIColor colorWithWhite:0.0 alpha:0.8];
|
||||
} else {
|
||||
self.view.backgroundColor = [UIColor colorWithWhite:1.0 alpha:0.8];
|
||||
}
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated {
|
||||
@@ -99,6 +97,8 @@
|
||||
self.waitingToAddTab = NO;
|
||||
}
|
||||
}
|
||||
//the timing is janky here but its better than nothing for now.
|
||||
|
||||
}
|
||||
|
||||
- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated {
|
||||
@@ -106,27 +106,13 @@
|
||||
[self addNavigationBarItemsToViewController:viewController.navigationItem];
|
||||
}
|
||||
|
||||
- (void)dismissViewControllerAnimated:(BOOL)flag completion:(void (^)(void))completion {
|
||||
// Workaround for UIActivityViewController trying to dismiss us for some reason
|
||||
if (![self.viewControllers.lastObject.presentedViewController isKindOfClass:UIActivityViewController.self]) {
|
||||
[super dismissViewControllerAnimated:flag completion:completion];
|
||||
}
|
||||
}
|
||||
|
||||
- (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
|
||||
if ([self.presentingViewController isKindOfClass:[FLEXExplorerViewController class]]) {
|
||||
[FLEXTabList.sharedList closeTab:self];
|
||||
}
|
||||
|
||||
[FLEXTabList.sharedList closeTab:self];
|
||||
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (BOOL)canShowToolbar {
|
||||
return self.topViewController.toolbarItems.count > 0;
|
||||
}
|
||||
|
||||
- (void)addNavigationBarItemsToViewController:(UINavigationItem *)navigationItem {
|
||||
if (!self.presentingViewController) {
|
||||
return;
|
||||
@@ -160,6 +146,7 @@
|
||||
}
|
||||
|
||||
- (void)addNavigationBarSwipeGesture {
|
||||
#if !TARGET_OS_TV
|
||||
UISwipeGestureRecognizer *swipe = [[UISwipeGestureRecognizer alloc]
|
||||
initWithTarget:self action:@selector(handleNavigationBarSwipe:)
|
||||
];
|
||||
@@ -167,6 +154,7 @@
|
||||
swipe.delegate = self;
|
||||
self.navigationBarSwipeGesture = swipe;
|
||||
[self.navigationBar addGestureRecognizer:swipe];
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)handleNavigationBarSwipe:(UISwipeGestureRecognizer *)sender {
|
||||
@@ -176,20 +164,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 && self.canShowToolbar) {
|
||||
#if !TARGET_OS_TV
|
||||
if (self.toolbarHidden) {
|
||||
[self setToolbarHidden:NO animated:YES];
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)g1 shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)g2 {
|
||||
if (g1 == self.navigationBarSwipeGesture && g2 == self.barHideOnSwipeGestureRecognizer) {
|
||||
return YES;
|
||||
@@ -197,10 +180,11 @@
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
#endif
|
||||
- (void)_gestureRecognizedInteractiveHide:(UIPanGestureRecognizer *)sender {
|
||||
#if !TARGET_OS_TV
|
||||
if (sender.state == UIGestureRecognizerStateRecognized) {
|
||||
BOOL show = self.canShowToolbar;
|
||||
BOOL show = self.topViewController.toolbarItems.count;
|
||||
CGFloat yTranslation = [sender translationInView:self.view].y;
|
||||
CGFloat yVelocity = [sender velocityInView:self.view].y;
|
||||
if (yVelocity > 2000) {
|
||||
@@ -211,21 +195,7 @@
|
||||
[self setToolbarHidden:YES animated:YES];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@implementation UINavigationController (FLEXObjectExploring)
|
||||
|
||||
- (void)pushExplorerForObject:(id)object {
|
||||
[self pushExplorerForObject:object animated:YES];
|
||||
}
|
||||
|
||||
- (void)pushExplorerForObject:(id)object animated:(BOOL)animated {
|
||||
UIViewController *explorer = [FLEXObjectExplorerFactory explorerViewControllerForObject:object];
|
||||
if (explorer) {
|
||||
[self pushViewController:explorer animated:animated];
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -67,10 +67,6 @@ extern CGFloat const kFLEXDebounceForExpensiveIO;
|
||||
/// Setting this to YES will make the search bar appear whenever the view appears.
|
||||
/// Otherwise, iOS will only show the search bar when you scroll up.
|
||||
@property (nonatomic) BOOL showSearchBarInitially;
|
||||
/// Defaults to NO.
|
||||
///
|
||||
/// Setting this to YES will make the search bar activate whenever the view appears.
|
||||
@property (nonatomic) BOOL activatesSearchBarAutomatically;
|
||||
|
||||
/// nil unless showsSearchBar is set to YES.
|
||||
///
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
#import "FLEXResources.h"
|
||||
#import "UIBarButtonItem+FLEX.h"
|
||||
#import <objc/runtime.h>
|
||||
|
||||
#if TARGET_OS_TV
|
||||
#import "FLEXTV.h"
|
||||
#endif
|
||||
@interface Block : NSObject
|
||||
- (void)invoke;
|
||||
@end
|
||||
@@ -39,6 +41,7 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
|
||||
@property (nonatomic) UIBarButtonItem *middleToolbarItem;
|
||||
@property (nonatomic) UIBarButtonItem *middleLeftToolbarItem;
|
||||
@property (nonatomic) UIBarButtonItem *leftmostToolbarItem;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXTableViewController
|
||||
@@ -50,12 +53,19 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
|
||||
#pragma mark - Initialization
|
||||
|
||||
- (id)init {
|
||||
#if FLEX_AT_LEAST_IOS13_SDK
|
||||
if (@available(iOS 13.0, *)) {
|
||||
#if !TARGET_OS_TV
|
||||
self = [self initWithStyle:UITableViewStyleInsetGrouped];
|
||||
#else
|
||||
self = [self initWithStyle:UITableViewStyleGrouped];
|
||||
#endif
|
||||
} else {
|
||||
self = [self initWithStyle:UITableViewStyleGrouped];
|
||||
}
|
||||
|
||||
#else
|
||||
self = [self initWithStyle:UITableViewStyleGrouped];
|
||||
#endif
|
||||
return self;
|
||||
}
|
||||
|
||||
@@ -66,9 +76,9 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
|
||||
_searchBarDebounceInterval = kFLEXDebounceFast;
|
||||
_showSearchBarInitially = YES;
|
||||
_style = style;
|
||||
_manuallyDeactivateSearchOnDisappear = (
|
||||
NSProcessInfo.processInfo.operatingSystemVersion.majorVersion < 11
|
||||
);
|
||||
_manuallyDeactivateSearchOnDisappear = ({
|
||||
NSProcessInfo.processInfo.operatingSystemVersion.majorVersion < 11;
|
||||
});
|
||||
|
||||
// We will be our own search delegate if we implement this method
|
||||
if ([self respondsToSelector:@selector(updateSearchResults:)]) {
|
||||
@@ -86,6 +96,15 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
|
||||
return (id)self.view.window;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
search is handled a bit differently on tvOS and i couldnt get its pardigm to cooperate, thankfully the UISearchController never needs to be visible to actually work its magic.
|
||||
since 3D snapshot viewing doesn't exist on tvOS the leftBarButtonItem is the perfect spot to add a 'search' button. this search button will present a new text entry controller
|
||||
the alpha on this viewController is decreased to 0.6 to make it possible to view the filtering changes underneath in realtime. The zero rect textfield associated with
|
||||
the search button acts as a proxy to transfer the text to our search bar as necessary
|
||||
|
||||
*/
|
||||
|
||||
- (void)setShowsSearchBar:(BOOL)showsSearchBar {
|
||||
if (_showsSearchBar == showsSearchBar) return;
|
||||
_showsSearchBar = showsSearchBar;
|
||||
@@ -96,21 +115,20 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
|
||||
self.searchController.searchBar.placeholder = @"Filter";
|
||||
self.searchController.searchResultsUpdater = (id)self;
|
||||
self.searchController.delegate = (id)self;
|
||||
if (@available(iOS 9.1, *)) {
|
||||
self.searchController.obscuresBackgroundDuringPresentation = NO;
|
||||
} else {
|
||||
self.searchController.dimsBackgroundDuringPresentation = NO;
|
||||
}
|
||||
#if !TARGET_OS_TV
|
||||
self.searchController.searchBar.delegate = self;
|
||||
self.searchController.dimsBackgroundDuringPresentation = NO;
|
||||
#endif
|
||||
self.searchController.hidesNavigationBarDuringPresentation = NO;
|
||||
/// Not necessary in iOS 13; remove this when iOS 13 is the minimum deployment target
|
||||
self.searchController.searchBar.delegate = self;
|
||||
|
||||
|
||||
self.automaticallyShowsSearchBarCancelButton = YES;
|
||||
|
||||
#if FLEX_AT_LEAST_IOS13_SDK
|
||||
if (@available(iOS 13, *)) {
|
||||
self.searchController.automaticallyShowsScopeBar = NO;
|
||||
}
|
||||
|
||||
#endif
|
||||
[self addSearchController:self.searchController];
|
||||
} else {
|
||||
// Search already shown and just set to NO, so remove it
|
||||
@@ -123,15 +141,18 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
|
||||
_showsCarousel = showsCarousel;
|
||||
|
||||
if (showsCarousel) {
|
||||
_carousel = ({ weakify(self)
|
||||
|
||||
_carousel = ({
|
||||
__weak __typeof(self) weakSelf = self;
|
||||
|
||||
FLEXScopeCarousel *carousel = [FLEXScopeCarousel new];
|
||||
carousel.selectedIndexChangedAction = ^(NSInteger idx) { strongify(self);
|
||||
carousel.selectedIndexChangedAction = ^(NSInteger idx) {
|
||||
__typeof(self) self = weakSelf;
|
||||
[self.searchDelegate updateSearchResults:self.searchText];
|
||||
};
|
||||
|
||||
// UITableView won't update the header size unless you reset the header view
|
||||
[carousel registerBlockForDynamicTypeChanges:^(FLEXScopeCarousel *_) { strongify(self);
|
||||
[carousel registerBlockForDynamicTypeChanges:^(FLEXScopeCarousel *carousel) {
|
||||
__typeof(self) self = weakSelf;
|
||||
[self layoutTableHeaderIfNeeded];
|
||||
}];
|
||||
|
||||
@@ -169,17 +190,21 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
|
||||
}
|
||||
|
||||
- (BOOL)automaticallyShowsSearchBarCancelButton {
|
||||
#if FLEX_AT_LEAST_IOS13_SDK
|
||||
if (@available(iOS 13, *)) {
|
||||
return self.searchController.automaticallyShowsCancelButton;
|
||||
}
|
||||
#endif
|
||||
|
||||
return _automaticallyShowsSearchBarCancelButton;
|
||||
}
|
||||
|
||||
- (void)setAutomaticallyShowsSearchBarCancelButton:(BOOL)value {
|
||||
#if FLEX_AT_LEAST_IOS13_SDK
|
||||
if (@available(iOS 13, *)) {
|
||||
self.searchController.automaticallyShowsCancelButton = value;
|
||||
}
|
||||
#endif
|
||||
|
||||
_automaticallyShowsSearchBarCancelButton = value;
|
||||
}
|
||||
@@ -201,9 +226,11 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
|
||||
}
|
||||
|
||||
- (void)disableToolbar {
|
||||
#if !TARGET_OS_TV
|
||||
self.navigationController.toolbarHidden = YES;
|
||||
self.navigationController.hidesBarsOnSwipe = NO;
|
||||
self.toolbarItems = nil;
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -214,8 +241,6 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
|
||||
self.tableView.dataSource = self;
|
||||
self.tableView.delegate = self;
|
||||
|
||||
self.tableView.estimatedRowHeight = 10;
|
||||
|
||||
_shareToolbarItem = FLEXBarButtonItemSystem(Action, self, @selector(shareButtonPressed:));
|
||||
_bookmarksToolbarItem = [UIBarButtonItem
|
||||
flex_itemWithImage:FLEXResources.bookmarksIcon target:self action:@selector(showBookmarks)
|
||||
@@ -235,9 +260,10 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
|
||||
self.tableView.keyboardDismissMode = UIScrollViewKeyboardDismissModeOnDrag;
|
||||
|
||||
// Toolbar
|
||||
self.navigationController.toolbarHidden = self.toolbarItems.count > 0;
|
||||
#if !TARGET_OS_TV
|
||||
self.navigationController.toolbarHidden = NO;
|
||||
self.navigationController.hidesBarsOnSwipe = YES;
|
||||
|
||||
#endif
|
||||
// On iOS 13, the root view controller shows it's search bar no matter what.
|
||||
// Turning this off avoids some weird flash the navigation bar does when we
|
||||
// toggle navigationItem.hidesSearchBarWhenScrolling on and off. The flash
|
||||
@@ -253,18 +279,19 @@ 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) {
|
||||
#if !TARGET_OS_TV
|
||||
self.navigationItem.hidesSearchBarWhenScrolling = NO;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
// Make the keyboard seem to appear faster
|
||||
if (self.activatesSearchBarAutomatically) {
|
||||
[self makeKeyboardAppearNow];
|
||||
}
|
||||
|
||||
#if TARGET_OS_TV
|
||||
UIEdgeInsets insets = self.tableView.contentInset;
|
||||
insets.top+=20;
|
||||
self.tableView.contentInset = insets;
|
||||
#endif
|
||||
[self setupToolbarItems];
|
||||
}
|
||||
|
||||
@@ -279,23 +306,14 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
|
||||
// the search bar appear initially results in a bugged search bar that
|
||||
// becomes transparent and floats over the screen as you scroll
|
||||
[UIView animateWithDuration:0.2 animations:^{
|
||||
#if !TARGET_OS_TV
|
||||
self.navigationItem.hidesSearchBarWhenScrolling = YES;
|
||||
#endif
|
||||
[self.navigationController.view setNeedsLayout];
|
||||
[self.navigationController.view layoutIfNeeded];
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -323,7 +341,7 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
|
||||
if (!self.isViewLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
self.toolbarItems = @[
|
||||
self.leftmostToolbarItem,
|
||||
UIBarButtonItem.flex_flexibleSpace,
|
||||
@@ -341,7 +359,7 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
|
||||
// This does not work for anything but fixed spaces for some reason
|
||||
// item.width = 60;
|
||||
}
|
||||
|
||||
#endif
|
||||
// Disable tabs entirely when not presented by FLEXExplorerViewController
|
||||
UIViewController *presenter = self.navigationController.presentingViewController;
|
||||
if (![presenter isKindOfClass:[FLEXExplorerViewController class]]) {
|
||||
@@ -469,6 +487,12 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
|
||||
}
|
||||
|
||||
- (void)addSearchController:(UISearchController *)controller {
|
||||
#if TARGET_OS_TV
|
||||
KBSearchButton *sb = [KBSearchButton buttonWithType:UIButtonTypeSystem];
|
||||
sb.searchBar = self.searchController.searchBar;
|
||||
UIBarButtonItem *searchButton = [[UIBarButtonItem alloc] initWithCustomView:sb];
|
||||
self.navigationItem.leftBarButtonItem = searchButton;
|
||||
#else
|
||||
if (@available(iOS 11.0, *)) {
|
||||
self.navigationItem.searchController = controller;
|
||||
} else {
|
||||
@@ -482,17 +506,21 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
|
||||
// Move the carousel down if it's already there
|
||||
if (self.showsCarousel) {
|
||||
self.carousel.frame = FLEXRectSetY(
|
||||
self.carousel.frame, subviewFrame.size.height
|
||||
);
|
||||
self.carousel.frame, subviewFrame.size.height
|
||||
);
|
||||
frame.size.height += self.carousel.frame.size.height;
|
||||
}
|
||||
|
||||
self.tableHeaderViewContainer.frame = frame;
|
||||
[self layoutTableHeaderIfNeeded];
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)removeSearchController:(UISearchController *)controller {
|
||||
#if TARGET_OS_TV
|
||||
self.navigationItem.leftBarButtonItem = nil;
|
||||
#else
|
||||
if (@available(iOS 11.0, *)) {
|
||||
self.navigationItem.searchController = nil;
|
||||
} else {
|
||||
@@ -507,6 +535,7 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
|
||||
_tableHeaderViewContainer = nil;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
- (UIView *)tableHeaderViewContainer {
|
||||
@@ -535,37 +564,13 @@ 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) = ^{
|
||||
void (^updateSearchResults)() = ^{
|
||||
if (self.searchResultsUpdater) {
|
||||
[self.searchResultsUpdater updateSearchResults:text];
|
||||
} else {
|
||||
@@ -582,7 +587,7 @@ static UITextField *kDummyTextField = nil;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
#pragma mark UISearchControllerDelegate
|
||||
|
||||
- (void)willPresentSearchController:(UISearchController *)searchController {
|
||||
@@ -606,19 +611,51 @@ static UITextField *kDummyTextField = nil;
|
||||
- (void)searchBar:(UISearchBar *)searchBar selectedScopeButtonIndexDidChange:(NSInteger)selectedScope {
|
||||
[self updateSearchResultsForSearchController:self.searchController];
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#pragma mark Table View
|
||||
|
||||
/// Not having a title in the first section looks weird with a rounded-corner table view style
|
||||
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
|
||||
if (@available(iOS 13, *)) {
|
||||
#if !TARGET_OS_TV
|
||||
if (self.style == UITableViewStyleInsetGrouped) {
|
||||
return @" ";
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
return nil; // For plain/gropued style
|
||||
}
|
||||
|
||||
#if TARGET_OS_TV
|
||||
#pragma mark tvOS
|
||||
|
||||
/*
|
||||
This tracks our most recently focused cell so when we leave / return to this view we can refocus to the proper index path.
|
||||
*/
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didUpdateFocusInContext:(UITableViewFocusUpdateContext *)context withAnimationCoordinator:(UIFocusAnimationCoordinator *)coordinator {
|
||||
[coordinator addCoordinatedAnimations:^{
|
||||
|
||||
NSIndexPath *nextIndexPath = context.nextFocusedIndexPath;
|
||||
KBTableView *table = (KBTableView *)tableView;
|
||||
if ([table respondsToSelector:@selector(setSelectedIndexPath:)]){
|
||||
if (nextIndexPath != nil){
|
||||
[table setSelectedIndexPath:nextIndexPath];
|
||||
}
|
||||
}
|
||||
} completion:nil];
|
||||
}
|
||||
|
||||
- (NSArray *)preferredFocusEnvironments {
|
||||
if (self.tableView.selectedIndexPath){
|
||||
return @[[self.tableView cellForRowAtIndexPath:self.tableView.selectedIndexPath]];
|
||||
}
|
||||
return @[self];
|
||||
}
|
||||
|
||||
|
||||
#endif
|
||||
|
||||
@end
|
||||
|
||||
@@ -8,8 +8,6 @@
|
||||
|
||||
#import "FLEXTableViewSection.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/// A section providing a specific single row.
|
||||
///
|
||||
/// You may optionally provide a view controller to push when the row
|
||||
@@ -18,15 +16,13 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
@interface FLEXSingleRowSection : FLEXTableViewSection
|
||||
|
||||
/// @param reuseIdentifier if nil, kFLEXDefaultCell is used.
|
||||
+ (instancetype)title:(nullable NSString *)sectionTitle
|
||||
reuse:(nullable NSString *)reuseIdentifier
|
||||
+ (instancetype)title:(NSString *)sectionTitle
|
||||
reuse:(NSString *)reuseIdentifier
|
||||
cell:(void(^)(__kindof UITableViewCell *cell))cellConfiguration;
|
||||
|
||||
@property (nullable, nonatomic) UIViewController *pushOnSelection;
|
||||
@property (nullable, nonatomic) void (^selectionAction)(UIViewController *host);
|
||||
@property (nonatomic) UIViewController *pushOnSelection;
|
||||
@property (nonatomic) void (^selectionAction)(UIViewController *host);
|
||||
/// Called to determine whether the single row should display itself or not.
|
||||
@property (nonatomic) BOOL (^filterMatcher)(NSString *filterText);
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -30,8 +30,6 @@
|
||||
- (id)initWithTitle:(NSString *)sectionTitle
|
||||
reuse:(NSString *)reuseIdentifier
|
||||
cell:(void (^)(__kindof UITableViewCell *))cellConfiguration {
|
||||
NSParameterAssert(cellConfiguration);
|
||||
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_title = sectionTitle;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "FLEXMacros.h"
|
||||
#import "NSArray+FLEX.h"
|
||||
@class FLEXTableView;
|
||||
|
||||
@@ -100,6 +101,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
- (nullable void(^)(__kindof UIViewController *host))didPressInfoButtonAction:(NSInteger)row;
|
||||
|
||||
#pragma mark - Context Menus
|
||||
#if FLEX_AT_LEAST_IOS13_SDK
|
||||
|
||||
/// By default, this is the title of the row.
|
||||
/// @return The title of the context menu, if any.
|
||||
@@ -119,6 +121,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
/// should be a description of what will be copied, and the values should be
|
||||
/// the strings to copy. Return an empty string as a value to show a disabled action.
|
||||
- (nullable NSArray<NSString *> *)copyMenuItemsForRow:(NSInteger)row API_AVAILABLE(ios(13.0));
|
||||
#endif
|
||||
|
||||
#pragma mark - Cell Configuration
|
||||
|
||||
|
||||
@@ -64,6 +64,8 @@
|
||||
return kFLEXDefaultCell;
|
||||
}
|
||||
|
||||
#if FLEX_AT_LEAST_IOS13_SDK
|
||||
|
||||
- (NSString *)menuTitleForRow:(NSInteger)row {
|
||||
NSString *title = [self titleForRow:row];
|
||||
NSString *subtitle = [self menuSubtitleForRow:row];
|
||||
@@ -79,7 +81,7 @@
|
||||
return @"";
|
||||
}
|
||||
|
||||
- (NSArray<UIMenuElement *> *)menuItemsForRow:(NSInteger)row sender:(UIViewController *)sender API_AVAILABLE(ios(13.0)) {
|
||||
- (NSArray<UIMenuElement *> *)menuItemsForRow:(NSInteger)row sender:(UIViewController *)sender API_AVAILABLE(ios(13)) {
|
||||
NSArray<NSString *> *copyItems = [self copyMenuItemsForRow:row];
|
||||
NSAssert(copyItems.count % 2 == 0, @"copyMenuItemsForRow: should return an even list");
|
||||
|
||||
@@ -99,7 +101,9 @@
|
||||
image:copyIcon
|
||||
identifier:nil
|
||||
handler:^(__kindof UIAction *action) {
|
||||
#if !TARGET_OS_TV
|
||||
UIPasteboard.generalPasteboard.string = value;
|
||||
#endif
|
||||
}
|
||||
];
|
||||
if (!value.length) {
|
||||
@@ -125,6 +129,8 @@
|
||||
return @[];
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
- (NSArray<NSString *> *)copyMenuItemsForRow:(NSInteger)row {
|
||||
return nil;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
#import "FLEXScopeCarousel.h"
|
||||
#import "FLEXCarouselCell.h"
|
||||
#import "FLEXColor.h"
|
||||
#import "FLEXMacros.h"
|
||||
#import "UIView+FLEX_Layout.h"
|
||||
|
||||
const CGFloat kCarouselItemSpacing = 0;
|
||||
@@ -73,10 +72,11 @@ NSString * const kCarouselCellReuseIdentifier = @"kCarouselCellReuseIdentifier";
|
||||
self.sizingCell.title = @"NSObject";
|
||||
|
||||
// Dynamic type
|
||||
weakify(self);
|
||||
__weak __typeof(self) weakSelf = self;
|
||||
_dynamicTypeObserver = [NSNotificationCenter.defaultCenter
|
||||
addObserverForName:UIContentSizeCategoryDidChangeNotification
|
||||
object:nil queue:nil usingBlock:^(NSNotification *note) { strongify(self)
|
||||
object:nil queue:nil usingBlock:^(NSNotification *note) {
|
||||
__typeof(self) self = weakSelf;
|
||||
[self.collectionView setNeedsLayout];
|
||||
[self setNeedsUpdateConstraints];
|
||||
|
||||
@@ -201,4 +201,10 @@ NSString * const kCarouselCellReuseIdentifier = @"kCarouselCellReuseIdentifier";
|
||||
[self sendActionsForControlEvents:UIControlEventValueChanged];
|
||||
}
|
||||
|
||||
#if TARGET_OS_TV
|
||||
- (BOOL)isEnabled {
|
||||
return FALSE;
|
||||
}
|
||||
#endif
|
||||
|
||||
@end
|
||||
|
||||
@@ -37,8 +37,9 @@
|
||||
UIFont *cellFont = UIFont.flex_defaultTableCellFont;
|
||||
self.titleLabel.font = cellFont;
|
||||
self.subtitleLabel.font = cellFont;
|
||||
#if !TARGET_OS_TV
|
||||
self.subtitleLabel.textColor = FLEXColor.deemphasizedTextColor;
|
||||
|
||||
#endif
|
||||
self.titleLabel.lineBreakMode = NSLineBreakByTruncatingMiddle;
|
||||
self.subtitleLabel.lineBreakMode = NSLineBreakByTruncatingMiddle;
|
||||
|
||||
|
||||
@@ -35,6 +35,11 @@ extern FLEXTableViewCellReuseIdentifier const kFLEXCodeFontCell;
|
||||
+ (instancetype)plainTableView;
|
||||
+ (instancetype)style:(UITableViewStyle)style;
|
||||
|
||||
#if TARGET_OS_TV
|
||||
/// tvOS tries to keep your selected index remembered when pushing and popping between views in a navigation controller, it doesn't do a very good job, this is to keep track of it ourselves.
|
||||
@property (nonatomic, strong) NSIndexPath *selectedIndexPath;
|
||||
#endif
|
||||
|
||||
/// You do not need to register classes for any of the default reuse identifiers above
|
||||
/// (annotated as \c FLEXTableViewCellReuseIdentifier types) unless you wish to provide
|
||||
/// a custom cell for any of those reuse identifiers. By default, \c FLEXTableViewCell,
|
||||
|
||||
@@ -30,21 +30,37 @@ FLEXTableViewCellReuseIdentifier const kFLEXCodeFontCell = @"kFLEXCodeFontCell";
|
||||
@implementation FLEXTableView
|
||||
|
||||
+ (instancetype)flexDefaultTableView {
|
||||
#if FLEX_AT_LEAST_IOS13_SDK
|
||||
if (@available(iOS 13.0, *)) {
|
||||
#if !TARGET_OS_TV
|
||||
return [[self alloc] initWithFrame:CGRectZero style:UITableViewStyleInsetGrouped];
|
||||
#else
|
||||
return [[self alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];
|
||||
#endif
|
||||
} 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, *)) {
|
||||
#if !TARGET_OS_TV
|
||||
return [[self alloc] initWithFrame:CGRectZero style:UITableViewStyleInsetGrouped];
|
||||
#else
|
||||
return [[self alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];
|
||||
#endif
|
||||
} else {
|
||||
return [[self alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];
|
||||
}
|
||||
#else
|
||||
return [[self alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];
|
||||
#endif
|
||||
}
|
||||
|
||||
+ (id)plainTableView {
|
||||
|
||||
@@ -9,12 +9,18 @@
|
||||
#import "FLEXArgumentInputColorView.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
|
||||
#if TARGET_OS_TV
|
||||
#import "FLEXTV.h"
|
||||
#endif
|
||||
@protocol FLEXColorComponentInputViewDelegate;
|
||||
|
||||
@interface FLEXColorComponentInputView : UIView
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
@property (nonatomic) UISlider *slider;
|
||||
#else
|
||||
@property (nonatomic) KBSlider *slider;
|
||||
#endif
|
||||
@property (nonatomic) UILabel *valueLabel;
|
||||
|
||||
@property (nonatomic, weak) id <FLEXColorComponentInputViewDelegate> delegate;
|
||||
@@ -33,7 +39,11 @@
|
||||
- (id)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
#if !TARGET_OS_TV
|
||||
self.slider = [UISlider new];
|
||||
#else
|
||||
self.slider = [[KBSlider alloc] initWithFrame:CGRectMake(0, 0, 1000, 53)];
|
||||
#endif
|
||||
[self.slider addTarget:self action:@selector(sliderChanged:) forControlEvents:UIControlEventValueChanged];
|
||||
[self addSubview:self.slider];
|
||||
|
||||
@@ -58,13 +68,17 @@
|
||||
[super layoutSubviews];
|
||||
|
||||
const CGFloat kValueLabelWidth = 50.0;
|
||||
|
||||
UIEdgeInsets sliderInset = UIEdgeInsetsZero;
|
||||
#if TARGET_OS_TV
|
||||
sliderInset.left = 40;
|
||||
sliderInset.right = 40;
|
||||
#endif
|
||||
[self.slider sizeToFit];
|
||||
CGFloat sliderWidth = self.bounds.size.width - kValueLabelWidth;
|
||||
self.slider.frame = CGRectMake(0, 0, sliderWidth, self.slider.frame.size.height);
|
||||
CGFloat sliderWidth = self.bounds.size.width - kValueLabelWidth - sliderInset.left - sliderInset.right;
|
||||
self.slider.frame = CGRectMake(sliderInset.left, 0, sliderWidth, self.slider.frame.size.height);
|
||||
|
||||
[self.valueLabel sizeToFit];
|
||||
CGFloat valueLabelOriginX = CGRectGetMaxX(self.slider.frame);
|
||||
CGFloat valueLabelOriginX = CGRectGetMaxX(self.slider.frame) + sliderInset.right/2.0;
|
||||
CGFloat valueLabelOriginY = FLEXFloor((self.slider.frame.size.height - self.valueLabel.frame.size.height) / 2.0);
|
||||
self.valueLabel.frame = CGRectMake(valueLabelOriginX, valueLabelOriginY, kValueLabelWidth, self.valueLabel.frame.size.height);
|
||||
}
|
||||
|
||||
@@ -8,11 +8,17 @@
|
||||
|
||||
#import "FLEXArgumentInputDateView.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
|
||||
#import <TargetConditionals.h>
|
||||
#if TARGET_OS_TV
|
||||
#import "FLEXTV.h"
|
||||
#endif
|
||||
@interface FLEXArgumentInputDateView ()
|
||||
|
||||
#if TARGET_OS_TV
|
||||
@property (nonatomic) KBDatePickerView *datePicker;
|
||||
#else
|
||||
@property (nonatomic) UIDatePicker *datePicker;
|
||||
|
||||
#endif
|
||||
@end
|
||||
|
||||
@implementation FLEXArgumentInputDateView
|
||||
@@ -20,11 +26,18 @@
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
|
||||
self = [super initWithArgumentTypeEncoding:typeEncoding];
|
||||
if (self) {
|
||||
#if !TARGET_OS_TV
|
||||
self.datePicker = [UIDatePicker new];
|
||||
self.datePicker.datePickerMode = UIDatePickerModeDateAndTime;
|
||||
|
||||
// Using UTC, because that's what the NSDate description prints
|
||||
self.datePicker.calendar = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierGregorian];
|
||||
self.datePicker.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"];
|
||||
#else
|
||||
self.datePicker = [[KBDatePickerView alloc] init];
|
||||
self.datePicker.showDateLabel = true;
|
||||
#endif
|
||||
|
||||
[self addSubview:self.datePicker];
|
||||
}
|
||||
return self;
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#import "FLEXArgumentInputViewFactory.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
#import "FLEXArgumentInputFontsPickerView.h"
|
||||
#import <TargetConditionals.h>
|
||||
|
||||
@interface FLEXArgumentInputFontView ()
|
||||
|
||||
@@ -74,11 +75,15 @@
|
||||
CGFloat runningOriginY = self.topInputFieldVerticalLayoutGuide;
|
||||
|
||||
CGSize fontNameFitSize = [self.fontNameInput sizeThatFits:self.bounds.size];
|
||||
self.fontNameInput.frame = CGRectMake(0, runningOriginY, fontNameFitSize.width, fontNameFitSize.height);
|
||||
CGFloat padding = 0;
|
||||
#if TARGET_OS_TV
|
||||
padding = 40;
|
||||
#endif
|
||||
self.fontNameInput.frame = CGRectMake(0, runningOriginY, fontNameFitSize.width, fontNameFitSize.height + padding);
|
||||
runningOriginY = CGRectGetMaxY(self.fontNameInput.frame) + [[self class] verticalPaddingBetweenFields];
|
||||
|
||||
CGSize pointSizeFitSize = [self.pointSizeInput sizeThatFits:self.bounds.size];
|
||||
self.pointSizeInput.frame = CGRectMake(0, runningOriginY, pointSizeFitSize.width, pointSizeFitSize.height);
|
||||
self.pointSizeInput.frame = CGRectMake(0, runningOriginY, pointSizeFitSize.width, pointSizeFitSize.height + padding);
|
||||
}
|
||||
|
||||
+ (CGFloat)verticalPaddingBetweenFields {
|
||||
|
||||
@@ -8,5 +8,9 @@
|
||||
|
||||
#import "FLEXArgumentInputTextView.h"
|
||||
|
||||
#if TARGET_OS_TV
|
||||
@interface FLEXArgumentInputFontsPickerView : FLEXArgumentInputTextView
|
||||
#else
|
||||
@interface FLEXArgumentInputFontsPickerView : FLEXArgumentInputTextView <UIPickerViewDataSource, UIPickerViewDelegate>
|
||||
#endif
|
||||
@end
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
|
||||
#import "FLEXArgumentInputFontsPickerView.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
|
||||
#import "FLEXFontListTableViewController.h"
|
||||
#import "NSObject+FLEX_Reflection.h"
|
||||
@interface FLEXArgumentInputFontsPickerView ()
|
||||
|
||||
@property (nonatomic) NSMutableArray<NSString *> *availableFonts;
|
||||
@@ -23,7 +24,17 @@
|
||||
if (self) {
|
||||
self.targetSize = FLEXArgumentInputViewSizeSmall;
|
||||
[self createAvailableFonts];
|
||||
#if TARGET_OS_TV
|
||||
FLEXFontListTableViewController *fontListController = [FLEXFontListTableViewController new];
|
||||
fontListController.itemSelectedBlock = ^(NSString *fontName) {
|
||||
[self.inputTextView setText:fontName];
|
||||
[[self topViewController] dismissViewControllerAnimated:true completion:nil];
|
||||
};
|
||||
self.inputTextView.inputViewController = fontListController;
|
||||
|
||||
#else
|
||||
self.inputTextView.inputView = [self createFontsPicker];
|
||||
#endif
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -33,7 +44,9 @@
|
||||
if ([self.availableFonts indexOfObject:inputValue] == NSNotFound) {
|
||||
[self.availableFonts insertObject:inputValue atIndex:0];
|
||||
}
|
||||
#if !TARGET_OS_TV
|
||||
[(UIPickerView *)self.inputTextView.inputView selectRow:[self.availableFonts indexOfObject:inputValue] inComponent:0 animated:NO];
|
||||
#endif
|
||||
}
|
||||
|
||||
- (id)inputValue {
|
||||
@@ -42,20 +55,6 @@
|
||||
|
||||
#pragma mark - private
|
||||
|
||||
- (UIPickerView*)createFontsPicker {
|
||||
UIPickerView *fontsPicker = [UIPickerView new];
|
||||
fontsPicker.dataSource = self;
|
||||
fontsPicker.delegate = self;
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
// Deprecated in iOS 13; from then on, selection is always shown
|
||||
fontsPicker.showsSelectionIndicator = YES;
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
return fontsPicker;
|
||||
}
|
||||
|
||||
- (void)createAvailableFonts {
|
||||
NSMutableArray<NSString *> *unsortedFontsArray = [NSMutableArray new];
|
||||
for (NSString *eachFontFamily in UIFont.familyNames) {
|
||||
@@ -66,6 +65,16 @@
|
||||
self.availableFonts = [NSMutableArray arrayWithArray:[unsortedFontsArray sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]];
|
||||
}
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
|
||||
- (UIPickerView*)createFontsPicker {
|
||||
UIPickerView *fontsPicker = [UIPickerView new];
|
||||
fontsPicker.dataSource = self;
|
||||
fontsPicker.delegate = self;
|
||||
fontsPicker.showsSelectionIndicator = YES;
|
||||
return fontsPicker;
|
||||
}
|
||||
|
||||
#pragma mark - UIPickerViewDataSource
|
||||
|
||||
- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView {
|
||||
@@ -99,4 +108,6 @@
|
||||
self.inputTextView.text = self.availableFonts[row];
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@end
|
||||
|
||||
@@ -10,7 +10,4 @@
|
||||
|
||||
@interface FLEXArgumentInputStructView : FLEXArgumentInputView
|
||||
|
||||
/// Enable displaying ivar names for custom struct types
|
||||
+ (void)registerFieldNames:(NSArray<NSString *> *)names forTypeEncoding:(NSString *)typeEncoding;
|
||||
|
||||
@end
|
||||
|
||||
@@ -19,41 +19,6 @@
|
||||
|
||||
@implementation FLEXArgumentInputStructView
|
||||
|
||||
static NSMutableDictionary<NSString *, NSArray<NSString *> *> *structFieldNameRegistrar = nil;
|
||||
+ (void)initialize {
|
||||
if (self == [FLEXArgumentInputStructView class]) {
|
||||
structFieldNameRegistrar = [NSMutableDictionary new];
|
||||
[self registerDefaultFieldNames];
|
||||
}
|
||||
}
|
||||
|
||||
+ (void)registerDefaultFieldNames {
|
||||
NSDictionary *defaults = @{
|
||||
@(@encode(CGRect)): @[@"CGPoint origin", @"CGSize size"],
|
||||
@(@encode(CGPoint)): @[@"CGFloat x", @"CGFloat y"],
|
||||
@(@encode(CGSize)): @[@"CGFloat width", @"CGFloat height"],
|
||||
@(@encode(CGVector)): @[@"CGFloat dx", @"CGFloat dy"],
|
||||
@(@encode(UIEdgeInsets)): @[@"CGFloat top", @"CGFloat left", @"CGFloat bottom", @"CGFloat right"],
|
||||
@(@encode(UIOffset)): @[@"CGFloat horizontal", @"CGFloat vertical"],
|
||||
@(@encode(NSRange)): @[@"NSUInteger location", @"NSUInteger length"],
|
||||
@(@encode(CATransform3D)): @[@"CGFloat m11", @"CGFloat m12", @"CGFloat m13", @"CGFloat m14",
|
||||
@"CGFloat m21", @"CGFloat m22", @"CGFloat m23", @"CGFloat m24",
|
||||
@"CGFloat m31", @"CGFloat m32", @"CGFloat m33", @"CGFloat m34",
|
||||
@"CGFloat m41", @"CGFloat m42", @"CGFloat m43", @"CGFloat m44"],
|
||||
@(@encode(CGAffineTransform)): @[@"CGFloat a", @"CGFloat b",
|
||||
@"CGFloat c", @"CGFloat d",
|
||||
@"CGFloat tx", @"CGFloat ty"],
|
||||
};
|
||||
|
||||
[structFieldNameRegistrar addEntriesFromDictionary:defaults];
|
||||
|
||||
if (@available(iOS 11.0, *)) {
|
||||
structFieldNameRegistrar[@(@encode(NSDirectionalEdgeInsets))] = @[
|
||||
@"CGFloat top", @"CGFloat leading", @"CGFloat bottom", @"CGFloat trailing"
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
|
||||
self = [super initWithArgumentTypeEncoding:typeEncoding];
|
||||
if (self) {
|
||||
@@ -216,13 +181,40 @@ static NSMutableDictionary<NSString *, NSArray<NSString *> *> *structFieldNameRe
|
||||
return NO;
|
||||
}
|
||||
|
||||
+ (void)registerFieldNames:(NSArray<NSString *> *)names forTypeEncoding:(NSString *)typeEncoding {
|
||||
NSParameterAssert(typeEncoding); NSParameterAssert(names);
|
||||
structFieldNameRegistrar[typeEncoding] = names;
|
||||
}
|
||||
|
||||
+ (NSArray<NSString *> *)customFieldTitlesForTypeEncoding:(const char *)typeEncoding {
|
||||
return structFieldNameRegistrar[@(typeEncoding)];
|
||||
NSArray<NSString *> *customTitles = nil;
|
||||
if (strcmp(typeEncoding, @encode(CGRect)) == 0) {
|
||||
customTitles = @[@"CGPoint origin", @"CGSize size"];
|
||||
} else if (strcmp(typeEncoding, @encode(CGPoint)) == 0) {
|
||||
customTitles = @[@"CGFloat x", @"CGFloat y"];
|
||||
} else if (strcmp(typeEncoding, @encode(CGSize)) == 0) {
|
||||
customTitles = @[@"CGFloat width", @"CGFloat height"];
|
||||
} else if (strcmp(typeEncoding, @encode(CGVector)) == 0) {
|
||||
customTitles = @[@"CGFloat dx", @"CGFloat dy"];
|
||||
} else if (strcmp(typeEncoding, @encode(UIEdgeInsets)) == 0) {
|
||||
customTitles = @[@"CGFloat top", @"CGFloat left", @"CGFloat bottom", @"CGFloat right"];
|
||||
} else if (strcmp(typeEncoding, @encode(UIOffset)) == 0) {
|
||||
customTitles = @[@"CGFloat horizontal", @"CGFloat vertical"];
|
||||
} else if (strcmp(typeEncoding, @encode(NSRange)) == 0) {
|
||||
customTitles = @[@"NSUInteger location", @"NSUInteger length"];
|
||||
} else if (strcmp(typeEncoding, @encode(CATransform3D)) == 0) {
|
||||
customTitles = @[@"CGFloat m11", @"CGFloat m12", @"CGFloat m13", @"CGFloat m14",
|
||||
@"CGFloat m21", @"CGFloat m22", @"CGFloat m23", @"CGFloat m24",
|
||||
@"CGFloat m31", @"CGFloat m32", @"CGFloat m33", @"CGFloat m34",
|
||||
@"CGFloat m41", @"CGFloat m42", @"CGFloat m43", @"CGFloat m44"];
|
||||
} else if (strcmp(typeEncoding, @encode(CGAffineTransform)) == 0) {
|
||||
customTitles = @[@"CGFloat a", @"CGFloat b",
|
||||
@"CGFloat c", @"CGFloat d",
|
||||
@"CGFloat tx", @"CGFloat ty"];
|
||||
} else {
|
||||
if (@available(iOS 11.0, *)) {
|
||||
if (strcmp(typeEncoding, @encode(NSDirectionalEdgeInsets)) == 0) {
|
||||
customTitles = @[@"CGFloat top", @"CGFloat leading",
|
||||
@"CGFloat bottom", @"CGFloat trailing"];
|
||||
}
|
||||
}
|
||||
}
|
||||
return customTitles;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -7,11 +7,13 @@
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputSwitchView.h"
|
||||
|
||||
#import "FLEXTV.h"
|
||||
@interface FLEXArgumentInputSwitchView ()
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
@property (nonatomic) UISwitch *inputSwitch;
|
||||
|
||||
#else
|
||||
@property (nonatomic) UIFLEXSwitch *inputSwitch;
|
||||
#endif
|
||||
@end
|
||||
|
||||
@implementation FLEXArgumentInputSwitchView
|
||||
@@ -19,14 +21,24 @@
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
|
||||
self = [super initWithArgumentTypeEncoding:typeEncoding];
|
||||
if (self) {
|
||||
#if !TARGET_OS_TV
|
||||
self.inputSwitch = [UISwitch new];
|
||||
[self.inputSwitch addTarget:self action:@selector(switchValueDidChange:) forControlEvents:UIControlEventValueChanged];
|
||||
[self.inputSwitch sizeToFit];
|
||||
[self.inputSwitch addTarget:self action:@selector(switchValueDidChange:) forControlEvents:UIControlEventValueChanged];
|
||||
|
||||
#else
|
||||
self.inputSwitch = [UIFLEXSwitch newSwitch];
|
||||
[self.inputSwitch addTarget:self action:@selector(changeSwitchValue:) forControlEvents:UIControlEventPrimaryActionTriggered];
|
||||
#endif
|
||||
[self addSubview:self.inputSwitch];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)changeSwitchValue:(UIFLEXSwitch *)switchView {
|
||||
[switchView setOn:!switchView.isOn];
|
||||
[self switchValueDidChange:switchView];
|
||||
}
|
||||
|
||||
#pragma mark Input/Output
|
||||
|
||||
@@ -59,13 +71,20 @@
|
||||
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
|
||||
#if TARGET_OS_TV
|
||||
self.inputSwitch.frame = CGRectMake(50, 50, 200, 70);
|
||||
#else
|
||||
self.inputSwitch.frame = CGRectMake(0, self.topInputFieldVerticalLayoutGuide, self.inputSwitch.frame.size.width, self.inputSwitch.frame.size.height);
|
||||
#endif
|
||||
}
|
||||
|
||||
- (CGSize)sizeThatFits:(CGSize)size {
|
||||
CGSize fitSize = [super sizeThatFits:size];
|
||||
#if TARGET_OS_TV
|
||||
fitSize.height += 110;
|
||||
#else
|
||||
fitSize.height += self.inputSwitch.frame.size.height;
|
||||
#endif
|
||||
return fitSize;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,12 +7,18 @@
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputView.h"
|
||||
#if TARGET_OS_TV
|
||||
#import "KBSelectableTextView.h"
|
||||
#endif
|
||||
|
||||
@interface FLEXArgumentInputTextView : FLEXArgumentInputView <UITextViewDelegate>
|
||||
|
||||
// For subclass eyes only
|
||||
|
||||
#if TARGET_OS_TV
|
||||
@property (nonatomic, readonly) KBSelectableTextView *inputTextView;
|
||||
#else
|
||||
@property (nonatomic, readonly) UITextView *inputTextView;
|
||||
#endif
|
||||
@property (nonatomic) NSString *inputPlaceholderText;
|
||||
|
||||
@end
|
||||
|
||||
@@ -12,7 +12,11 @@
|
||||
|
||||
@interface FLEXArgumentInputTextView ()
|
||||
|
||||
#if TARGET_OS_TV
|
||||
@property (nonatomic) KBSelectableTextView *inputTextView;
|
||||
#else
|
||||
@property (nonatomic) UITextView *inputTextView;
|
||||
#endif
|
||||
@property (nonatomic) UILabel *placeholderLabel;
|
||||
@property (nonatomic, readonly) NSUInteger numberOfInputLines;
|
||||
|
||||
@@ -23,7 +27,12 @@
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
|
||||
self = [super initWithArgumentTypeEncoding:typeEncoding];
|
||||
if (self) {
|
||||
#if TARGET_OS_TV
|
||||
self.inputTextView = [[KBSelectableTextView alloc] initWithFrame:CGRectZero];
|
||||
#else
|
||||
self.inputTextView = [UITextView new];
|
||||
self.inputTextView.inputAccessoryView = [self createToolBar];
|
||||
#endif
|
||||
self.inputTextView.font = [[self class] inputFont];
|
||||
self.inputTextView.backgroundColor = FLEXColor.secondaryGroupedBackgroundColor;
|
||||
self.inputTextView.layer.cornerRadius = 10.f;
|
||||
@@ -31,9 +40,7 @@
|
||||
self.inputTextView.autocapitalizationType = UITextAutocapitalizationTypeNone;
|
||||
self.inputTextView.autocorrectionType = UITextAutocorrectionTypeNo;
|
||||
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;
|
||||
@@ -53,7 +60,7 @@
|
||||
}
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
- (UIToolbar *)createToolBar {
|
||||
UIToolbar *toolBar = [UIToolbar new];
|
||||
[toolBar sizeToFit];
|
||||
@@ -72,7 +79,7 @@
|
||||
toolBar.items = @[spaceItem, pasteItem, doneItem];
|
||||
return toolBar;
|
||||
}
|
||||
|
||||
#endif
|
||||
- (void)setInputPlaceholderText:(NSString *)placeholder {
|
||||
self.placeholderLabel.text = placeholder;
|
||||
if (placeholder.length) {
|
||||
@@ -128,7 +135,11 @@
|
||||
}
|
||||
|
||||
- (CGFloat)inputTextViewHeight {
|
||||
return ceil([[self class] inputFont].lineHeight * self.numberOfInputLines) + 16.0;
|
||||
CGFloat padding = 16.0;
|
||||
#if TARGET_OS_TV
|
||||
padding = 40.0;
|
||||
#endif
|
||||
return ceil([[self class] inputFont].lineHeight * self.numberOfInputLines) + padding;
|
||||
}
|
||||
|
||||
- (CGSize)sizeThatFits:(CGSize)size {
|
||||
|
||||
@@ -21,7 +21,4 @@
|
||||
/// Useful when deciding whether to edit or explore a property, ivar, or NSUserDefaults value.
|
||||
+ (BOOL)canEditFieldWithTypeEncoding:(const char *)typeEncoding currentValue:(id)currentValue;
|
||||
|
||||
/// Enable displaying ivar names for custom struct types
|
||||
+ (void)registerFieldNames:(NSArray<NSString *> *)names forTypeEncoding:(NSString *)typeEncoding;
|
||||
|
||||
@end
|
||||
|
||||
@@ -67,9 +67,4 @@
|
||||
return [self argumentInputViewSubclassForTypeEncoding:typeEncoding currentValue:currentValue] != nil;
|
||||
}
|
||||
|
||||
/// Enable displaying ivar names for custom struct types
|
||||
+ (void)registerFieldNames:(NSArray<NSString *> *)names forTypeEncoding:(NSString *)typeEncoding {
|
||||
[FLEXArgumentInputStructView registerFieldNames:names forTypeEncoding:typeEncoding];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -12,7 +12,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FLEXDefaultEditorViewController : FLEXVariableEditorViewController
|
||||
|
||||
+ (instancetype)target:(NSUserDefaults *)defaults key:(NSString *)key commitHandler:(void(^_Nullable)(void))onCommit;
|
||||
+ (instancetype)target:(NSUserDefaults *)defaults key:(NSString *)key commitHandler:(void(^_Nullable)())onCommit;
|
||||
|
||||
+ (BOOL)canEditDefaultWithValue:(nullable id)currentValue;
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
@implementation FLEXDefaultEditorViewController
|
||||
|
||||
+ (instancetype)target:(NSUserDefaults *)defaults key:(NSString *)key commitHandler:(void(^_Nullable)(void))onCommit {
|
||||
+ (instancetype)target:(NSUserDefaults *)defaults key:(NSString *)key commitHandler:(void(^_Nullable)())onCommit {
|
||||
FLEXDefaultEditorViewController *editor = [self target:defaults data:key commitHandler:onCommit];
|
||||
editor.title = @"Edit Default";
|
||||
return editor;
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
#import "FLEXFieldEditorView.h"
|
||||
#import "FLEXArgumentInputView.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXColor.h"
|
||||
|
||||
@interface FLEXFieldEditorView ()
|
||||
|
||||
@@ -123,7 +122,7 @@
|
||||
}
|
||||
|
||||
+ (UIColor *)dividerColor {
|
||||
return FLEXColor.tertiaryBackgroundColor;
|
||||
return UIColor.lightGrayColor;
|
||||
}
|
||||
|
||||
+ (CGFloat)horizontalPadding {
|
||||
|
||||
@@ -15,13 +15,16 @@ 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)(void))onCommit;
|
||||
+ (nullable instancetype)target:(id)target property:(FLEXProperty *)property commitHandler:(void(^_Nullable)())onCommit;
|
||||
/// @return nil if the ivar type is unsupported
|
||||
+ (nullable instancetype)target:(id)target ivar:(FLEXIvar *)ivar commitHandler:(void(^_Nullable)(void))onCommit;
|
||||
|
||||
+ (nullable instancetype)target:(id)target ivar:(FLEXIvar *)ivar commitHandler:(void(^_Nullable)())onCommit;
|
||||
#if TARGET_OS_TV
|
||||
/// Subclasses can change the button title via the \c title property
|
||||
@property (nonatomic, readonly) UIButton *getterButton;
|
||||
#else
|
||||
/// Subclasses can change the button title via the \c title property
|
||||
@property (nonatomic, readonly) UIBarButtonItem *getterButton;
|
||||
|
||||
#endif
|
||||
- (void)getterButtonPressed:(id)sender;
|
||||
|
||||
@end
|
||||
|
||||
@@ -11,14 +11,14 @@
|
||||
#import "FLEXArgumentInputViewFactory.h"
|
||||
#import "FLEXPropertyAttributes.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
#import "FLEXMetadataExtras.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXColor.h"
|
||||
#import "UIBarButtonItem+FLEX.h"
|
||||
#import <TargetConditionals.h>
|
||||
#import "FLEXArgumentInputDateView.h"
|
||||
|
||||
@interface FLEXFieldEditorViewController () <FLEXArgumentInputViewDelegate>
|
||||
|
||||
@property (nonatomic, readonly) id<FLEXMetadataAuxiliaryInfo> auxiliaryInfoProvider;
|
||||
@property (nonatomic) FLEXProperty *property;
|
||||
@property (nonatomic) FLEXIvar *ivar;
|
||||
|
||||
@@ -32,14 +32,19 @@
|
||||
|
||||
#pragma mark - Initialization
|
||||
|
||||
+ (instancetype)target:(id)target property:(nonnull FLEXProperty *)property commitHandler:(void(^)(void))onCommit {
|
||||
+ (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;
|
||||
}
|
||||
|
||||
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(^)(void))onCommit {
|
||||
+ (instancetype)target:(id)target ivar:(nonnull FLEXIvar *)ivar commitHandler:(void(^_Nullable)())onCommit {
|
||||
FLEXFieldEditorViewController *editor = [self target:target data:ivar commitHandler:onCommit];
|
||||
editor.title = [@"Ivar: " stringByAppendingString:ivar.name];
|
||||
editor.ivar = ivar;
|
||||
@@ -52,7 +57,7 @@
|
||||
[super viewDidLoad];
|
||||
|
||||
self.view.backgroundColor = FLEXColor.groupedBackgroundColor;
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
// Create getter button
|
||||
_getterButton = [[UIBarButtonItem alloc]
|
||||
initWithTitle:@"Get"
|
||||
@@ -60,11 +65,23 @@
|
||||
target:self
|
||||
action:@selector(getterButtonPressed:)
|
||||
];
|
||||
|
||||
self.toolbarItems = @[
|
||||
UIBarButtonItem.flex_flexibleSpace, self.getterButton, self.actionButton
|
||||
];
|
||||
|
||||
[self registerAuxiliaryInfo];
|
||||
#else
|
||||
_getterButton = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
[_getterButton setTitle:@"Get" forState:UIControlStateNormal];
|
||||
[_getterButton addTarget:self action:@selector(getterButtonPressed:) forControlEvents:UIControlEventPrimaryActionTriggered];
|
||||
_getterButton.frame = CGRectMake(100, 600, 200, 70);
|
||||
[self.view addSubview:_getterButton];
|
||||
UIFocusGuide *focusGuide = [[UIFocusGuide alloc] init];
|
||||
[self.view addLayoutGuide:focusGuide];
|
||||
[focusGuide.topAnchor constraintEqualToAnchor:self.actionButton.topAnchor].active = true;
|
||||
[focusGuide.bottomAnchor constraintEqualToAnchor:self.getterButton.bottomAnchor].active = true;
|
||||
focusGuide.preferredFocusEnvironments = self.preferredFocusEnvironments;
|
||||
#endif
|
||||
|
||||
// Configure input view
|
||||
self.fieldEditorView.fieldDescription = self.fieldDescription;
|
||||
@@ -72,18 +89,32 @@
|
||||
inputView.inputValue = self.currentValue;
|
||||
inputView.delegate = self;
|
||||
self.fieldEditorView.argumentInputViews = @[inputView];
|
||||
|
||||
// Don't show a "set" button for switches; we mutate when the switch is flipped
|
||||
if ([inputView isKindOfClass:[FLEXArgumentInputSwitchView class]]) {
|
||||
self.actionButton.enabled = NO;
|
||||
#if !TARGET_OS_TV
|
||||
self.actionButton.title = @"Flip the switch to call the setter";
|
||||
// Put getter button before setter button
|
||||
// Put getter button before setter button
|
||||
self.toolbarItems = @[
|
||||
UIBarButtonItem.flex_flexibleSpace, self.actionButton, self.getterButton
|
||||
];
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
- (NSArray *)preferredFocusEnvironments {
|
||||
if ([self actionButton] && _getterButton){
|
||||
return @[[self actionButton],_getterButton];
|
||||
} else {
|
||||
if ([self actionButton]){
|
||||
return @[[self actionButton]];
|
||||
} else if (_getterButton){
|
||||
return @[_getterButton];
|
||||
}
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (void)actionButtonPressed:(id)sender {
|
||||
if (self.property) {
|
||||
id userInputObject = self.firstInputView.inputValue;
|
||||
@@ -124,19 +155,29 @@
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
- (void)registerAuxiliaryInfo {
|
||||
// This is how Reflex will get Swift struct field names into the editor at runtime
|
||||
NSDictionary<NSString *, NSArray *> *labels = [self.auxiliaryInfoProvider
|
||||
auxiliaryInfoForKey:FLEXAuxiliarynfoKeyFieldLabels
|
||||
];
|
||||
- (void)viewWillLayoutSubviews {
|
||||
[super viewWillLayoutSubviews];
|
||||
#if TARGET_OS_TV
|
||||
|
||||
for (NSString *type in labels) {
|
||||
[FLEXArgumentInputViewFactory registerFieldNames:labels[type] forTypeEncoding:type];
|
||||
FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:self.typeEncoding];
|
||||
if ([inputView isKindOfClass:[FLEXArgumentInputDateView class]]){
|
||||
[self actionButton].frame = CGRectMake(100, 350, 200, 60);
|
||||
[self getterButton].frame = CGRectMake(100, 550, 200, 60);
|
||||
return;
|
||||
}
|
||||
|
||||
CGRect getterFrame = _getterButton.frame;
|
||||
CGFloat actionOffset = [[self actionButton] frame].origin.y;
|
||||
//CGRect fieldEditorFrame = self.fieldEditorView.frame;
|
||||
//CGFloat buttonOffset = (fieldEditorFrame.origin.y + fieldEditorFrame.size.height) + (130 + 67);
|
||||
getterFrame.origin.y = actionOffset;
|
||||
_getterButton.frame = getterFrame;
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
- (id)currentValue {
|
||||
if (self.property) {
|
||||
return [self.property getValue:self.target];
|
||||
@@ -145,10 +186,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (id<FLEXMetadataAuxiliaryInfo>)auxiliaryInfoProvider {
|
||||
return self.ivar ?: self.property;
|
||||
}
|
||||
|
||||
- (const FLEXTypeEncoding *)typeEncoding {
|
||||
if (self.property) {
|
||||
return self.property.attributes.typeEncoding.UTF8String;
|
||||
@@ -165,4 +202,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
+ (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
|
||||
|
||||
@@ -39,9 +39,11 @@
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
self.actionButton.title = @"Call";
|
||||
|
||||
#else
|
||||
[self.actionButton setTitle:@"Call" forState:UIControlStateNormal];
|
||||
#endif
|
||||
// Configure field editor view
|
||||
self.fieldEditorView.argumentInputViews = [self argumentInputViews];
|
||||
self.fieldEditorView.fieldDescription = [NSString stringWithFormat:
|
||||
|
||||
@@ -21,17 +21,17 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
@protected
|
||||
id _target;
|
||||
_Nullable id _data;
|
||||
void (^_Nullable _commitHandler)(void);
|
||||
void (^_Nullable _commitHandler)();
|
||||
}
|
||||
|
||||
/// @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)(void))onCommit;
|
||||
+ (instancetype)target:(id)target data:(nullable id)data commitHandler:(void(^_Nullable)())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)(void))onCommit;
|
||||
- (id)initWithTarget:(id)target data:(nullable id)data commitHandler:(void(^_Nullable)())onCommit;
|
||||
|
||||
@property (nonatomic, readonly) id target;
|
||||
|
||||
@@ -39,8 +39,13 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
@property (nonatomic, readonly, nullable) FLEXArgumentInputView *firstInputView;
|
||||
|
||||
@property (nonatomic, readonly) FLEXFieldEditorView *fieldEditorView;
|
||||
#if TARGET_OS_TV
|
||||
/// Subclasses can change the button title via the button's \c title property
|
||||
@property (nonatomic, readonly) UIButton *actionButton;
|
||||
#else
|
||||
/// Subclasses can change the button title via the button's \c title property
|
||||
@property (nonatomic, readonly) UIBarButtonItem *actionButton;
|
||||
#endif
|
||||
|
||||
/// Subclasses should override to provide "set" functionality.
|
||||
/// The commit handler--if present--is called here.
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#import "FLEXArgumentInputViewFactory.h"
|
||||
#import "FLEXObjectExplorerViewController.h"
|
||||
#import "UIBarButtonItem+FLEX.h"
|
||||
#import "FLEXArgumentInputDateView.h"
|
||||
|
||||
@interface FLEXVariableEditorViewController () <UIScrollViewDelegate>
|
||||
@property (nonatomic) UIScrollView *scrollView;
|
||||
@@ -25,24 +26,26 @@
|
||||
|
||||
#pragma mark - Initialization
|
||||
|
||||
+ (instancetype)target:(id)target data:(nullable id)data commitHandler:(void(^_Nullable)(void))onCommit {
|
||||
+ (instancetype)target:(id)target data:(nullable id)data commitHandler:(void(^_Nullable)())onCommit {
|
||||
return [[self alloc] initWithTarget:target data:data commitHandler:onCommit];
|
||||
}
|
||||
|
||||
- (id)initWithTarget:(id)target data:(nullable id)data commitHandler:(void(^_Nullable)(void))onCommit {
|
||||
- (id)initWithTarget:(id)target data:(nullable id)data commitHandler:(void(^_Nullable)())onCommit {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_target = target;
|
||||
_data = data;
|
||||
_commitHandler = onCommit;
|
||||
[NSNotificationCenter.defaultCenter
|
||||
#if !TARGET_OS_TV
|
||||
[NSNotificationCenter.defaultCenter
|
||||
addObserver:self selector:@selector(keyboardDidShow:)
|
||||
name:UIKeyboardWillShowNotification object:nil
|
||||
name:UIKeyboardDidShowNotification object:nil
|
||||
];
|
||||
[NSNotificationCenter.defaultCenter
|
||||
addObserver:self selector:@selector(keyboardWillHide:)
|
||||
name:UIKeyboardWillHideNotification object:nil
|
||||
];
|
||||
#endif
|
||||
}
|
||||
|
||||
return self;
|
||||
@@ -55,6 +58,7 @@
|
||||
#pragma mark - UIViewController methods
|
||||
|
||||
- (void)keyboardDidShow:(NSNotification *)notification {
|
||||
#if !TARGET_OS_TV
|
||||
CGRect keyboardRectInWindow = [[[notification userInfo] objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
|
||||
CGSize keyboardSize = [self.view convertRect:keyboardRectInWindow fromView:nil].size;
|
||||
UIEdgeInsets scrollInsets = self.scrollView.contentInset;
|
||||
@@ -70,9 +74,11 @@
|
||||
break;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)keyboardWillHide:(NSNotification *)notification {
|
||||
|
||||
UIEdgeInsets scrollInsets = self.scrollView.contentInset;
|
||||
scrollInsets.bottom = 0.0;
|
||||
self.scrollView.contentInset = scrollInsets;
|
||||
@@ -93,7 +99,7 @@
|
||||
_fieldEditorView = [FLEXFieldEditorView new];
|
||||
self.fieldEditorView.targetDescription = [NSString stringWithFormat:@"%@ %p", [self.target class], self.target];
|
||||
[self.scrollView addSubview:self.fieldEditorView];
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
_actionButton = [[UIBarButtonItem alloc]
|
||||
initWithTitle:@"Set"
|
||||
style:UIBarButtonItemStyleDone
|
||||
@@ -103,12 +109,30 @@
|
||||
|
||||
self.navigationController.toolbarHidden = NO;
|
||||
self.toolbarItems = @[UIBarButtonItem.flex_flexibleSpace, self.actionButton];
|
||||
#else
|
||||
_actionButton = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
[_actionButton setTitle:@"Set" forState:UIControlStateNormal];
|
||||
[_actionButton addTarget:self action:@selector(actionButtonPressed:) forControlEvents:UIControlEventPrimaryActionTriggered];
|
||||
_actionButton.frame = CGRectMake(500, 600, 200, 70);
|
||||
[self.view addSubview:_actionButton];
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)viewWillLayoutSubviews {
|
||||
CGSize constrainSize = CGSizeMake(self.scrollView.bounds.size.width, CGFLOAT_MAX);
|
||||
CGSize fieldEditorSize = [self.fieldEditorView sizeThatFits:constrainSize];
|
||||
self.fieldEditorView.frame = CGRectMake(0, 0, fieldEditorSize.width, fieldEditorSize.height);
|
||||
#if TARGET_OS_TV
|
||||
|
||||
CGRect actionFrame = _actionButton.frame;
|
||||
CGRect fieldEditorFrame = self.fieldEditorView.frame;
|
||||
CGFloat buttonOffset = (fieldEditorFrame.origin.y + fieldEditorFrame.size.height) + (130 + 67);
|
||||
actionFrame.origin.y = buttonOffset;
|
||||
_actionButton.frame = actionFrame;
|
||||
|
||||
|
||||
|
||||
#endif
|
||||
self.scrollView.contentSize = fieldEditorSize;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
#import "FLEXTableView.h"
|
||||
#import "NSObject+FLEX_Reflection.h"
|
||||
|
||||
@interface FLEXBookmarksViewController ()
|
||||
@property (nonatomic, copy) NSArray *bookmarks;
|
||||
@@ -32,8 +33,9 @@
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
self.navigationController.hidesBarsOnSwipe = NO;
|
||||
#endif
|
||||
self.tableView.allowsMultipleSelectionDuringEditing = YES;
|
||||
|
||||
[self reloadData];
|
||||
@@ -42,6 +44,13 @@
|
||||
- (void)viewWillAppear:(BOOL)animated {
|
||||
[super viewWillAppear:animated];
|
||||
[self setupDefaultBarItems];
|
||||
#if TARGET_OS_TV
|
||||
if ([self darkMode]){
|
||||
self.view.backgroundColor = [UIColor colorWithWhite:0.0 alpha:0.8];
|
||||
} else {
|
||||
self.view.backgroundColor = [UIColor colorWithWhite:1.0 alpha:0.8];
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -56,6 +65,7 @@
|
||||
|
||||
- (void)setupDefaultBarItems {
|
||||
self.navigationItem.rightBarButtonItem = FLEXBarButtonItemSystem(Done, self, @selector(dismissAnimated));
|
||||
#if !TARGET_OS_TV
|
||||
self.toolbarItems = @[
|
||||
UIBarButtonItem.flex_flexibleSpace,
|
||||
FLEXBarButtonItemSystem(Edit, self, @selector(toggleEditing)),
|
||||
@@ -63,18 +73,20 @@
|
||||
|
||||
// Disable editing if no bookmarks available
|
||||
self.toolbarItems.lastObject.enabled = self.bookmarks.count > 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)setupEditingBarItems {
|
||||
self.navigationItem.rightBarButtonItem = nil;
|
||||
#if !TARGET_OS_TV
|
||||
self.toolbarItems = @[
|
||||
[UIBarButtonItem flex_itemWithTitle:@"Close All" target:self action:@selector(closeAllButtonPressed:)],
|
||||
UIBarButtonItem.flex_flexibleSpace,
|
||||
// We use a non-system done item because we change its title dynamically
|
||||
[UIBarButtonItem flex_doneStyleitemWithTitle:@"Done" target:self action:@selector(toggleEditing)]
|
||||
];
|
||||
|
||||
self.toolbarItems.firstObject.tintColor = FLEXColor.destructiveColor;
|
||||
#endif
|
||||
}
|
||||
|
||||
- (FLEXExplorerViewController *)corePresenter {
|
||||
@@ -198,8 +210,10 @@
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
if (self.editing) {
|
||||
// Case: editing with multi-select
|
||||
#if !TARGET_OS_TV
|
||||
self.toolbarItems.lastObject.title = @"Remove Selected";
|
||||
self.toolbarItems.lastObject.tintColor = FLEXColor.destructiveColor;
|
||||
#endif
|
||||
} else {
|
||||
// Case: selected a bookmark
|
||||
[self dismissAnimated:self.bookmarks[indexPath.row]];
|
||||
@@ -210,8 +224,10 @@
|
||||
NSParameterAssert(self.editing);
|
||||
|
||||
if (tableView.indexPathsForSelectedRows.count == 0) {
|
||||
#if !TARGET_OS_TV
|
||||
self.toolbarItems.lastObject.title = @"Done";
|
||||
self.toolbarItems.lastObject.tintColor = self.view.tintColor;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,21 +21,11 @@
|
||||
|
||||
- (BOOL)shouldReceiveTouchAtWindowPoint:(CGPoint)pointInWindowCoordinates;
|
||||
|
||||
/// @brief Used to present (or dismiss) a modal view controller ("tool"),
|
||||
/// typically triggered by pressing a button in the toolbar.
|
||||
/// @brief Used to present (or dismiss) a modal view controller ("tool"), typically triggered by pressing a button in the toolbar.
|
||||
///
|
||||
/// If a tool is already presented, this method simply dismisses it and calls the completion block.
|
||||
/// If no tool is presented, @code future() @endcode is presented and the completion block is called.
|
||||
- (void)toggleToolWithViewControllerProvider:(UINavigationController *(^)(void))future
|
||||
completion:(void (^)(void))completion;
|
||||
|
||||
/// @brief Used to present (or dismiss) a modal view controller ("tool"),
|
||||
/// typically triggered by pressing a button in the toolbar.
|
||||
///
|
||||
/// If a tool is already presented, this method dismisses it and presents the given tool.
|
||||
/// The completion block is called once the tool has been presented.
|
||||
- (void)presentTool:(UINavigationController *(^)(void))future
|
||||
completion:(void (^)(void))completion;
|
||||
- (void)toggleToolWithViewControllerProvider:(UINavigationController *(^)(void))future completion:(void(^)(void))completion;
|
||||
|
||||
// Keyboard shortcut helpers
|
||||
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
#import "FLEXWindowManagerController.h"
|
||||
#import "FLEXViewControllersViewController.h"
|
||||
#import "NSUserDefaults+FLEX.h"
|
||||
#import "FLEXManager.h"
|
||||
#import "FLEXResources.h"
|
||||
|
||||
typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
FLEXExplorerModeDefault,
|
||||
@@ -28,7 +30,9 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
FLEXExplorerModeMove
|
||||
};
|
||||
|
||||
@interface FLEXExplorerViewController () <FLEXHierarchyDelegate, UIAdaptivePresentationControllerDelegate>
|
||||
@interface FLEXExplorerViewController () <FLEXHierarchyDelegate, UIAdaptivePresentationControllerDelegate>{
|
||||
UIImageView *cursorView;
|
||||
}
|
||||
|
||||
/// Tracks the currently active tool/mode
|
||||
@property (nonatomic) FLEXExplorerMode currentMode;
|
||||
@@ -61,22 +65,109 @@ 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;
|
||||
|
||||
/// All views that we're KVOing. Used to help us clean up properly.
|
||||
@property (nonatomic) NSMutableSet<UIView *> *observedViews;
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
/// Used to actuate changes in view selection on iOS 10+
|
||||
@property (nonatomic, readonly) UISelectionFeedbackGenerator *selectionFBG API_AVAILABLE(ios(10.0));
|
||||
|
||||
/// Used to preserve the target app's UIMenuController items.
|
||||
@property (nonatomic) NSArray<UIMenuItem *> *appMenuItems;
|
||||
#endif
|
||||
|
||||
@property CGPoint lastTouchLocation;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXExplorerViewController
|
||||
|
||||
#pragma mark - Cursor Input
|
||||
|
||||
#if TARGET_OS_TV
|
||||
|
||||
- (void)pressesBegan:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event
|
||||
{
|
||||
for (UIPress *press in presses) {
|
||||
if (press.type == UIPressTypeMenu) {
|
||||
|
||||
} else {
|
||||
[super pressesBegan:presses withEvent:event];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-(void)pressesEnded:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event {
|
||||
|
||||
FXLog(@"presses.anyObject.type: %lu", presses.anyObject.type);
|
||||
if (self.currentMode != FLEXExplorerModeSelect && self.currentMode != FLEXExplorerModeMove){
|
||||
[super pressesEnded:presses withEvent:event];
|
||||
return;
|
||||
}
|
||||
CGPoint point = [self.view convertPoint:cursorView.frame.origin toView:nil];
|
||||
FXLog(@"clicked point: %@", NSStringFromCGPoint(point));
|
||||
if (self.currentMode == FLEXExplorerModeSelect){
|
||||
[self updateOutlineViewsForSelectionPoint:point];
|
||||
}
|
||||
if (presses.anyObject.type == UIPressTypeMenu) {
|
||||
if (self.currentMode == FLEXExplorerModeMove){
|
||||
self.currentMode = FLEXExplorerModeSelect;
|
||||
cursorView.hidden = false;
|
||||
} else if (self.currentMode == FLEXExplorerModeSelect){
|
||||
self.currentMode = FLEXExplorerModeDefault;
|
||||
cursorView.hidden = true;
|
||||
[self enableToolbar];
|
||||
}
|
||||
} else if (presses.anyObject.type == UIPressTypeUpArrow) {
|
||||
} else if (presses.anyObject.type == UIPressTypeDownArrow) {
|
||||
} else if (presses.anyObject.type == UIPressTypeSelect) {
|
||||
} else if (presses.anyObject.type == UIPressTypePlayPause){
|
||||
}
|
||||
}
|
||||
|
||||
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
|
||||
{
|
||||
self.lastTouchLocation = CGPointMake(-1, -1);
|
||||
}
|
||||
|
||||
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
|
||||
{
|
||||
for (UITouch *touch in touches)
|
||||
{
|
||||
CGPoint location = [touch locationInView:self.view];
|
||||
|
||||
if(self.lastTouchLocation.x == -1 && self.lastTouchLocation.y == -1)
|
||||
{
|
||||
// Prevent cursor from recentering
|
||||
self.lastTouchLocation = location;
|
||||
}
|
||||
else
|
||||
{
|
||||
CGFloat xDiff = location.x - self.lastTouchLocation.x;
|
||||
CGFloat yDiff = location.y - self.lastTouchLocation.y;
|
||||
CGRect rect = cursorView.frame;
|
||||
|
||||
if(rect.origin.x + xDiff >= 0 && rect.origin.x + xDiff <= 1920)
|
||||
rect.origin.x += xDiff;//location.x - self.startPos.x;//+= xDiff; //location.x;
|
||||
|
||||
if(rect.origin.y + yDiff >= 0 && rect.origin.y + yDiff <= 1080)
|
||||
rect.origin.y += yDiff;//location.y - self.startPos.y;//+= yDiff; //location.y;
|
||||
|
||||
cursorView.frame = rect;
|
||||
self.lastTouchLocation = location;
|
||||
}
|
||||
|
||||
// We only use one touch, break the loop
|
||||
break;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
|
||||
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
|
||||
if (self) {
|
||||
@@ -93,50 +184,140 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
|
||||
// Toolbar
|
||||
_explorerToolbar = [FLEXExplorerToolbar new];
|
||||
|
||||
|
||||
// Start the toolbar off below any bars that may be at the top of the view.
|
||||
CGFloat toolbarOriginY = NSUserDefaults.standardUserDefaults.flex_toolbarTopMargin;
|
||||
|
||||
|
||||
CGRect safeArea = [self viewSafeArea];
|
||||
CGSize toolbarSize = [self.explorerToolbar sizeThatFits:CGSizeMake(
|
||||
CGRectGetWidth(self.view.bounds), CGRectGetHeight(safeArea)
|
||||
)];
|
||||
CGRectGetWidth(self.view.bounds), CGRectGetHeight(safeArea)
|
||||
)];
|
||||
[self updateToolbarPositionWithUnconstrainedFrame:CGRectMake(
|
||||
CGRectGetMinX(safeArea), toolbarOriginY, toolbarSize.width, toolbarSize.height
|
||||
)];
|
||||
CGRectGetMinX(safeArea), toolbarOriginY, toolbarSize.width, toolbarSize.height
|
||||
)];
|
||||
self.explorerToolbar.autoresizingMask = UIViewAutoresizingFlexibleWidth |
|
||||
UIViewAutoresizingFlexibleBottomMargin |
|
||||
UIViewAutoresizingFlexibleTopMargin;
|
||||
UIViewAutoresizingFlexibleBottomMargin |
|
||||
UIViewAutoresizingFlexibleTopMargin;
|
||||
[self.view addSubview:self.explorerToolbar];
|
||||
[self setupToolbarActions];
|
||||
[self setupToolbarGestures];
|
||||
|
||||
// View selection
|
||||
UITapGestureRecognizer *selectionTapGR = [[UITapGestureRecognizer alloc]
|
||||
initWithTarget:self action:@selector(handleSelectionTap:)
|
||||
];
|
||||
initWithTarget:self action:@selector(handleSelectionTap:)
|
||||
];
|
||||
[self.view addGestureRecognizer:selectionTapGR];
|
||||
|
||||
// View moving
|
||||
self.movePanGR = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleMovePan:)];
|
||||
self.movePanGR.enabled = self.currentMode == FLEXExplorerModeMove;
|
||||
[self.view addGestureRecognizer:self.movePanGR];
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
// Feedback
|
||||
if (@available(iOS 10.0, *)) {
|
||||
_selectionFBG = [UISelectionFeedbackGenerator new];
|
||||
}
|
||||
#else
|
||||
cursorView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, 64, 64)];
|
||||
cursorView.center = CGPointMake(CGRectGetMidX([UIScreen mainScreen].bounds), CGRectGetMidY([UIScreen mainScreen].bounds));
|
||||
cursorView.image = [FLEXResources cursorImage];
|
||||
cursorView.backgroundColor = [UIColor clearColor];
|
||||
cursorView.hidden = YES;
|
||||
|
||||
|
||||
// Observe keyboard to move self out of the way
|
||||
[NSNotificationCenter.defaultCenter
|
||||
addObserver:self
|
||||
selector:@selector(keyboardShown:)
|
||||
name:UIKeyboardWillShowNotification
|
||||
object:nil
|
||||
];
|
||||
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(doubleTap:)];
|
||||
longPress.allowedPressTypes = @[[NSNumber numberWithInteger:UIPressTypePlayPause], [NSNumber numberWithInteger:UIPressTypeSelect]];
|
||||
[self.view addGestureRecognizer:longPress];
|
||||
[self.view addSubview:cursorView];
|
||||
|
||||
UITapGestureRecognizer *doubleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(doubleTap:)];
|
||||
doubleTap.allowedPressTypes = @[[NSNumber numberWithInteger:UIPressTypePlayPause], [NSNumber numberWithInteger:UIPressTypeSelect]];
|
||||
doubleTap.numberOfTapsRequired = 2;
|
||||
[self.view addGestureRecognizer:doubleTap];
|
||||
|
||||
UITapGestureRecognizer *rightTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(doubleTap:)];
|
||||
rightTap.allowedPressTypes = @[[NSNumber numberWithInteger:UIPressTypeRightArrow]];
|
||||
[self.view addGestureRecognizer:rightTap];
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)doubleTap:(UITapGestureRecognizer *)gesture {
|
||||
if (gesture.state == UIGestureRecognizerStateEnded) {
|
||||
if (self.currentMode == FLEXExplorerModeSelect || self.currentMode == FLEXExplorerModeMove){
|
||||
[self showTVOSOptionsAlert];
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
- (void)showObjectControllerForSelectedView {
|
||||
FLEXObjectExplorerViewController *viewExplorer = [FLEXObjectExplorerFactory explorerViewControllerForObject:self.selectedView];
|
||||
if (!viewExplorer) return;
|
||||
if ([self presentedViewController]){
|
||||
FLEXHierarchyViewController *vc = (FLEXHierarchyViewController*)[self presentedViewController];
|
||||
if ([vc respondsToSelector:@selector(pushViewController:animated:)]){
|
||||
[vc pushViewController:viewExplorer animated:true];
|
||||
}
|
||||
} else {
|
||||
[self toggleViewsToolWithCompletion:^{
|
||||
FLEXHierarchyViewController *vc = (FLEXHierarchyViewController*)[self presentedViewController];
|
||||
if ([vc respondsToSelector:@selector(pushViewController:animated:)]){
|
||||
[vc pushViewController:viewExplorer animated:true];
|
||||
}
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)showTVOSOptionsAlert {
|
||||
[FLEXAlert makeAlert:^(FLEXAlert *make) {
|
||||
make.title(@"What would you like to do?");
|
||||
make.button(@"Show Details").handler(^(NSArray<NSString *> *strings) {
|
||||
[self showObjectControllerForSelectedView];
|
||||
[[FLEXManager sharedManager] showExplorer];
|
||||
});
|
||||
if (self.currentMode == FLEXExplorerModeMove){
|
||||
make.button(@"Select View").handler(^(NSArray<NSString *> *strings) {
|
||||
self.currentMode = FLEXExplorerModeSelect;
|
||||
cursorView.hidden = false;
|
||||
[[FLEXManager sharedManager] showExplorer];
|
||||
});
|
||||
} else if (self.currentMode == FLEXExplorerModeSelect){
|
||||
make.button(@"Move View").handler(^(NSArray<NSString *> *strings) {
|
||||
self.currentMode = FLEXExplorerModeMove;
|
||||
cursorView.hidden = true;
|
||||
[[FLEXManager sharedManager] showExplorer];
|
||||
});
|
||||
}
|
||||
make.button(@"Show Views").handler(^(NSArray<NSString *> *strings) {
|
||||
[self toggleViewsTool];
|
||||
[[FLEXManager sharedManager] showExplorer];
|
||||
});
|
||||
make.button(@"Show View Controllers").handler(^(NSArray<NSString *> *strings) {
|
||||
UIViewController *list = [FLEXViewControllersViewController
|
||||
controllersForViews:self.viewsAtTapPoint
|
||||
];
|
||||
[self presentViewController:
|
||||
[FLEXNavigationController withRootViewController:list
|
||||
] animated:YES completion:nil];
|
||||
});
|
||||
make.button(@"Show Usage Hints").handler(^(NSArray<NSString *> *strings) {
|
||||
[[FLEXManager sharedManager] showHintsAlert];
|
||||
});
|
||||
make.button(@"Cancel").cancelStyle().handler(^(NSArray<NSString *> *strings) {
|
||||
[[FLEXManager sharedManager] showExplorer];
|
||||
});
|
||||
} showFrom:self];
|
||||
}
|
||||
|
||||
- (void)longPress:(UILongPressGestureRecognizer*)gesture {
|
||||
if ( gesture.state == UIGestureRecognizerStateEnded) {
|
||||
[self toggleSelectTool];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated {
|
||||
@@ -170,10 +351,7 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
|
||||
UIViewController *viewControllerToAsk = [self viewControllerForRotationAndOrientation];
|
||||
UIInterfaceOrientationMask supportedOrientations = FLEXUtility.infoPlistSupportedInterfaceOrientationsMask;
|
||||
// We check its class by name because using isKindOfClass will fail for the same class defined
|
||||
// twice in the runtime; and the goal here is to avoid calling -supportedInterfaceOrientations
|
||||
// recursively when I'm inspecting FLEX with itself from a tweak dylib
|
||||
if (viewControllerToAsk && ![NSStringFromClass([viewControllerToAsk class]) hasPrefix:@"FLEX"]) {
|
||||
if (viewControllerToAsk && ![viewControllerToAsk isKindOfClass:[self class]]) {
|
||||
supportedOrientations = [viewControllerToAsk supportedInterfaceOrientations];
|
||||
}
|
||||
|
||||
@@ -386,21 +564,6 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
return [self.view convertRect:frameInWindow fromView:nil];
|
||||
}
|
||||
|
||||
- (void)keyboardShown:(NSNotification *)notif {
|
||||
CGRect keyboardFrame = [notif.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
|
||||
CGRect toolbarFrame = self.explorerToolbar.frame;
|
||||
|
||||
if (CGRectGetMinY(keyboardFrame) < CGRectGetMaxY(toolbarFrame)) {
|
||||
toolbarFrame.origin.y = keyboardFrame.origin.y - toolbarFrame.size.height;
|
||||
// Subtract a little more, to ignore accessory input views
|
||||
toolbarFrame.origin.y -= 50;
|
||||
|
||||
[UIView animateWithDuration:0.5 delay:0 usingSpringWithDamping:1 initialSpringVelocity:0.5
|
||||
options:UIViewAnimationOptionCurveEaseOut animations:^{
|
||||
[self updateToolbarPositionWithUnconstrainedFrame:toolbarFrame];
|
||||
} completion:nil];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Toolbar Buttons
|
||||
|
||||
@@ -416,7 +579,11 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
};
|
||||
|
||||
[actionsToItems enumerateKeysAndObjectsUsingBlock:^(NSString *sel, FLEXExplorerToolbarItem *item, BOOL *stop) {
|
||||
[item addTarget:self action:NSSelectorFromString(sel) forControlEvents:UIControlEventTouchUpInside];
|
||||
#if !TARGET_OS_TV
|
||||
[item addTarget:self action:NSSelectorFromString(sel) forControlEvents:UIControlEventTouchUpInside];
|
||||
#else
|
||||
[item addTarget:self action:NSSelectorFromString(sel) forControlEvents:UIControlEventPrimaryActionTriggered];
|
||||
#endif
|
||||
}];
|
||||
}
|
||||
|
||||
@@ -429,12 +596,8 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
}
|
||||
|
||||
- (UIWindow *)statusWindow {
|
||||
if (!@available(iOS 16, *)) {
|
||||
NSString *statusBarString = [NSString stringWithFormat:@"%@arWindow", @"_statusB"];
|
||||
return [UIApplication.sharedApplication valueForKey:statusBarString];
|
||||
}
|
||||
|
||||
return nil;
|
||||
NSString *statusBarString = [NSString stringWithFormat:@"%@arWindow", @"_statusB"];
|
||||
return [UIApplication.sharedApplication valueForKey:statusBarString];
|
||||
}
|
||||
|
||||
- (void)recentButtonTapped:(FLEXExplorerToolbarItem *)sender {
|
||||
@@ -466,11 +629,7 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
toolbar.moveItem.selected = self.currentMode == FLEXExplorerModeMove;
|
||||
|
||||
// Recent only enabled when we have a last active tab
|
||||
if (!self.presentedViewController) {
|
||||
toolbar.recentItem.enabled = FLEXTabList.sharedList.activeTab != nil;
|
||||
} else {
|
||||
toolbar.recentItem.enabled = NO;
|
||||
}
|
||||
toolbar.recentItem.enabled = FLEXTabList.sharedList.activeTab != nil;
|
||||
}
|
||||
|
||||
|
||||
@@ -494,22 +653,34 @@ 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
|
||||
UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc]
|
||||
UIPanGestureRecognizer *leftSwipe = [[UIPanGestureRecognizer alloc]
|
||||
initWithTarget:self action:@selector(handleChangeViewAtPointGesture:)
|
||||
];
|
||||
[toolbar.selectedViewDescriptionContainer addGestureRecognizer:panGesture];
|
||||
// UIPanGestureRecognizer *rightSwipe = [[UIPanGestureRecognizer alloc]
|
||||
// initWithTarget:self action:@selector(handleChangeViewAtPointGesture:)
|
||||
// ];
|
||||
// leftSwipe.direction = UISwipeGestureRecognizerDirectionLeft;
|
||||
// rightSwipe.direction = UISwipeGestureRecognizerDirectionRight;
|
||||
[toolbar.selectedViewDescriptionContainer addGestureRecognizer:leftSwipe];
|
||||
// [toolbar.selectedViewDescriptionContainer addGestureRecognizer:rightSwipe];
|
||||
|
||||
UILongPressGestureRecognizer *globalLongPressGesture = [[UILongPressGestureRecognizer alloc]
|
||||
initWithTarget:self action:@selector(handleToolbarShowTabsGesture:)];
|
||||
#if TARGET_OS_TV
|
||||
globalLongPressGesture.allowedPressTypes = @[[NSNumber numberWithInteger:UIPressTypePlayPause],[NSNumber numberWithInteger:UIPressTypeSelect]];
|
||||
#endif
|
||||
|
||||
// Long press gesture to present tabs manager
|
||||
[toolbar.globalsItem addGestureRecognizer:[[UILongPressGestureRecognizer alloc]
|
||||
initWithTarget:self action:@selector(handleToolbarShowTabsGesture:)
|
||||
]];
|
||||
[toolbar.globalsItem addGestureRecognizer:globalLongPressGesture];
|
||||
|
||||
UILongPressGestureRecognizer *selectLongPressGesture = [[UILongPressGestureRecognizer alloc]
|
||||
initWithTarget:self action:@selector(handleToolbarWindowManagerGesture:)];
|
||||
#if TARGET_OS_TV
|
||||
selectLongPressGesture.allowedPressTypes = @[[NSNumber numberWithInteger:UIPressTypePlayPause],[NSNumber numberWithInteger:UIPressTypeSelect]];
|
||||
#endif
|
||||
// Long press gesture to present window manager
|
||||
[toolbar.selectItem addGestureRecognizer:[[UILongPressGestureRecognizer alloc]
|
||||
initWithTarget:self action:@selector(handleToolbarWindowManagerGesture:)
|
||||
]];
|
||||
[toolbar.selectItem addGestureRecognizer:selectLongPressGesture];
|
||||
|
||||
// Long press gesture to present view controllers at tap
|
||||
[toolbar.hierarchyItem addGestureRecognizer:[[UILongPressGestureRecognizer alloc]
|
||||
@@ -589,8 +760,9 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
- (void)handleToolbarShowTabsGesture:(UILongPressGestureRecognizer *)sender {
|
||||
if (sender.state == UIGestureRecognizerStateBegan) {
|
||||
// Back up the UIMenuController items since dismissViewController: will attempt to replace them
|
||||
#if !TARGET_OS_TV
|
||||
self.appMenuItems = UIMenuController.sharedMenuController.menuItems;
|
||||
|
||||
#endif
|
||||
// Don't use FLEXNavigationController because the tab viewer itself is not a tab
|
||||
[super presentViewController:[[UINavigationController alloc]
|
||||
initWithRootViewController:[FLEXTabsViewController new]
|
||||
@@ -601,8 +773,9 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
- (void)handleToolbarWindowManagerGesture:(UILongPressGestureRecognizer *)sender {
|
||||
if (sender.state == UIGestureRecognizerStateBegan) {
|
||||
// Back up the UIMenuController items since dismissViewController: will attempt to replace them
|
||||
#if !TARGET_OS_TV
|
||||
self.appMenuItems = UIMenuController.sharedMenuController.menuItems;
|
||||
|
||||
#endif
|
||||
[super presentViewController:[FLEXNavigationController
|
||||
withRootViewController:[FLEXWindowManagerController new]
|
||||
] animated:YES completion:nil];
|
||||
@@ -612,8 +785,9 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
- (void)handleToolbarShowViewControllersGesture:(UILongPressGestureRecognizer *)sender {
|
||||
if (sender.state == UIGestureRecognizerStateBegan && self.viewsAtTapPoint.count) {
|
||||
// Back up the UIMenuController items since dismissViewController: will attempt to replace them
|
||||
#if !TARGET_OS_TV
|
||||
self.appMenuItems = UIMenuController.sharedMenuController.menuItems;
|
||||
|
||||
#endif
|
||||
UIViewController *list = [FLEXViewControllersViewController
|
||||
controllersForViews:self.viewsAtTapPoint
|
||||
];
|
||||
@@ -684,9 +858,11 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
}
|
||||
|
||||
- (void)actuateSelectionChangedFeedback {
|
||||
#if !TARGET_OS_TV
|
||||
if (@available(iOS 10.0, *)) {
|
||||
[self.selectionFBG selectionChanged];
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)updateOutlineViewsForSelectionPoint:(CGPoint)selectionPointInWindow {
|
||||
@@ -852,34 +1028,31 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
#pragma mark - Touch Handling
|
||||
|
||||
- (BOOL)shouldReceiveTouchAtWindowPoint:(CGPoint)pointInWindowCoordinates {
|
||||
BOOL shouldReceiveTouch = NO;
|
||||
|
||||
CGPoint pointInLocalCoordinates = [self.view convertPoint:pointInWindowCoordinates fromView:nil];
|
||||
|
||||
// If we have a modal presented, is it in the modal?
|
||||
if (self.presentedViewController) {
|
||||
UIView *presentedView = self.presentedViewController.view;
|
||||
CGPoint pipvc = [presentedView convertPoint:pointInLocalCoordinates fromView:self.view];
|
||||
UIView *hit = [presentedView hitTest:pipvc withEvent:nil];
|
||||
if (hit != nil) {
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
|
||||
// Always if we're in selection mode
|
||||
if (self.currentMode == FLEXExplorerModeSelect) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
// Always in move mode too
|
||||
if (self.currentMode == FLEXExplorerModeMove) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
// Always if it's on the toolbar
|
||||
if (CGRectContainsPoint(self.explorerToolbar.frame, pointInLocalCoordinates)) {
|
||||
return YES;
|
||||
shouldReceiveTouch = YES;
|
||||
}
|
||||
|
||||
return NO;
|
||||
// Always if we're in selection mode
|
||||
if (!shouldReceiveTouch && self.currentMode == FLEXExplorerModeSelect) {
|
||||
shouldReceiveTouch = YES;
|
||||
}
|
||||
|
||||
// Always in move mode too
|
||||
if (!shouldReceiveTouch && self.currentMode == FLEXExplorerModeMove) {
|
||||
shouldReceiveTouch = YES;
|
||||
}
|
||||
|
||||
// Always if we have a modal presented
|
||||
if (!shouldReceiveTouch && self.presentedViewController) {
|
||||
shouldReceiveTouch = YES;
|
||||
}
|
||||
|
||||
return shouldReceiveTouch;
|
||||
}
|
||||
|
||||
|
||||
@@ -898,7 +1071,8 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
|
||||
// If we now have a selected view and we didn't have one previously, go to "select" mode.
|
||||
if (self.currentMode == FLEXExplorerModeDefault && selectedView) {
|
||||
self.currentMode = FLEXExplorerModeSelect;
|
||||
//self.currentMode = FLEXExplorerModeSelect;
|
||||
[self toggleSelectTool];
|
||||
}
|
||||
|
||||
// The selected view setter will also update the selected view overlay appropriately.
|
||||
@@ -919,86 +1093,80 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
if (!@available(iOS 13, *)) {
|
||||
[self statusWindow].windowLevel = self.view.window.windowLevel + 1.0;
|
||||
}
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
// Back up and replace the UIMenuController items
|
||||
// Edit: no longer replacing the items, but still backing them
|
||||
// up in case we start replacing them again in the future
|
||||
self.appMenuItems = UIMenuController.sharedMenuController.menuItems;
|
||||
|
||||
[self updateButtonStates];
|
||||
|
||||
#endif
|
||||
// Show the view controller
|
||||
[super presentViewController:toPresent animated:animated completion:^{
|
||||
[self updateButtonStates];
|
||||
|
||||
if (completion) completion();
|
||||
}];
|
||||
[super presentViewController:toPresent animated:animated completion:completion];
|
||||
}
|
||||
|
||||
- (void)dismissViewControllerAnimated:(BOOL)animated completion:(void (^)(void))completion {
|
||||
UIWindow *appWindow = self.window.previousKeyWindow;
|
||||
[appWindow makeKeyWindow];
|
||||
#if !TARGET_OS_TV
|
||||
[appWindow.rootViewController setNeedsStatusBarAppearanceUpdate];
|
||||
|
||||
// Restore previous UIMenuController items
|
||||
// Back up and replace the UIMenuController items
|
||||
UIMenuController.sharedMenuController.menuItems = self.appMenuItems;
|
||||
[UIMenuController.sharedMenuController update];
|
||||
self.appMenuItems = nil;
|
||||
|
||||
// Restore the status bar window's normal window level.
|
||||
// We want it above FLEX while a modal is presented for
|
||||
// scroll to top, but below FLEX otherwise for exploration.
|
||||
[self statusWindow].windowLevel = UIWindowLevelStatusBar;
|
||||
#endif
|
||||
|
||||
[self updateButtonStates];
|
||||
|
||||
[super dismissViewControllerAnimated:animated completion:^{
|
||||
[self updateButtonStates];
|
||||
|
||||
if (completion) completion();
|
||||
}];
|
||||
[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;
|
||||
}
|
||||
|
||||
- (void)disableToolbar {
|
||||
[self.explorerToolbar setUserInteractionEnabled:false];
|
||||
[self.explorerToolbar setAlpha:0.5];
|
||||
[self setNeedsFocusUpdate];
|
||||
[self updateFocusIfNeeded];
|
||||
}
|
||||
|
||||
- (void)enableToolbar {
|
||||
[self.explorerToolbar setUserInteractionEnabled:true];
|
||||
[self.explorerToolbar setAlpha:1.0];
|
||||
[self setNeedsFocusUpdate];
|
||||
[self updateFocusIfNeeded];
|
||||
}
|
||||
|
||||
#pragma mark - Keyboard Shortcut Helpers
|
||||
|
||||
- (void)toggleSelectTool {
|
||||
if (self.currentMode == FLEXExplorerModeSelect) {
|
||||
self.currentMode = FLEXExplorerModeDefault;
|
||||
cursorView.hidden = true;
|
||||
[self enableToolbar];
|
||||
} else {
|
||||
self.currentMode = FLEXExplorerModeSelect;
|
||||
cursorView.hidden = false;
|
||||
[self disableToolbar];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1017,6 +1185,7 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
- (void)toggleViewsToolWithCompletion:(void(^)(void))completion {
|
||||
[self toggleToolWithViewControllerProvider:^UINavigationController *{
|
||||
if (self.selectedView) {
|
||||
FXLog(@"we have a selected view still: %@", self.selectedView);
|
||||
return [FLEXHierarchyViewController
|
||||
delegate:self
|
||||
viewsAtTap:self.viewsAtTapPoint
|
||||
@@ -1025,7 +1194,11 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
} else {
|
||||
return [FLEXHierarchyViewController delegate:self];
|
||||
}
|
||||
} completion:completion];
|
||||
} completion:^{
|
||||
if (completion) {
|
||||
completion();
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)toggleMenuTool {
|
||||
|
||||
@@ -18,13 +18,20 @@
|
||||
// Some apps have windows at UIWindowLevelStatusBar + n.
|
||||
// If we make the window level too high, we block out UIAlertViews.
|
||||
// There's a balance between staying above the app's windows and staying below alerts.
|
||||
self.windowLevel = UIWindowLevelAlert - 1;
|
||||
// UIWindowLevelStatusBar + 100 seems to hit that balance.
|
||||
#if !TARGET_OS_TV
|
||||
self.windowLevel = UIWindowLevelStatusBar + 100.0;
|
||||
#endif
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
|
||||
return [self.eventDelegate shouldHandleTouchAtPoint:point];
|
||||
BOOL pointInside = NO;
|
||||
if ([self.eventDelegate shouldHandleTouchAtPoint:point]) {
|
||||
pointInside = [super pointInside:point withEvent:event];
|
||||
}
|
||||
return pointInside;
|
||||
}
|
||||
|
||||
- (BOOL)shouldAffectStatusBarAppearance {
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#import "FLEXManager+Private.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
#import <TargetConditionals.h>
|
||||
|
||||
@interface FLEXWindowManagerController ()
|
||||
@property (nonatomic) UIWindow *keyWindow;
|
||||
@@ -72,7 +73,7 @@
|
||||
[self dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (void)showRevertOrDismissAlert:(void(^)(void))revertBlock {
|
||||
- (void)showRevertOrDismissAlert:(void(^)())revertBlock {
|
||||
[self.tableView deselectRowAtIndexPath:self.tableView.indexPathForSelectedRow animated:YES];
|
||||
[self reloadData];
|
||||
[self.tableView reloadData];
|
||||
@@ -161,7 +162,9 @@
|
||||
|
||||
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kFLEXDetailCell forIndexPath:indexPath];
|
||||
#if !TARGET_OS_TV
|
||||
cell.accessoryType = UITableViewCellAccessoryDetailButton;
|
||||
#endif
|
||||
cell.textLabel.lineBreakMode = NSLineBreakByTruncatingTail;
|
||||
|
||||
UIWindow *window = nil;
|
||||
|
||||
@@ -80,20 +80,10 @@
|
||||
|
||||
- (void)closeTab:(UINavigationController *)tab {
|
||||
NSParameterAssert(tab);
|
||||
NSParameterAssert([self.openTabs containsObject:tab]);
|
||||
NSInteger idx = [self.openTabs indexOfObject:tab];
|
||||
if (idx != NSNotFound) {
|
||||
[self closeTabAtIndex:idx];
|
||||
}
|
||||
|
||||
// Not sure how this is possible, but it happens sometimes
|
||||
if (self.activeTab == tab) {
|
||||
[self chooseNewActiveTab];
|
||||
}
|
||||
|
||||
// It is possible for an object explorer to form a retain cycle
|
||||
// with its own navigation controller; clearing the view controllers
|
||||
// manually when closing a tab breaks the cycle
|
||||
tab.viewControllers = @[];
|
||||
[self closeTabAtIndex:idx];
|
||||
}
|
||||
|
||||
- (void)closeTabAtIndex:(NSInteger)idx {
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#import "FLEXExplorerViewController.h"
|
||||
#import "FLEXGlobalsViewController.h"
|
||||
#import "FLEXBookmarksViewController.h"
|
||||
#import "NSObject+FLEX_Reflection.h"
|
||||
|
||||
@interface FLEXTabsViewController ()
|
||||
@property (nonatomic, copy) NSArray<UINavigationController *> *openTabs;
|
||||
@@ -39,7 +40,9 @@
|
||||
[super viewDidLoad];
|
||||
|
||||
self.title = @"Open Tabs";
|
||||
#if !TARGET_OS_TV
|
||||
self.navigationController.hidesBarsOnSwipe = NO;
|
||||
#endif
|
||||
self.tableView.allowsMultipleSelectionDuringEditing = YES;
|
||||
|
||||
[self reloadData:NO];
|
||||
@@ -48,6 +51,15 @@
|
||||
- (void)viewWillAppear:(BOOL)animated {
|
||||
[super viewWillAppear:animated];
|
||||
[self setupDefaultBarItems];
|
||||
#if TARGET_OS_TV
|
||||
if ([self darkMode]){
|
||||
self.view.backgroundColor = [UIColor colorWithWhite:0.0 alpha:0.8];
|
||||
} else {
|
||||
self.view.backgroundColor = [UIColor colorWithWhite:1.0 alpha:0.8];
|
||||
}
|
||||
UIBarButtonItem *addButton = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(addTabButtonPressed:)];
|
||||
self.navigationItem.leftBarButtonItem = addButton;
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated {
|
||||
@@ -106,6 +118,7 @@
|
||||
|
||||
- (void)setupDefaultBarItems {
|
||||
self.navigationItem.rightBarButtonItem = FLEXBarButtonItemSystem(Done, self, @selector(dismissAnimated));
|
||||
#if !TARGET_OS_TV
|
||||
self.toolbarItems = @[
|
||||
UIBarButtonItem.flex_fixedSpace,
|
||||
UIBarButtonItem.flex_flexibleSpace,
|
||||
@@ -116,10 +129,12 @@
|
||||
|
||||
// Disable editing if no tabs available
|
||||
self.toolbarItems.lastObject.enabled = self.openTabs.count > 0;
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)setupEditingBarItems {
|
||||
self.navigationItem.rightBarButtonItem = nil;
|
||||
#if !TARGET_OS_TV
|
||||
self.toolbarItems = @[
|
||||
[UIBarButtonItem flex_itemWithTitle:@"Close All" target:self action:@selector(closeAllButtonPressed:)],
|
||||
UIBarButtonItem.flex_flexibleSpace,
|
||||
@@ -130,6 +145,7 @@
|
||||
];
|
||||
|
||||
self.toolbarItems.firstObject.tintColor = FLEXColor.destructiveColor;
|
||||
#endif
|
||||
}
|
||||
|
||||
- (FLEXExplorerViewController *)corePresenter {
|
||||
@@ -287,8 +303,10 @@
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
if (self.editing) {
|
||||
// Case: editing with multi-select
|
||||
#if !TARGET_OS_TV
|
||||
self.toolbarItems.lastObject.title = @"Close Selected";
|
||||
self.toolbarItems.lastObject.tintColor = FLEXColor.destructiveColor;
|
||||
#endif
|
||||
} else {
|
||||
if (self.activeIndex == indexPath.row && self.corePresenter != self.presentingViewController) {
|
||||
// Case: selected the already active tab
|
||||
@@ -307,8 +325,10 @@
|
||||
NSParameterAssert(self.editing);
|
||||
|
||||
if (tableView.indexPathsForSelectedRows.count == 0) {
|
||||
#if !TARGET_OS_TV
|
||||
self.toolbarItems.lastObject.title = @"Done";
|
||||
self.toolbarItems.lastObject.tintColor = self.view.tintColor;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+13
-12
@@ -6,16 +6,17 @@
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "UIBarButtonItem+FLEX.h"
|
||||
#import "CALayer+FLEX.h"
|
||||
#import "UIFont+FLEX.h"
|
||||
#import "UIGestureRecognizer+Blocks.h"
|
||||
#import "UIPasteboard+FLEX.h"
|
||||
#import "UIMenu+FLEX.h"
|
||||
#import "UITextField+Range.h"
|
||||
|
||||
#import "NSObject+FLEX_Reflection.h"
|
||||
#import "NSArray+FLEX.h"
|
||||
#import "NSUserDefaults+FLEX.h"
|
||||
#import "NSTimer+FLEX.h"
|
||||
#import "NSDateFormatter+FLEX.h"
|
||||
|
||||
#import <FLEX/UIBarButtonItem+FLEX.h>
|
||||
#import <FLEX/CALayer+FLEX.h>
|
||||
#import <FLEX/UIFont+FLEX.h>
|
||||
#import <FLEX/UIGestureRecognizer+Blocks.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/NSUserDefaults+FLEX.h>
|
||||
#import <FLEX/NSTimer+FLEX.h>
|
||||
|
||||
+12
-11
@@ -6,17 +6,18 @@
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXFilteringTableViewController.h"
|
||||
#import "FLEXNavigationController.h"
|
||||
#import "FLEXTableViewController.h"
|
||||
#import "FLEXTableView.h"
|
||||
#import <FLEX/FLEXFilteringTableViewController.h>
|
||||
#import <FLEX/FLEXNavigationController.h>
|
||||
#import <FLEX/FLEXTableViewController.h>
|
||||
#import <FLEX/FLEXTableView.h>
|
||||
|
||||
#import "FLEXSingleRowSection.h"
|
||||
#import "FLEXTableViewSection.h"
|
||||
#import <FLEX/FLEXSingleRowSection.h>
|
||||
#import <FLEX/FLEXTableViewSection.h>
|
||||
|
||||
#import "FLEXCodeFontCell.h"
|
||||
#import "FLEXSubtitleTableViewCell.h"
|
||||
#import "FLEXTableViewCell.h"
|
||||
#import "FLEXMultilineTableViewCell.h"
|
||||
#import "FLEXKeyValueTableViewCell.h"
|
||||
#import <FLEX/FLEXCodeFontCell.h>
|
||||
#import <FLEX/FLEXSubtitleTableViewCell.h>
|
||||
#import <FLEX/FLEXTableViewCell.h>
|
||||
#import <FLEX/FLEXMultilineTableViewCell.h>
|
||||
#import <FLEX/FLEXKeyValueTableViewCell.h>
|
||||
|
||||
#import <FLEX/FLEXScopeCarousel.h>
|
||||
|
||||
@@ -6,17 +6,25 @@
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
#import "FLEXObjectExplorerViewController.h"
|
||||
#import <FLEX/FLEXObjectExplorerFactory.h>
|
||||
#import <FLEX/FLEXObjectExplorerViewController.h>
|
||||
|
||||
#import "FLEXObjectExplorer.h"
|
||||
#import <FLEX/FLEXObjectExplorer.h>
|
||||
|
||||
#import "FLEXShortcut.h"
|
||||
#import "FLEXShortcutsSection.h"
|
||||
#import <FLEX/FLEXShortcut.h>
|
||||
#import <FLEX/FLEXShortcutsFactory+Defaults.h>
|
||||
#import <FLEX/FLEXShortcutsSection.h>
|
||||
#import <FLEX/FLEXBlockShortcuts.h>
|
||||
#import <FLEX/FLEXBundleShortcuts.h>
|
||||
#import <FLEX/FLEXClassShortcuts.h>
|
||||
#import <FLEX/FLEXImageShortcuts.h>
|
||||
#import <FLEX/FLEXLayerShortcuts.h>
|
||||
#import <FLEX/FLEXViewControllerShortcuts.h>
|
||||
#import <FLEX/FLEXViewShortcuts.h>
|
||||
|
||||
#import "FLEXCollectionContentSection.h"
|
||||
#import "FLEXColorPreviewSection.h"
|
||||
#import "FLEXDefaultsContentSection.h"
|
||||
#import "FLEXMetadataSection.h"
|
||||
#import "FLEXMutableListSection.h"
|
||||
#import "FLEXObjectInfoSection.h"
|
||||
#import <FLEX/FLEXCollectionContentSection.h>
|
||||
#import <FLEX/FLEXColorPreviewSection.h>
|
||||
#import <FLEX/FLEXDefaultsContentSection.h>
|
||||
#import <FLEX/FLEXMetadataSection.h>
|
||||
#import <FLEX/FLEXMutableListSection.h>
|
||||
#import <FLEX/FLEXObjectInfoSection.h>
|
||||
|
||||
+15
-17
@@ -6,22 +6,20 @@
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXObjcInternal.h"
|
||||
#import "FLEXSwiftInternal.h"
|
||||
#import "FLEXRuntimeSafety.h"
|
||||
#import "FLEXBlockDescription.h"
|
||||
#import "FLEXTypeEncodingParser.h"
|
||||
#import <FLEX/FLEXObjcInternal.h>
|
||||
#import <FLEX/FLEXRuntimeSafety.h>
|
||||
#import <FLEX/FLEXBlockDescription.h>
|
||||
#import <FLEX/FLEXTypeEncodingParser.h>
|
||||
|
||||
#import "FLEXMirror.h"
|
||||
#import "FLEXProtocol.h"
|
||||
#import "FLEXProperty.h"
|
||||
#import "FLEXIvar.h"
|
||||
#import "FLEXMethodBase.h"
|
||||
#import "FLEXMethod.h"
|
||||
#import "FLEXPropertyAttributes.h"
|
||||
#import "FLEXRuntime+Compare.h"
|
||||
#import "FLEXRuntime+UIKitHelpers.h"
|
||||
#import "FLEXMetadataExtras.h"
|
||||
#import <FLEX/FLEXMirror.h>
|
||||
#import <FLEX/FLEXProtocol.h>
|
||||
#import <FLEX/FLEXProperty.h>
|
||||
#import <FLEX/FLEXIvar.h>
|
||||
#import <FLEX/FLEXMethodBase.h>
|
||||
#import <FLEX/FLEXMethod.h>
|
||||
#import <FLEX/FLEXPropertyAttributes.h>
|
||||
#import <FLEX/FLEXRuntime+Compare.h>
|
||||
#import <FLEX/FLEXRuntime+UIKitHelpers.h>
|
||||
|
||||
#import "FLEXProtocolBuilder.h"
|
||||
#import "FLEXClassBuilder.h"
|
||||
#import <FLEX/FLEXProtocolBuilder.h>
|
||||
#import <FLEX/FLEXClassBuilder.h>
|
||||
|
||||
+13
-13
@@ -7,19 +7,19 @@
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXManager.h"
|
||||
#import "FLEXManager+Extensibility.h"
|
||||
#import "FLEXManager+Networking.h"
|
||||
#import <FLEX/FLEXManager.h>
|
||||
#import <FLEX/FLEXManager+Extensibility.h>
|
||||
#import <FLEX/FLEXManager+Networking.h>
|
||||
|
||||
#import "FLEXExplorerToolbar.h"
|
||||
#import "FLEXExplorerToolbarItem.h"
|
||||
#import "FLEXGlobalsEntry.h"
|
||||
#import <FLEX/FLEXExplorerToolbar.h>
|
||||
#import <FLEX/FLEXExplorerToolbarItem.h>
|
||||
#import <FLEX/FLEXGlobalsEntry.h>
|
||||
|
||||
#import "FLEX-Core.h"
|
||||
#import "FLEX-Runtime.h"
|
||||
#import "FLEX-Categories.h"
|
||||
#import "FLEX-ObjectExploring.h"
|
||||
#import <FLEX/FLEX-Core.h>
|
||||
#import <FLEX/FLEX-Runtime.h>
|
||||
#import <FLEX/FLEX-Categories.h>
|
||||
#import <FLEX/FLEX-ObjectExploring.h>
|
||||
|
||||
#import "FLEXMacros.h"
|
||||
#import "FLEXAlert.h"
|
||||
#import "FLEXResources.h"
|
||||
#import <FLEX/FLEXMacros.h>
|
||||
#import <FLEX/FLEXAlert.h>
|
||||
#import <FLEX/FLEXResources.h>
|
||||
|
||||
@@ -8,21 +8,12 @@
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@class FLEXDBQueryRowCell;
|
||||
|
||||
extern NSString * const kFLEXDBQueryRowCellReuse;
|
||||
|
||||
@protocol FLEXDBQueryRowCellLayoutSource <NSObject>
|
||||
|
||||
- (CGFloat)dbQueryRowCell:(FLEXDBQueryRowCell *)dbQueryRowCell minXForColumn:(NSUInteger)column;
|
||||
- (CGFloat)dbQueryRowCell:(FLEXDBQueryRowCell *)dbQueryRowCell widthForColumn:(NSUInteger)column;
|
||||
|
||||
@end
|
||||
|
||||
@interface FLEXDBQueryRowCell : UITableViewCell
|
||||
|
||||
/// An array of NSString, NSNumber, or NSData objects
|
||||
@property (nonatomic) NSArray *data;
|
||||
@property (nonatomic, weak) id<FLEXDBQueryRowCellLayoutSource> layoutSource;
|
||||
|
||||
@end
|
||||
|
||||
@@ -63,12 +63,11 @@ NSString * const kFLEXDBQueryRowCellReuse = @"kFLEXDBQueryRowCellReuse";
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
|
||||
CGFloat width = self.contentView.frame.size.width / self.labels.count;
|
||||
CGFloat height = self.contentView.frame.size.height;
|
||||
|
||||
[self.labels flex_forEach:^(UILabel *label, NSUInteger i) {
|
||||
CGFloat width = [self.layoutSource dbQueryRowCell:self widthForColumn:i];
|
||||
CGFloat minX = [self.layoutSource dbQueryRowCell:self minXForColumn:i];
|
||||
label.frame = CGRectMake(minX + 5, 0, (width - 10), height);
|
||||
label.frame = CGRectMake(width * i + 5, 0, (width - 10), height);
|
||||
}];
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,6 @@
|
||||
|
||||
@optional
|
||||
|
||||
- (NSArray<NSString *> *)queryRowIDsInTable:(NSString *)tableName;
|
||||
- (FLEXSQLResult *)executeStatement:(NSString *)SQLStatement;
|
||||
|
||||
@end
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
- (NSString *)rowTitle:(NSInteger)row;
|
||||
- (NSArray<NSString *> *)contentForRow:(NSInteger)row;
|
||||
|
||||
- (CGFloat)multiColumnTableView:(FLEXMultiColumnTableView *)tableView minWidthForContentCellInColumn:(NSInteger)column;
|
||||
- (CGFloat)multiColumnTableView:(FLEXMultiColumnTableView *)tableView widthForContentCellInColumn:(NSInteger)column;
|
||||
- (CGFloat)multiColumnTableView:(FLEXMultiColumnTableView *)tableView heightForContentCellInRow:(NSInteger)row;
|
||||
- (CGFloat)heightForTopHeaderInTableView:(FLEXMultiColumnTableView *)tableView;
|
||||
- (CGFloat)widthForLeftHeaderInTableView:(FLEXMultiColumnTableView *)tableView;
|
||||
|
||||
@@ -9,12 +9,10 @@
|
||||
#import "FLEXMultiColumnTableView.h"
|
||||
#import "FLEXDBQueryRowCell.h"
|
||||
#import "FLEXTableLeftCell.h"
|
||||
#import "NSArray+FLEX.h"
|
||||
#import "FLEXColor.h"
|
||||
|
||||
@interface FLEXMultiColumnTableView () <
|
||||
UITableViewDataSource, UITableViewDelegate,
|
||||
UIScrollViewDelegate, FLEXDBQueryRowCellLayoutSource
|
||||
UITableViewDataSource, UITableViewDelegate, UIScrollViewDelegate
|
||||
>
|
||||
|
||||
@property (nonatomic) UIScrollView *contentScrollView;
|
||||
@@ -23,12 +21,12 @@
|
||||
@property (nonatomic) UITableView *contentTableView;
|
||||
@property (nonatomic) UIView *leftHeader;
|
||||
|
||||
@property (nonatomic) NSArray<UIView *> *headerViews;
|
||||
|
||||
/// \c NSNotFound if no column selected
|
||||
@property (nonatomic) NSInteger sortColumn;
|
||||
@property (nonatomic) FLEXTableColumnHeaderSortType sortType;
|
||||
|
||||
@property (nonatomic) NSArray *rowData;
|
||||
|
||||
@property (nonatomic, readonly) NSInteger numberOfColumns;
|
||||
@property (nonatomic, readonly) NSInteger numberOfRows;
|
||||
@property (nonatomic, readonly) CGFloat topHeaderHeight;
|
||||
@@ -73,9 +71,9 @@ static const CGFloat kColumnMargin = 1;
|
||||
}
|
||||
|
||||
CGFloat contentWidth = 0.0;
|
||||
NSInteger columnsCount = self.numberOfColumns;
|
||||
for (int i = 0; i < columnsCount; i++) {
|
||||
contentWidth += CGRectGetWidth(self.headerViews[i].bounds);
|
||||
NSInteger rowsCount = self.numberOfColumns;
|
||||
for (int i = 0; i < rowsCount; i++) {
|
||||
contentWidth += [self contentWidthForColumn:i];
|
||||
}
|
||||
|
||||
CGFloat contentHeight = height - topheaderHeight - topInsets;
|
||||
@@ -119,7 +117,9 @@ static const CGFloat kColumnMargin = 1;
|
||||
UITableView *tableView = [UITableView new];
|
||||
tableView.delegate = self;
|
||||
tableView.dataSource = self;
|
||||
#if !TARGET_OS_TV
|
||||
tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
|
||||
#endif
|
||||
[tableView registerClass:[FLEXDBQueryRowCell class]
|
||||
forCellReuseIdentifier:kFLEXDBQueryRowCellReuse
|
||||
];
|
||||
@@ -135,7 +135,9 @@ static const CGFloat kColumnMargin = 1;
|
||||
UITableView *leftTableView = [UITableView new];
|
||||
leftTableView.delegate = self;
|
||||
leftTableView.dataSource = self;
|
||||
#if !TARGET_OS_TV
|
||||
leftTableView.separatorStyle = UITableViewCellSeparatorStyleNone;
|
||||
#endif
|
||||
self.leftTableView = leftTableView;
|
||||
[self addSubview:leftTableView];
|
||||
|
||||
@@ -149,30 +151,26 @@ static const CGFloat kColumnMargin = 1;
|
||||
#pragma mark - Data
|
||||
|
||||
- (void)reloadData {
|
||||
[self loadHeaderData];
|
||||
[self loadLeftViewData];
|
||||
[self loadContentData];
|
||||
[self loadHeaderData];
|
||||
}
|
||||
|
||||
- (void)loadHeaderData {
|
||||
// Remove existing headers, if any
|
||||
for (UIView *subview in self.headerViews) {
|
||||
for (UIView *subview in self.headerScrollView.subviews) {
|
||||
[subview removeFromSuperview];
|
||||
}
|
||||
|
||||
__block CGFloat xOffset = 0;
|
||||
|
||||
self.headerViews = [NSArray flex_forEachUpTo:self.numberOfColumns map:^id(NSUInteger column) {
|
||||
FLEXTableColumnHeader *header = [FLEXTableColumnHeader new];
|
||||
CGFloat xOffset = 0.0;
|
||||
for (NSInteger column = 0; column < self.numberOfColumns; column++) {
|
||||
CGFloat width = [self contentWidthForColumn:column] + self.columnMargin;
|
||||
|
||||
FLEXTableColumnHeader *header = [[FLEXTableColumnHeader alloc]
|
||||
initWithFrame:CGRectMake(xOffset, 0, width, self.topHeaderHeight - 1)
|
||||
];
|
||||
header.titleLabel.text = [self columnTitle:column];
|
||||
|
||||
CGSize fittingSize = CGSizeMake(CGFLOAT_MAX, self.topHeaderHeight - 1);
|
||||
CGFloat width = self.columnMargin + MAX(
|
||||
[self minContentWidthForColumn:column],
|
||||
[header sizeThatFits:fittingSize].width
|
||||
);
|
||||
header.frame = CGRectMake(xOffset, 0, width, self.topHeaderHeight - 1);
|
||||
|
||||
if (column == self.sortColumn) {
|
||||
header.sortType = self.sortType;
|
||||
}
|
||||
@@ -184,22 +182,21 @@ static const CGFloat kColumnMargin = 1;
|
||||
[header addGestureRecognizer:gesture];
|
||||
header.userInteractionEnabled = YES;
|
||||
|
||||
xOffset += width;
|
||||
[self.headerScrollView addSubview:header];
|
||||
return header;
|
||||
}];
|
||||
xOffset += width;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)contentHeaderTap:(UIGestureRecognizer *)gesture {
|
||||
NSInteger newSortColumn = [self.headerViews indexOfObject:gesture.view];
|
||||
NSInteger newSortColumn = [self.headerScrollView.subviews indexOfObject:gesture.view];
|
||||
FLEXTableColumnHeaderSortType newType = FLEXNextTableColumnHeaderSortType(self.sortType);
|
||||
|
||||
// Reset old header
|
||||
FLEXTableColumnHeader *oldHeader = (id)self.headerViews[self.sortColumn];
|
||||
FLEXTableColumnHeader *oldHeader = (id)self.headerScrollView.subviews[self.sortColumn];
|
||||
oldHeader.sortType = FLEXTableColumnHeaderSortTypeNone;
|
||||
|
||||
// Update new header
|
||||
FLEXTableColumnHeader *newHeader = (id)self.headerViews[newSortColumn];
|
||||
FLEXTableColumnHeader *newHeader = (id)self.headerScrollView.subviews[newSortColumn];
|
||||
newHeader.sortType = newType;
|
||||
|
||||
// Update self
|
||||
@@ -234,13 +231,13 @@ static const CGFloat kColumnMargin = 1;
|
||||
}
|
||||
// Right side table view for data
|
||||
else {
|
||||
self.rowData = [self.dataSource contentForRow:indexPath.row];
|
||||
FLEXDBQueryRowCell *cell = [tableView
|
||||
dequeueReusableCellWithIdentifier:kFLEXDBQueryRowCellReuse forIndexPath:indexPath
|
||||
];
|
||||
|
||||
cell.contentView.backgroundColor = backgroundColor;
|
||||
cell.data = [self.dataSource contentForRow:indexPath.row];
|
||||
cell.layoutSource = self;
|
||||
NSAssert(cell.data.count == self.numberOfColumns, @"Count of data provided was incorrect");
|
||||
return cell;
|
||||
}
|
||||
@@ -287,17 +284,6 @@ static const CGFloat kColumnMargin = 1;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark FLEXDBQueryRowCellLayoutSource
|
||||
|
||||
- (CGFloat)dbQueryRowCell:(FLEXDBQueryRowCell *)dbQueryRowCell minXForColumn:(NSUInteger)column {
|
||||
return CGRectGetMinX(self.headerViews[column].frame);
|
||||
}
|
||||
|
||||
- (CGFloat)dbQueryRowCell:(FLEXDBQueryRowCell *)dbQueryRowCell widthForColumn:(NSUInteger)column {
|
||||
return CGRectGetWidth(self.headerViews[column].bounds);
|
||||
}
|
||||
|
||||
|
||||
#pragma mark DataSource Accessor
|
||||
|
||||
- (NSInteger)numberOfRows {
|
||||
@@ -316,8 +302,8 @@ static const CGFloat kColumnMargin = 1;
|
||||
return [self.dataSource rowTitle:row];
|
||||
}
|
||||
|
||||
- (CGFloat)minContentWidthForColumn:(NSInteger)column {
|
||||
return [self.dataSource multiColumnTableView:self minWidthForContentCellInColumn:column];
|
||||
- (CGFloat)contentWidthForColumn:(NSInteger)column {
|
||||
return [self.dataSource multiColumnTableView:self widthForContentCellInColumn:column];
|
||||
}
|
||||
|
||||
- (CGFloat)contentHeightForRow:(NSInteger)row {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
@synthesize keyedRows = _keyedRows;
|
||||
|
||||
+ (instancetype)message:(NSString *)message {
|
||||
return [[self alloc] initWithMessage:message columns:nil rows:nil];
|
||||
return [[self alloc] initWithmessage:message columns:nil rows:nil];
|
||||
}
|
||||
|
||||
+ (instancetype)error:(NSString *)message {
|
||||
@@ -23,12 +23,12 @@
|
||||
}
|
||||
|
||||
+ (instancetype)columns:(NSArray<NSString *> *)columnNames rows:(NSArray<NSArray<NSString *> *> *)rowData {
|
||||
return [[self alloc] initWithMessage:nil columns:columnNames rows:rowData];
|
||||
return [[self alloc] initWithmessage:nil columns:columnNames rows:rowData];
|
||||
}
|
||||
|
||||
- (instancetype)initWithMessage:(NSString *)message columns:(NSArray<NSString *> *)columns rows:(NSArray<NSArray<NSString *> *> *)rows {
|
||||
- (id)initWithmessage:(NSString *)message columns:(NSArray *)columns rows:(NSArray<NSArray *> *)rows {
|
||||
NSParameterAssert(message || (columns && rows));
|
||||
NSParameterAssert(rows.count == 0 || columns.count == rows.firstObject.count);
|
||||
NSParameterAssert(columns.count == rows.firstObject.count);
|
||||
|
||||
self = [super init];
|
||||
if (self) {
|
||||
|
||||
@@ -12,10 +12,7 @@
|
||||
#import "FLEXRuntimeConstants.h"
|
||||
#import <sqlite3.h>
|
||||
|
||||
#define kQuery(name, str) static NSString * const QUERY_##name = str
|
||||
|
||||
kQuery(TABLENAMES, @"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name");
|
||||
kQuery(ROWIDS, @"SELECT rowid FROM \"%@\" ORDER BY rowid ASC");
|
||||
static NSString * const QUERY_TABLENAMES = @"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name";
|
||||
|
||||
@interface FLEXSQLiteDatabaseManager ()
|
||||
@property (nonatomic) sqlite3 *db;
|
||||
@@ -33,7 +30,7 @@ kQuery(ROWIDS, @"SELECT rowid FROM \"%@\" ORDER BY rowid ASC");
|
||||
- (instancetype)initWithPath:(NSString *)path {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
self.path = path;
|
||||
self.path = path;;
|
||||
}
|
||||
|
||||
return self;
|
||||
@@ -110,36 +107,16 @@ kQuery(ROWIDS, @"SELECT rowid FROM \"%@\" ORDER BY rowid ASC");
|
||||
- (NSArray<NSString *> *)queryAllColumnsOfTable:(NSString *)tableName {
|
||||
NSString *sql = [NSString stringWithFormat:@"PRAGMA table_info('%@')",tableName];
|
||||
FLEXSQLResult *results = [self executeStatement:sql];
|
||||
|
||||
// https://github.com/FLEXTool/FLEX/issues/554
|
||||
if (!results.keyedRows.count) {
|
||||
sql = [NSString stringWithFormat:@"SELECT * FROM pragma_table_info('%@')", tableName];
|
||||
results = [self executeStatement:sql];
|
||||
|
||||
// Fallback to empty query
|
||||
if (!results.keyedRows.count) {
|
||||
sql = [NSString stringWithFormat:@"SELECT * FROM \"%@\" where 0=1", tableName];
|
||||
return [self executeStatement:sql].columns ?: @[];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return [results.keyedRows flex_mapped:^id(NSDictionary *column, NSUInteger idx) {
|
||||
return column[@"name"];
|
||||
}] ?: @[];
|
||||
}
|
||||
|
||||
- (NSArray<NSArray *> *)queryAllDataInTable:(NSString *)tableName {
|
||||
NSString *command = [NSString stringWithFormat:@"SELECT * FROM \"%@\"", tableName];
|
||||
return [self executeStatement:command].rows ?: @[];
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)queryRowIDsInTable:(NSString *)tableName {
|
||||
NSString *command = [NSString stringWithFormat:QUERY_ROWIDS, tableName];
|
||||
NSArray<NSArray<NSString *> *> *data = [self executeStatement:command].rows ?: @[];
|
||||
|
||||
return [data flex_mapped:^id(NSArray<NSString *> *obj, NSUInteger idx) {
|
||||
return obj.firstObject;
|
||||
}];
|
||||
return [self executeStatement:[@"SELECT * FROM "
|
||||
stringByAppendingString:tableName
|
||||
]].rows ?: @[];
|
||||
}
|
||||
|
||||
- (FLEXSQLResult *)executeStatement:(NSString *)sql {
|
||||
@@ -161,7 +138,7 @@ kQuery(ROWIDS, @"SELECT rowid FROM \"%@\" ORDER BY rowid ASC");
|
||||
return self.lastResult;
|
||||
}
|
||||
|
||||
// Grab columns (columnCount will be 0 for insert/update/delete)
|
||||
// Grab columns
|
||||
int columnCount = sqlite3_column_count(pstmt);
|
||||
NSArray<NSString *> *columns = [NSArray flex_forEachUpTo:columnCount map:^id(NSUInteger i) {
|
||||
return @(sqlite3_column_name(pstmt, (int)i));
|
||||
@@ -179,9 +156,8 @@ kQuery(ROWIDS, @"SELECT rowid FROM \"%@\" ORDER BY rowid ASC");
|
||||
}
|
||||
|
||||
if (status == SQLITE_DONE) {
|
||||
// columnCount will be 0 for insert/update/delete
|
||||
if (rows.count || columnCount > 0) {
|
||||
// We executed a SELECT query
|
||||
if (rows.count) {
|
||||
// We selected some rows
|
||||
result = _lastResult = [FLEXSQLResult columns:columns rows:rows];
|
||||
} else {
|
||||
// We executed a query like INSERT, UDPATE, or DELETE
|
||||
@@ -281,7 +257,7 @@ kQuery(ROWIDS, @"SELECT rowid FROM \"%@\" ORDER BY rowid ASC");
|
||||
- (FLEXSQLResult *)errorResult:(NSString *)description {
|
||||
const char *error = sqlite3_errmsg(_db);
|
||||
NSString *message = error ? @(error) : [NSString
|
||||
stringWithFormat:@"(%@: empty error)", description
|
||||
stringWithFormat:@"(%@: empty error", description
|
||||
];
|
||||
|
||||
return [FLEXSQLResult error:message];
|
||||
|
||||
@@ -11,9 +11,6 @@
|
||||
#import "UIFont+FLEX.h"
|
||||
#import "FLEXUtility.h"
|
||||
|
||||
static const CGFloat kMargin = 5;
|
||||
static const CGFloat kArrowWidth = 20;
|
||||
|
||||
@interface FLEXTableColumnHeader ()
|
||||
@property (nonatomic, readonly) UILabel *arrowLabel;
|
||||
@property (nonatomic, readonly) UIView *lineView;
|
||||
@@ -63,16 +60,9 @@ static const CGFloat kArrowWidth = 20;
|
||||
|
||||
CGSize size = self.frame.size;
|
||||
|
||||
self.titleLabel.frame = CGRectMake(kMargin, 0, size.width - kArrowWidth - kMargin, size.height);
|
||||
self.arrowLabel.frame = CGRectMake(size.width - kArrowWidth, 0, kArrowWidth, size.height);
|
||||
self.titleLabel.frame = CGRectMake(5, 0, size.width - 25, size.height);
|
||||
self.arrowLabel.frame = CGRectMake(size.width - 20, 0, 20, size.height);
|
||||
self.lineView.frame = CGRectMake(size.width - 1, 2, FLEXPointsToPixels(1), size.height - 4);
|
||||
}
|
||||
|
||||
- (CGSize)sizeThatFits:(CGSize)size {
|
||||
CGFloat margins = kArrowWidth - 2 * kMargin;
|
||||
size = CGSizeMake(size.width - margins, size.height);
|
||||
CGFloat width = [_titleLabel sizeThatFits:size].width + margins;
|
||||
return CGSizeMake(width, size.height);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -7,30 +7,10 @@
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "FLEXDatabaseManager.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FLEXTableContentViewController : UIViewController
|
||||
|
||||
/// Display a mutable table with the given columns, rows, and name.
|
||||
///
|
||||
/// @param columnNames self explanatory.
|
||||
/// @param rowData an array of rows, where each row is an array of column data.
|
||||
/// @param rowIDs an array of stringy row IDs. Required for deleting rows.
|
||||
/// @param tableName an optional name of the table being viewed, if any. Enables adding rows.
|
||||
/// @param databaseManager an optional manager to allow modifying the table.
|
||||
/// Required for deleting rows. Required for adding rows if \c tableName is supplied.
|
||||
+ (instancetype)columns:(NSArray<NSString *> *)columnNames
|
||||
rows:(NSArray<NSArray<NSString *> *> *)rowData
|
||||
rowIDs:(NSArray<NSString *> *)rowIDs
|
||||
tableName:(NSString *)tableName
|
||||
database:(id<FLEXDatabaseManager>)databaseManager;
|
||||
|
||||
/// Display an immutable table with the given columns and rows.
|
||||
+ (instancetype)columns:(NSArray<NSString *> *)columnNames
|
||||
rows:(NSArray<NSArray<NSString *> *> *)rowData;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -7,22 +7,15 @@
|
||||
//
|
||||
|
||||
#import "FLEXTableContentViewController.h"
|
||||
#import "FLEXTableRowDataViewController.h"
|
||||
#import "FLEXMultiColumnTableView.h"
|
||||
#import "FLEXWebViewController.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "UIBarButtonItem+FLEX.h"
|
||||
|
||||
@interface FLEXTableContentViewController () <
|
||||
FLEXMultiColumnTableViewDataSource, FLEXMultiColumnTableViewDelegate
|
||||
>
|
||||
@property (nonatomic, readonly) NSArray<NSString *> *columns;
|
||||
@property (nonatomic) NSMutableArray<NSArray *> *rows;
|
||||
@property (nonatomic, readonly) NSString *tableName;
|
||||
@property (nonatomic, nullable) NSMutableArray<NSString *> *rowIDs;
|
||||
@property (nonatomic, readonly, nullable) id<FLEXDatabaseManager> databaseManager;
|
||||
|
||||
@property (nonatomic, readonly) BOOL canRefresh;
|
||||
@property (nonatomic, copy) NSArray<NSArray *> *rows;
|
||||
|
||||
@property (nonatomic) FLEXMultiColumnTableView *multiColumnView;
|
||||
@end
|
||||
@@ -30,44 +23,11 @@
|
||||
@implementation FLEXTableContentViewController
|
||||
|
||||
+ (instancetype)columns:(NSArray<NSString *> *)columnNames
|
||||
rows:(NSArray<NSArray<NSString *> *> *)rowData
|
||||
rowIDs:(NSArray<NSString *> *)rowIDs
|
||||
tableName:(NSString *)tableName
|
||||
database:(id<FLEXDatabaseManager>)databaseManager {
|
||||
return [[self alloc]
|
||||
initWithColumns:columnNames
|
||||
rows:rowData
|
||||
rowIDs:rowIDs
|
||||
tableName:tableName
|
||||
database:databaseManager
|
||||
];
|
||||
}
|
||||
|
||||
+ (instancetype)columns:(NSArray<NSString *> *)cols
|
||||
rows:(NSArray<NSArray<NSString *> *> *)rowData {
|
||||
return [[self alloc] initWithColumns:cols rows:rowData rowIDs:nil tableName:nil database:nil];
|
||||
}
|
||||
|
||||
- (instancetype)initWithColumns:(NSArray<NSString *> *)columnNames
|
||||
rows:(NSArray<NSArray<NSString *> *> *)rowData
|
||||
rowIDs:(nullable NSArray<NSString *> *)rowIDs
|
||||
tableName:(nullable NSString *)tableName
|
||||
database:(nullable id<FLEXDatabaseManager>)databaseManager {
|
||||
// Must supply all optional parameters as one, or none
|
||||
BOOL all = rowIDs && tableName && databaseManager;
|
||||
BOOL none = !rowIDs && !tableName && !databaseManager;
|
||||
NSParameterAssert(all || none);
|
||||
|
||||
self = [super init];
|
||||
if (self) {
|
||||
self->_columns = columnNames.copy;
|
||||
self->_rows = rowData.mutableCopy;
|
||||
self->_rowIDs = rowIDs.mutableCopy;
|
||||
self->_tableName = tableName.copy;
|
||||
self->_databaseManager = databaseManager;
|
||||
}
|
||||
|
||||
return self;
|
||||
FLEXTableContentViewController *controller = [self new];
|
||||
controller->_columns = columnNames;
|
||||
controller->_rows = rowData;
|
||||
return controller;
|
||||
}
|
||||
|
||||
- (void)loadView {
|
||||
@@ -78,9 +38,9 @@
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
self.title = self.tableName;
|
||||
|
||||
self.edgesForExtendedLayout = UIRectEdgeNone;
|
||||
[self.multiColumnView reloadData];
|
||||
[self setupToolbarItems];
|
||||
}
|
||||
|
||||
- (FLEXMultiColumnTableView *)multiColumnView {
|
||||
@@ -96,10 +56,6 @@
|
||||
return _multiColumnView;
|
||||
}
|
||||
|
||||
- (BOOL)canRefresh {
|
||||
return self.databaseManager && self.tableName;
|
||||
}
|
||||
|
||||
#pragma mark MultiColumnTableView DataSource
|
||||
|
||||
- (NSInteger)numberOfColumnsInTableView:(FLEXMultiColumnTableView *)tableView {
|
||||
@@ -128,8 +84,8 @@
|
||||
}
|
||||
|
||||
- (CGFloat)multiColumnTableView:(FLEXMultiColumnTableView *)tableView
|
||||
minWidthForContentCellInColumn:(NSInteger)column {
|
||||
return 100;
|
||||
widthForContentCellInColumn:(NSInteger)column {
|
||||
return 120;
|
||||
}
|
||||
|
||||
- (CGFloat)heightForTopHeaderInTableView:(FLEXMultiColumnTableView *)tableView {
|
||||
@@ -155,45 +111,15 @@
|
||||
return [NSString stringWithFormat:@"%@:\n%@", self.columns[idx], field];
|
||||
}];
|
||||
|
||||
NSArray<NSString *> *values = [self.rows[row] flex_mapped:^id(NSString *value, NSUInteger idx) {
|
||||
return [NSString stringWithFormat:@"'%@'", value];
|
||||
}];
|
||||
|
||||
[FLEXAlert makeAlert:^(FLEXAlert *make) {
|
||||
make.title([@"Row " stringByAppendingString:@(row).stringValue]);
|
||||
NSString *message = [fields componentsJoinedByString:@"\n\n"];
|
||||
make.message(message);
|
||||
make.button(@"Copy").handler(^(NSArray<NSString *> *strings) {
|
||||
#if !TARGET_OS_TV
|
||||
UIPasteboard.generalPasteboard.string = message;
|
||||
#endif
|
||||
});
|
||||
make.button(@"Copy as CSV").handler(^(NSArray<NSString *> *strings) {
|
||||
UIPasteboard.generalPasteboard.string = [values componentsJoinedByString:@", "];
|
||||
});
|
||||
make.button(@"Focus on Row").handler(^(NSArray<NSString *> *strings) {
|
||||
UIViewController *focusedRow = [FLEXTableRowDataViewController
|
||||
rows:[NSDictionary dictionaryWithObjects:self.rows[row] forKeys:self.columns]
|
||||
];
|
||||
[self.navigationController pushViewController:focusedRow animated:YES];
|
||||
});
|
||||
|
||||
// Option to delete row
|
||||
BOOL hasRowID = self.rows.count && row < self.rows.count;
|
||||
if (hasRowID && self.canRefresh) {
|
||||
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 reloadTableDataFromDB];
|
||||
}
|
||||
}];
|
||||
});
|
||||
}
|
||||
|
||||
make.button(@"Dismiss").cancelStyle();
|
||||
} showFrom:self];
|
||||
}
|
||||
@@ -203,8 +129,7 @@
|
||||
sortType:(FLEXTableColumnHeaderSortType)sortType {
|
||||
|
||||
NSArray<NSArray *> *sortContentData = [self.rows
|
||||
sortedArrayWithOptions:NSSortStable
|
||||
usingComparator:^NSComparisonResult(NSArray *obj1, NSArray *obj2) {
|
||||
sortedArrayUsingComparator:^NSComparisonResult(NSArray *obj1, NSArray *obj2) {
|
||||
id a = obj1[column], b = obj2[column];
|
||||
if (a == NSNull.null) {
|
||||
return NSOrderedAscending;
|
||||
@@ -212,11 +137,6 @@
|
||||
if (b == NSNull.null) {
|
||||
return NSOrderedDescending;
|
||||
}
|
||||
|
||||
if ([a respondsToSelector:@selector(compare:options:)] &&
|
||||
[b respondsToSelector:@selector(compare:options:)]) {
|
||||
return [a compare:b options:NSNumericSearch];
|
||||
}
|
||||
|
||||
if ([a respondsToSelector:@selector(compare:)] && [b respondsToSelector:@selector(compare:)]) {
|
||||
return [a compare:b];
|
||||
@@ -230,11 +150,12 @@
|
||||
sortContentData = sortContentData.reverseObjectEnumerator.allObjects.copy;
|
||||
}
|
||||
|
||||
self.rows = sortContentData.mutableCopy;
|
||||
self.rows = sortContentData;
|
||||
[self.multiColumnView reloadData];
|
||||
}
|
||||
|
||||
#pragma mark - About Transition
|
||||
#pragma mark -
|
||||
#pragma mark About Transition
|
||||
|
||||
- (void)willTransitionToTraitCollection:(UITraitCollection *)newCollection
|
||||
withTransitionCoordinator:(id <UIViewControllerTransitionCoordinator>)coordinator {
|
||||
@@ -252,108 +173,4 @@
|
||||
} completion:nil];
|
||||
}
|
||||
|
||||
#pragma mark - Toolbar
|
||||
|
||||
- (void)setupToolbarItems {
|
||||
// We do not support modifying realm databases
|
||||
if (![self.databaseManager respondsToSelector:@selector(executeStatement:)]) {
|
||||
return;
|
||||
}
|
||||
|
||||
UIBarButtonItem *trashButton = FLEXBarButtonItemSystem(Trash, self, @selector(trashPressed));
|
||||
UIBarButtonItem *addButton = FLEXBarButtonItemSystem(Add, self, @selector(addPressed));
|
||||
|
||||
// Only allow adding rows or deleting rows if we have a table name
|
||||
trashButton.enabled = self.canRefresh;
|
||||
addButton.enabled = self.canRefresh;
|
||||
|
||||
self.toolbarItems = @[
|
||||
UIBarButtonItem.flex_flexibleSpace,
|
||||
addButton,
|
||||
UIBarButtonItem.flex_flexibleSpace,
|
||||
[trashButton flex_withTintColor:UIColor.redColor],
|
||||
];
|
||||
}
|
||||
|
||||
- (void)trashPressed {
|
||||
NSParameterAssert(self.tableName);
|
||||
|
||||
[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];
|
||||
}
|
||||
|
||||
- (void)addPressed {
|
||||
NSParameterAssert(self.tableName);
|
||||
|
||||
[FLEXAlert makeAlert:^(FLEXAlert *make) {
|
||||
make.title(@"Add a New Row");
|
||||
make.message(@"Comma separate values to use in an INSERT statement.\n\n");
|
||||
make.message(@"INSERT INTO [table] VALUES (your_input)");
|
||||
make.textField(@"5, 'John Smith', 14,...");
|
||||
make.button(@"Insert").handler(^(NSArray<NSString *> *strings) {
|
||||
NSString *statement = [NSString stringWithFormat:
|
||||
@"INSERT INTO %@ VALUES (%@)", self.tableName, strings[0]
|
||||
];
|
||||
|
||||
[self executeStatementAndShowResult:statement completion:^(BOOL success) {
|
||||
if (success) {
|
||||
[self reloadTableDataFromDB];
|
||||
}
|
||||
}];
|
||||
});
|
||||
make.button(@"Cancel").cancelStyle();
|
||||
} showFrom:self];
|
||||
}
|
||||
|
||||
#pragma mark - Helpers
|
||||
|
||||
- (void)executeStatementAndShowResult:(NSString *)statement
|
||||
completion:(void (^_Nullable)(BOOL success))completion {
|
||||
NSParameterAssert(self.databaseManager);
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
- (void)reloadTableDataFromDB {
|
||||
if (!self.canRefresh) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSArray<NSArray *> *rows = [self.databaseManager queryAllDataInTable:self.tableName];
|
||||
NSArray<NSString *> *rowIDs = nil;
|
||||
if ([self.databaseManager respondsToSelector:@selector(queryRowIDsInTable:)]) {
|
||||
rowIDs = [self.databaseManager queryRowIDsInTable:self.tableName];
|
||||
}
|
||||
|
||||
self.rows = rows.mutableCopy;
|
||||
self.rowIDs = rowIDs.mutableCopy;
|
||||
[self.multiColumnView reloadData];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
#import "FLEXMutableListSection.h"
|
||||
#import "NSArray+FLEX.h"
|
||||
#import "FLEXAlert.h"
|
||||
#import "FLEXMacros.h"
|
||||
|
||||
@interface FLEXTableListViewController ()
|
||||
@property (nonatomic, readonly) id<FLEXDatabaseManager> dbm;
|
||||
@@ -71,13 +70,9 @@
|
||||
self.tables.selectionHandler = ^(FLEXTableListViewController *host, NSString *tableName) {
|
||||
NSArray *rows = [host.dbm queryAllDataInTable:tableName];
|
||||
NSArray *columns = [host.dbm queryAllColumnsOfTable:tableName];
|
||||
NSArray *rowIDs = nil;
|
||||
if ([host.dbm respondsToSelector:@selector(queryRowIDsInTable:)]) {
|
||||
rowIDs = [host.dbm queryRowIDsInTable:tableName];
|
||||
}
|
||||
UIViewController *resultsScreen = [FLEXTableContentViewController
|
||||
columns:columns rows:rows rowIDs:rowIDs tableName:tableName database:host.dbm
|
||||
];
|
||||
|
||||
UIViewController *resultsScreen = [FLEXTableContentViewController columns:columns rows:rows];
|
||||
resultsScreen.title = tableName;
|
||||
[host.navigationController pushViewController:resultsScreen animated:YES];
|
||||
};
|
||||
|
||||
@@ -93,37 +88,16 @@
|
||||
}
|
||||
|
||||
- (void)queryButtonPressed {
|
||||
[self showQueryInput:nil];
|
||||
}
|
||||
|
||||
- (void)showQueryInput:(NSString *)prefillQuery {
|
||||
FLEXSQLiteDatabaseManager *database = self.dbm;
|
||||
|
||||
[FLEXAlert makeAlert:^(FLEXAlert *make) {
|
||||
make.title(@"Execute an SQL query");
|
||||
make.configuredTextField(^(UITextField *textField) {
|
||||
textField.text = prefillQuery;
|
||||
});
|
||||
|
||||
make.textField(nil);
|
||||
make.button(@"Run").handler(^(NSArray<NSString *> *strings) {
|
||||
NSString *query = strings[0];
|
||||
FLEXSQLResult *result = [database executeStatement:query];
|
||||
FLEXSQLResult *result = [database executeStatement:strings[0]];
|
||||
|
||||
if (result.message) {
|
||||
// Allow users to edit their last query if it had an error
|
||||
if ([result.message containsString:@"error"]) {
|
||||
[FLEXAlert makeAlert:^(FLEXAlert *make) {
|
||||
make.title(@"Error").message(result.message);
|
||||
make.button(@"Edit Query").preferred().handler(^(NSArray<NSString *> *_) {
|
||||
// Show query editor again with our last input
|
||||
[self showQueryInput:query];
|
||||
});
|
||||
|
||||
make.button(@"Cancel").cancelStyle();
|
||||
} showFrom:self];
|
||||
} else {
|
||||
[FLEXAlert showAlert:@"Message" message:result.message from:self];
|
||||
}
|
||||
[FLEXAlert showAlert:@"Message" message:result.message from:self];
|
||||
} else {
|
||||
UIViewController *resultsScreen = [FLEXTableContentViewController
|
||||
columns:result.columns rows:result.rows
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
//
|
||||
// FLEXTableRowDataViewController.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Chaoshuai Lu on 7/8/20.
|
||||
//
|
||||
|
||||
#import "FLEXFilteringTableViewController.h"
|
||||
|
||||
@interface FLEXTableRowDataViewController : FLEXFilteringTableViewController
|
||||
|
||||
+ (instancetype)rows:(NSDictionary<NSString *, id> *)rowData;
|
||||
|
||||
@end
|
||||
@@ -1,54 +0,0 @@
|
||||
//
|
||||
// FLEXTableRowDataViewController.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Chaoshuai Lu on 7/8/20.
|
||||
//
|
||||
|
||||
#import "FLEXTableRowDataViewController.h"
|
||||
#import "FLEXMutableListSection.h"
|
||||
#import "FLEXAlert.h"
|
||||
|
||||
@interface FLEXTableRowDataViewController ()
|
||||
@property (nonatomic) NSDictionary<NSString *, NSString *> *rowsByColumn;
|
||||
@end
|
||||
|
||||
@implementation FLEXTableRowDataViewController
|
||||
|
||||
#pragma mark - Initialization
|
||||
|
||||
+ (instancetype)rows:(NSDictionary<NSString *, id> *)rowData {
|
||||
FLEXTableRowDataViewController *controller = [self new];
|
||||
controller.rowsByColumn = rowData;
|
||||
return controller;
|
||||
}
|
||||
|
||||
#pragma mark - Overrides
|
||||
|
||||
- (NSArray<FLEXTableViewSection *> *)makeSections {
|
||||
NSDictionary<NSString *, NSString *> *rowsByColumn = self.rowsByColumn;
|
||||
|
||||
FLEXMutableListSection<NSString *> *section = [FLEXMutableListSection list:self.rowsByColumn.allKeys
|
||||
cellConfiguration:^(UITableViewCell *cell, NSString *column, NSInteger row) {
|
||||
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
|
||||
cell.textLabel.text = column;
|
||||
cell.detailTextLabel.text = rowsByColumn[column].description;
|
||||
} filterMatcher:^BOOL(NSString *filterText, NSString *column) {
|
||||
return [column localizedCaseInsensitiveContainsString:filterText] ||
|
||||
[rowsByColumn[column] localizedCaseInsensitiveContainsString:filterText];
|
||||
}
|
||||
];
|
||||
|
||||
section.selectionHandler = ^(UIViewController *host, NSString *column) {
|
||||
UIPasteboard.generalPasteboard.string = rowsByColumn[column].description;
|
||||
[FLEXAlert makeAlert:^(FLEXAlert *make) {
|
||||
make.title(@"Column Copied to Clipboard");
|
||||
make.message(rowsByColumn[column].description);
|
||||
make.button(@"Dismiss").cancelStyle();
|
||||
} showFrom:host];
|
||||
};
|
||||
|
||||
return @[section];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,14 +0,0 @@
|
||||
//
|
||||
// FLEXAPNSViewController.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 6/28/22.
|
||||
// Copyright © 2022 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXGlobalsEntry.h"
|
||||
#import "FLEXFilteringTableViewController.h"
|
||||
|
||||
@interface FLEXAPNSViewController : FLEXFilteringTableViewController <FLEXGlobalsEntry>
|
||||
|
||||
@end
|
||||
@@ -1,372 +0,0 @@
|
||||
//
|
||||
// FLEXAPNSViewController.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 6/28/22.
|
||||
// Copyright © 2022 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXAPNSViewController.h"
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
#import "FLEXMutableListSection.h"
|
||||
#import "FLEXSingleRowSection.h"
|
||||
#import "NSUserDefaults+FLEX.h"
|
||||
#import "UIBarButtonItem+FLEX.h"
|
||||
#import "NSDateFormatter+FLEX.h"
|
||||
#import "FLEXResources.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
#import "flex_fishhook.h"
|
||||
#import <dlfcn.h>
|
||||
#import <UserNotifications/UserNotifications.h>
|
||||
|
||||
#define orig(method, ...) if (orig_##method) { orig_##method(__VA_ARGS__); }
|
||||
#define method_lookup(__selector, __cls, __return, ...) \
|
||||
([__cls instancesRespondToSelector:__selector] ? \
|
||||
(__return(*)(__VA_ARGS__))class_getMethodImplementation(__cls, __selector) : nil)
|
||||
|
||||
@interface FLEXAPNSViewController ()
|
||||
@property (nonatomic, readonly, class) Class appDelegateClass;
|
||||
@property (nonatomic, class) NSData *deviceToken;
|
||||
@property (nonatomic, class) NSError *registrationError;
|
||||
@property (nonatomic, readonly, class) NSString *deviceTokenString;
|
||||
@property (nonatomic, readonly, class) NSMutableArray<NSDictionary *> *remoteNotifications;
|
||||
@property (nonatomic, readonly, class) NSMutableArray<UNNotification *> *userNotifications API_AVAILABLE(ios(10.0));
|
||||
|
||||
@property (nonatomic) FLEXSingleRowSection *deviceToken;
|
||||
@property (nonatomic) FLEXMutableListSection<NSDictionary *> *remoteNotifications;
|
||||
@property (nonatomic) FLEXMutableListSection<UNNotification *> *userNotifications API_AVAILABLE(ios(10.0));
|
||||
@end
|
||||
|
||||
@implementation FLEXAPNSViewController
|
||||
|
||||
#pragma mark Swizzles
|
||||
|
||||
/// Hook User Notifications related methods on the app delegate
|
||||
/// and UNUserNotificationCenter delegate classes
|
||||
+ (void)load { FLEX_EXIT_IF_NO_CTORS()
|
||||
if (!NSUserDefaults.standardUserDefaults.flex_enableAPNSCapture) {
|
||||
return;
|
||||
}
|
||||
|
||||
//──────────────────────//
|
||||
// App Delegate //
|
||||
//──────────────────────//
|
||||
|
||||
// Hook UIApplication to intercept app delegate
|
||||
Class uiapp = UIApplication.self;
|
||||
auto orig_uiapp_setDelegate = (void(*)(id, SEL, id))class_getMethodImplementation(
|
||||
uiapp, @selector(setDelegate:)
|
||||
);
|
||||
|
||||
IMP uiapp_setDelegate = imp_implementationWithBlock(^(id _, id delegate) {
|
||||
[self hookAppDelegateClass:[delegate class]];
|
||||
orig_uiapp_setDelegate(_, @selector(setDelegate:), delegate);
|
||||
});
|
||||
|
||||
class_replaceMethod(
|
||||
uiapp,
|
||||
@selector(setDelegate:),
|
||||
uiapp_setDelegate,
|
||||
"v@:@"
|
||||
);
|
||||
|
||||
//───────────────────────────────────────────//
|
||||
// UNUserNotificationCenter Delegate //
|
||||
//───────────────────────────────────────────//
|
||||
|
||||
if (@available(iOS 10.0, *)) {
|
||||
Class unusernc = UNUserNotificationCenter.self;
|
||||
auto orig_unusernc_setDelegate = (void(*)(id, SEL, id))class_getMethodImplementation(
|
||||
unusernc, @selector(setDelegate:)
|
||||
);
|
||||
|
||||
IMP unusernc_setDelegate = imp_implementationWithBlock(^(id _, id delegate) {
|
||||
[self hookUNUserNotificationCenterDelegateClass:[delegate class]];
|
||||
orig_unusernc_setDelegate(_, @selector(setDelegate:), delegate);
|
||||
});
|
||||
|
||||
class_replaceMethod(
|
||||
unusernc,
|
||||
@selector(setDelegate:),
|
||||
unusernc_setDelegate,
|
||||
"v@:@"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+ (void)hookAppDelegateClass:(Class)appDelegate {
|
||||
// Abort if we already hooked something
|
||||
if (_appDelegateClass) {
|
||||
return;
|
||||
}
|
||||
|
||||
_appDelegateClass = appDelegate;
|
||||
|
||||
// Better documentation for what's happening is in hookUNUserNotificationCenterDelegateClass: below
|
||||
|
||||
auto types_didRegisterForRemoteNotificationsWithDeviceToken = "v@:@@";
|
||||
auto types_didFailToRegisterForRemoteNotificationsWithError = "v@:@@";
|
||||
auto types_didReceiveRemoteNotification = "v@:@@@?";
|
||||
|
||||
auto sel_didRegisterForRemoteNotifications = @selector(application:didRegisterForRemoteNotificationsWithDeviceToken:);
|
||||
auto sel_didFailToRegisterForRemoteNotifs = @selector(application:didFailToRegisterForRemoteNotificationsWithError:);
|
||||
auto sel_didReceiveRemoteNotification = @selector(application:didReceiveRemoteNotification:fetchCompletionHandler:);
|
||||
|
||||
auto orig_didRegisterForRemoteNotificationsWithDeviceToken = method_lookup(
|
||||
sel_didRegisterForRemoteNotifications, appDelegate, void, id, SEL, id, id);
|
||||
auto orig_didFailToRegisterForRemoteNotificationsWithError = method_lookup(
|
||||
sel_didFailToRegisterForRemoteNotifs, appDelegate, void, id, SEL, id, id);
|
||||
auto orig_didReceiveRemoteNotification = method_lookup(
|
||||
sel_didReceiveRemoteNotification, appDelegate, void, id, SEL, id, id, id);
|
||||
|
||||
IMP didRegisterForRemoteNotificationsWithDeviceToken = imp_implementationWithBlock(^(id _, id app, NSData *token) {
|
||||
self.deviceToken = token;
|
||||
orig(didRegisterForRemoteNotificationsWithDeviceToken, _, nil, app, token);
|
||||
});
|
||||
IMP didFailToRegisterForRemoteNotificationsWithError = imp_implementationWithBlock(^(id _, id app, NSError *error) {
|
||||
self.registrationError = error;
|
||||
orig(didFailToRegisterForRemoteNotificationsWithError, _, nil, app, error);
|
||||
});
|
||||
IMP didReceiveRemoteNotification = imp_implementationWithBlock(^(id _, id app, NSDictionary *payload, id handler) {
|
||||
// TODO: notify when new notifications are added
|
||||
[self.remoteNotifications addObject:payload];
|
||||
orig(didReceiveRemoteNotification, _, nil, app, payload, handler);
|
||||
});
|
||||
|
||||
class_replaceMethod(
|
||||
appDelegate,
|
||||
sel_didRegisterForRemoteNotifications,
|
||||
didRegisterForRemoteNotificationsWithDeviceToken,
|
||||
types_didRegisterForRemoteNotificationsWithDeviceToken
|
||||
);
|
||||
class_replaceMethod(
|
||||
appDelegate,
|
||||
sel_didFailToRegisterForRemoteNotifs,
|
||||
didFailToRegisterForRemoteNotificationsWithError,
|
||||
types_didFailToRegisterForRemoteNotificationsWithError
|
||||
);
|
||||
class_replaceMethod(
|
||||
appDelegate,
|
||||
sel_didReceiveRemoteNotification,
|
||||
didReceiveRemoteNotification,
|
||||
types_didReceiveRemoteNotification
|
||||
);
|
||||
}
|
||||
|
||||
+ (void)hookUNUserNotificationCenterDelegateClass:(Class)delegate API_AVAILABLE(ios(10.0)) {
|
||||
// Selector
|
||||
auto sel_didReceiveNotification =
|
||||
@selector(userNotificationCenter:willPresentNotification:withCompletionHandler:);
|
||||
// Original implementation (or nil if unimplemented)
|
||||
auto orig_didReceiveNotification = method_lookup(
|
||||
sel_didReceiveNotification, delegate, void, id, SEL, id, id, id);
|
||||
// Our hook (ignores self and other unneeded parameters)
|
||||
IMP didReceiveNotification = imp_implementationWithBlock(^(id _, id __, UNNotification *notification, id ___) {
|
||||
[self.userNotifications addObject:notification];
|
||||
// This macro is a no-op if there is no original implementation
|
||||
orig(didReceiveNotification, _, nil, __, notification, ___);
|
||||
});
|
||||
|
||||
// Set the hook
|
||||
class_replaceMethod(
|
||||
delegate,
|
||||
sel_didReceiveNotification,
|
||||
didReceiveNotification,
|
||||
"v@:@@@?"
|
||||
);
|
||||
}
|
||||
|
||||
#pragma mark Class Properties
|
||||
|
||||
static Class _appDelegateClass = nil;
|
||||
+ (Class)appDelegateClass {
|
||||
return _appDelegateClass;
|
||||
}
|
||||
|
||||
static NSData *_apnsDeviceToken = nil;
|
||||
+ (NSData *)deviceToken {
|
||||
return _apnsDeviceToken;
|
||||
}
|
||||
|
||||
+ (void)setDeviceToken:(NSData *)deviceToken {
|
||||
_apnsDeviceToken = deviceToken;
|
||||
}
|
||||
|
||||
+ (NSString *)deviceTokenString {
|
||||
static NSString *_deviceTokenString = nil;
|
||||
|
||||
if (!_deviceTokenString && self.deviceToken) {
|
||||
NSData *token = self.deviceToken;
|
||||
NSUInteger capacity = token.length * 2;
|
||||
NSMutableString *tokenString = [NSMutableString stringWithCapacity:capacity];
|
||||
|
||||
const UInt8 *tokenData = token.bytes;
|
||||
for (NSUInteger idx = 0; idx < token.length; ++idx) {
|
||||
[tokenString appendFormat:@"%02X", (int)tokenData[idx]];
|
||||
}
|
||||
|
||||
_deviceTokenString = tokenString;
|
||||
}
|
||||
|
||||
return _deviceTokenString;
|
||||
}
|
||||
|
||||
static NSError *_apnsRegistrationError = nil;
|
||||
+ (NSError *)registrationError {
|
||||
return _apnsRegistrationError;
|
||||
}
|
||||
|
||||
+ (void)setRegistrationError:(NSError *)error {
|
||||
_apnsRegistrationError = error;
|
||||
}
|
||||
|
||||
+ (NSMutableArray<NSDictionary *> *)userNotifications {
|
||||
static NSMutableArray *_userNotifications = nil;
|
||||
if (!_userNotifications) {
|
||||
_userNotifications = [NSMutableArray new];
|
||||
}
|
||||
|
||||
return _userNotifications;
|
||||
}
|
||||
|
||||
+ (NSMutableArray<NSDictionary *> *)remoteNotifications {
|
||||
static NSMutableArray *_remoteNotifications = nil;
|
||||
if (!_remoteNotifications) {
|
||||
_remoteNotifications = [NSMutableArray new];
|
||||
}
|
||||
|
||||
return _remoteNotifications;
|
||||
}
|
||||
|
||||
#pragma mark Instance stuff
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
self.title = @"Push Notifications";
|
||||
|
||||
self.refreshControl = [UIRefreshControl new];
|
||||
[self.refreshControl addTarget:self action:@selector(reloadData) forControlEvents:UIControlEventValueChanged];
|
||||
|
||||
[self addToolbarItems:@[
|
||||
[UIBarButtonItem
|
||||
flex_itemWithImage:FLEXResources.gearIcon
|
||||
target:self
|
||||
action:@selector(settingsButtonTapped)
|
||||
],
|
||||
]];
|
||||
}
|
||||
|
||||
- (NSArray<FLEXTableViewSection *> *)makeSections {
|
||||
self.deviceToken = [FLEXSingleRowSection title:@"APNS Device Token" reuse:nil cell:^(UITableViewCell *cell) {
|
||||
NSString *tokenString = FLEXAPNSViewController.deviceTokenString;
|
||||
if (tokenString) {
|
||||
cell.textLabel.text = tokenString;
|
||||
cell.textLabel.numberOfLines = 0;
|
||||
}
|
||||
else if (!NSUserDefaults.standardUserDefaults.flex_enableAPNSCapture) {
|
||||
cell.textLabel.text = @"APNS capture disabled";
|
||||
}
|
||||
else {
|
||||
cell.textLabel.text = @"Not yet registered";
|
||||
}
|
||||
}];
|
||||
self.deviceToken.selectionAction = ^(UIViewController *host) {
|
||||
UIPasteboard.generalPasteboard.string = FLEXAPNSViewController.deviceTokenString;
|
||||
[FLEXAlert showQuickAlert:@"Copied to Clipboard" from:host];
|
||||
};
|
||||
|
||||
// Remote Notifications //
|
||||
|
||||
self.remoteNotifications = [FLEXMutableListSection list:FLEXAPNSViewController.remoteNotifications
|
||||
cellConfiguration:^(UITableViewCell *cell, NSDictionary *notif, NSInteger row) {
|
||||
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
|
||||
// TODO: date received
|
||||
cell.detailTextLabel.text = [FLEXRuntimeUtility summaryForObject:notif];
|
||||
}
|
||||
filterMatcher:^BOOL(NSString *filterText, NSDictionary *notif) {
|
||||
return [notif.description localizedCaseInsensitiveContainsString:filterText];
|
||||
}
|
||||
];
|
||||
|
||||
self.remoteNotifications.customTitle = @"Remote Notifications";
|
||||
self.remoteNotifications.selectionHandler = ^(UIViewController *host, NSDictionary *notif) {
|
||||
[host.navigationController pushViewController:[
|
||||
FLEXObjectExplorerFactory explorerViewControllerForObject:notif
|
||||
] animated:YES];
|
||||
};
|
||||
|
||||
// User Notifications //
|
||||
|
||||
if (@available(iOS 10.0, *)) {
|
||||
self.userNotifications = [FLEXMutableListSection list:FLEXAPNSViewController.userNotifications
|
||||
cellConfiguration:^(UITableViewCell *cell, UNNotification *notif, NSInteger row) {
|
||||
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
|
||||
|
||||
// Subtitle is 'subtitle \n date'
|
||||
NSString *dateString = [NSDateFormatter flex_stringFrom:notif.date format:FLEXDateFormatPreciseClock];
|
||||
NSString *subtitle = notif.request.content.subtitle;
|
||||
subtitle = subtitle ? [NSString stringWithFormat:@"%@\n%@", subtitle, dateString] : dateString;
|
||||
|
||||
cell.textLabel.text = notif.request.content.title;
|
||||
cell.detailTextLabel.text = subtitle;
|
||||
}
|
||||
filterMatcher:^BOOL(NSString *filterText, NSDictionary *notif) {
|
||||
return [notif.description localizedCaseInsensitiveContainsString:filterText];
|
||||
}
|
||||
];
|
||||
|
||||
self.userNotifications.customTitle = @"Push Notifications";
|
||||
self.userNotifications.selectionHandler = ^(UIViewController *host, UNNotification *notif) {
|
||||
[host.navigationController pushViewController:[
|
||||
FLEXObjectExplorerFactory explorerViewControllerForObject:notif.request
|
||||
] animated:YES];
|
||||
};
|
||||
|
||||
return @[self.deviceToken, self.remoteNotifications, self.userNotifications];
|
||||
}
|
||||
else {
|
||||
return @[self.deviceToken, self.remoteNotifications];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)reloadData {
|
||||
[self.refreshControl endRefreshing];
|
||||
|
||||
self.remoteNotifications.customTitle = [NSString stringWithFormat:
|
||||
@"%@ notifications", @(self.remoteNotifications.filteredList.count)
|
||||
];
|
||||
[super reloadData];
|
||||
}
|
||||
|
||||
- (void)settingsButtonTapped {
|
||||
NSUserDefaults *defaults = NSUserDefaults.standardUserDefaults;
|
||||
BOOL enabled = defaults.flex_enableAPNSCapture;
|
||||
|
||||
NSString *apnsToggle = enabled ? @"Disable Capture" : @"Enable Capture";
|
||||
|
||||
[FLEXAlert makeAlert:^(FLEXAlert *make) {
|
||||
make.title(@"Settings")
|
||||
.message(@"Enable or disable the capture of push notifications.\n\n")
|
||||
.message(@"This will hook UIApplicationMain on launch until it is disabled, ")
|
||||
.message(@"and swizzle some app delegate methods. Restart the app for changes to take effect.");
|
||||
|
||||
make.button(apnsToggle).destructiveStyle().handler(^(NSArray<NSString *> *strings) {
|
||||
[defaults flex_toggleBoolForKey:kFLEXDefaultsAPNSCaptureEnabledKey];
|
||||
});
|
||||
make.button(@"Dismiss").cancelStyle();
|
||||
} showFrom:self];
|
||||
}
|
||||
|
||||
#pragma mark - FLEXGlobalsEntry
|
||||
|
||||
+ (NSString *)globalsEntryTitle:(FLEXGlobalsRow)row {
|
||||
return @"📌 Push Notifications";
|
||||
}
|
||||
|
||||
+ (UIViewController *)globalsEntryViewController:(FLEXGlobalsRow)row {
|
||||
return [self new];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -37,13 +37,17 @@
|
||||
[FLEXAlert makeAlert:^(FLEXAlert *make) {
|
||||
make.title(title).message(message);
|
||||
make.configuredTextField(^(UITextField *textField) {
|
||||
#if !TARGET_OS_TV
|
||||
NSString *copied = UIPasteboard.generalPasteboard.string;
|
||||
#endif
|
||||
textField.placeholder = @"0x00000070deadbeef";
|
||||
// Go ahead and paste our clipboard if we have an address copied
|
||||
#if !TARGET_OS_TV
|
||||
if ([copied hasPrefix:@"0x"]) {
|
||||
textField.text = copied;
|
||||
[textField selectAll:nil];
|
||||
}
|
||||
#endif
|
||||
});
|
||||
make.button(@"Explore").handler(^(NSArray<NSString *> *strings) {
|
||||
[host tryExploreAddress:strings.firstObject safely:YES];
|
||||
|
||||
@@ -26,14 +26,6 @@
|
||||
self.title = @"Cookies";
|
||||
}
|
||||
|
||||
- (NSString *)headerTitle {
|
||||
return self.cookies.title;
|
||||
}
|
||||
|
||||
- (void)setHeaderTitle:(NSString *)headerTitle {
|
||||
self.cookies.customTitle = headerTitle;
|
||||
}
|
||||
|
||||
- (NSArray<FLEXTableViewSection *> *)makeSections {
|
||||
NSSortDescriptor *nameSortDescriptor = [[NSSortDescriptor alloc]
|
||||
initWithKey:@"name" ascending:YES selector:@selector(caseInsensitiveCompare:)
|
||||
|
||||
@@ -35,17 +35,25 @@ 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"];
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
self.refreshControl = [UIRefreshControl new];
|
||||
[self.refreshControl addTarget:self action:@selector(refreshControlDidRefresh:) forControlEvents:UIControlEventValueChanged];
|
||||
|
||||
#endif
|
||||
[self reloadTableData];
|
||||
}
|
||||
|
||||
- (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];
|
||||
});
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)allClassNames {
|
||||
return self.instanceCountsForClassNames.allKeys;
|
||||
}
|
||||
@@ -93,7 +101,9 @@ static const NSInteger kFLEXLiveObjectsSortBySizeIndex = 2;
|
||||
|
||||
- (void)refreshControlDidRefresh:(id)sender {
|
||||
[self reloadTableData];
|
||||
#if !TARGET_OS_TV
|
||||
[self.refreshControl endRefreshing];
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)updateHeaderTitle {
|
||||
@@ -226,10 +236,7 @@ static const NSInteger kFLEXLiveObjectsSortBySizeIndex = 2;
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
NSString *className = self.filteredClassNames[indexPath.row];
|
||||
UIViewController *instances = [FLEXObjectListViewController
|
||||
instancesOfClassWithName:className
|
||||
retained:YES
|
||||
];
|
||||
UIViewController *instances = [FLEXObjectListViewController instancesOfClassWithName:className];
|
||||
[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 retained:(BOOL)retain;
|
||||
+ (UIViewController *)instancesOfClassWithName:(NSString *)className;
|
||||
+ (instancetype)subclassesOfClassWithName:(NSString *)className;
|
||||
+ (instancetype)objectsWithReferencesToObject:(id)object retained:(BOOL)retain;
|
||||
+ (instancetype)objectsWithReferencesToObject:(id)object;
|
||||
|
||||
@end
|
||||
|
||||
@@ -29,16 +29,24 @@ typedef NS_ENUM(NSUInteger, FLEXObjectReferenceSection) {
|
||||
FLEXObjectReferenceSectionCount
|
||||
};
|
||||
|
||||
NSArray<NSString *> * FLEXObjectReferenceSectionTitles() {
|
||||
return @[
|
||||
@"", @"AutoLayout", @"Key-Value Observing", @"FLEX"
|
||||
];
|
||||
}
|
||||
|
||||
NSString const * FLEXTitleForObjectReferenceSection(FLEXObjectReferenceSection section) {
|
||||
switch (section) {
|
||||
case FLEXObjectReferenceSectionCount: @throw NSInternalInconsistencyException;
|
||||
default: return FLEXObjectReferenceSectionTitles()[section];
|
||||
}
|
||||
}
|
||||
|
||||
@interface FLEXObjectListViewController ()
|
||||
|
||||
@property (nonatomic, readonly, class) NSArray<NSPredicate *> *defaultPredicates;
|
||||
@property (nonatomic, readonly, class) NSArray<NSString *> *defaultSectionTitles;
|
||||
|
||||
|
||||
@property (nonatomic, copy) NSArray<FLEXMutableListSection *> *sections;
|
||||
@property (nonatomic, copy) NSArray<FLEXMutableListSection *> *allSections;
|
||||
|
||||
@property (nonatomic, readonly, nullable) NSArray<FLEXObjectRef *> *references;
|
||||
@property (nonatomic, readonly) NSArray<FLEXObjectRef *> *references;
|
||||
@property (nonatomic, readonly) NSArray<NSPredicate *> *predicates;
|
||||
@property (nonatomic, readonly) NSArray<NSString *> *sectionTitles;
|
||||
|
||||
@@ -113,16 +121,10 @@ typedef NS_ENUM(NSUInteger, FLEXObjectReferenceSection) {
|
||||
}];
|
||||
}
|
||||
|
||||
+ (NSArray<NSString *> *)defaultSectionTitles {
|
||||
return @[
|
||||
@"", @"AutoLayout", @"Key-Value Observing", @"FLEX"
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Initialization
|
||||
|
||||
- (id)initWithReferences:(nullable NSArray<FLEXObjectRef *> *)references {
|
||||
- (id)initWithReferences:(NSArray<FLEXObjectRef *> *)references {
|
||||
return [self initWithReferences:references predicates:nil sectionTitles:nil];
|
||||
}
|
||||
|
||||
@@ -141,41 +143,89 @@ typedef NS_ENUM(NSUInteger, FLEXObjectReferenceSection) {
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (UIViewController *)instancesOfClassWithName:(NSString *)className retained:(BOOL)retain {
|
||||
NSArray<FLEXObjectRef *> *references = [FLEXHeapEnumerator
|
||||
instancesOfClassWithName:className retained:retain
|
||||
];
|
||||
|
||||
+ (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];
|
||||
if (references.count == 1) {
|
||||
return [FLEXObjectExplorerFactory
|
||||
explorerViewControllerForObject:references.firstObject.object
|
||||
explorerViewControllerForObject:references.firstObject.object
|
||||
];
|
||||
}
|
||||
|
||||
FLEXObjectListViewController *controller = [[self alloc] initWithReferences:references];
|
||||
controller.title = [NSString stringWithFormat:@"%@ (%@)", className, @(references.count)];
|
||||
controller.title = [NSString stringWithFormat:@"%@ (%lu)", className, (unsigned long)instances.count];
|
||||
return controller;
|
||||
}
|
||||
|
||||
+ (instancetype)subclassesOfClassWithName:(NSString *)className {
|
||||
NSArray<FLEXObjectRef *> *references = [FLEXRuntimeUtility subclassesOfClassWithName:className];
|
||||
NSArray<Class> *classes = FLEXGetAllSubclasses(NSClassFromString(className), NO);
|
||||
NSArray<FLEXObjectRef *> *references = [FLEXObjectRef referencingClasses:classes];
|
||||
FLEXObjectListViewController *controller = [[self alloc] initWithReferences:references];
|
||||
controller.title = [NSString stringWithFormat:@"Subclasses of %@ (%@)",
|
||||
className, @(references.count)
|
||||
controller.title = [NSString stringWithFormat:@"Subclasses of %@ (%lu)",
|
||||
className, (unsigned long)classes.count
|
||||
];
|
||||
|
||||
return controller;
|
||||
}
|
||||
|
||||
+ (instancetype)objectsWithReferencesToObject:(id)object retained:(BOOL)retain {
|
||||
NSArray<FLEXObjectRef *> *instances = [FLEXHeapEnumerator
|
||||
objectsWithReferencesToObject:object retained:retain
|
||||
];
|
||||
+ (instancetype)objectsWithReferencesToObject:(id)object {
|
||||
static Class SwiftObjectClass = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
SwiftObjectClass = NSClassFromString(@"SwiftObject");
|
||||
if (!SwiftObjectClass) {
|
||||
SwiftObjectClass = NSClassFromString(@"Swift._SwiftObject");
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
|
||||
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 = FLEXObjectReferenceSectionTitles();
|
||||
FLEXObjectListViewController *viewController = [[self alloc]
|
||||
initWithReferences:instances
|
||||
predicates:self.defaultPredicates
|
||||
sectionTitles:self.defaultSectionTitles
|
||||
predicates:predicates
|
||||
sectionTitles:sectionTitles
|
||||
];
|
||||
viewController.title = [NSString stringWithFormat:@"Referencing %@ %p",
|
||||
[FLEXRuntimeUtility safeClassNameForObject:object], object
|
||||
@@ -228,10 +278,14 @@ typedef NS_ENUM(NSUInteger, FLEXObjectReferenceSection) {
|
||||
}
|
||||
];
|
||||
|
||||
section.selectionHandler = ^(UIViewController *host, FLEXObjectRef *ref) {
|
||||
[host.navigationController pushViewController:[
|
||||
FLEXObjectExplorerFactory explorerViewControllerForObject:ref.object
|
||||
] animated:YES];
|
||||
__weak __typeof(self) weakSelf = self;
|
||||
section.selectionHandler = ^(__kindof UIViewController *host, FLEXObjectRef *ref) {
|
||||
__strong __typeof(self) strongSelf = weakSelf;
|
||||
if (strongSelf) {
|
||||
[strongSelf.navigationController pushViewController:[
|
||||
FLEXObjectExplorerFactory explorerViewControllerForObject:ref.object
|
||||
] animated:YES];
|
||||
}
|
||||
};
|
||||
|
||||
section.customTitle = title;
|
||||
|
||||
@@ -10,19 +10,10 @@
|
||||
|
||||
@interface FLEXObjectRef : NSObject
|
||||
|
||||
/// 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;
|
||||
+ (instancetype)referencing:(id)object;
|
||||
+ (instancetype)referencing:(id)object ivar:(NSString *)ivarName;
|
||||
|
||||
/// 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;
|
||||
+ (NSArray<FLEXObjectRef *> *)referencingAll:(NSArray *)objects;
|
||||
/// Classes do not have a summary, and the reference is just the class name.
|
||||
+ (NSArray<FLEXObjectRef *> *)referencingClasses:(NSArray<Class> *)classes;
|
||||
|
||||
@@ -31,11 +22,6 @@
|
||||
/// For instances, this is the result of -[FLEXRuntimeUtility summaryForObject:]
|
||||
/// For classes, there is no summary.
|
||||
@property (nonatomic, readonly) NSString *summary;
|
||||
@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;
|
||||
@property (nonatomic, readonly) id object;
|
||||
|
||||
@end
|
||||
|
||||
@@ -10,68 +10,42 @@
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
#import "NSArray+FLEX.h"
|
||||
|
||||
@interface FLEXObjectRef () {
|
||||
/// Used to retain the object if desired
|
||||
id _retainer;
|
||||
}
|
||||
@interface FLEXObjectRef ()
|
||||
@property (nonatomic, readonly) BOOL wantsSummary;
|
||||
@end
|
||||
|
||||
@implementation FLEXObjectRef
|
||||
@synthesize summary = _summary;
|
||||
|
||||
+ (instancetype)unretained:(__unsafe_unretained id)object {
|
||||
return [self referencing:object showSummary:YES retained:NO];
|
||||
+ (instancetype)referencing:(id)object {
|
||||
return [self referencing:object showSummary:YES];
|
||||
}
|
||||
|
||||
+ (instancetype)unretained:(__unsafe_unretained id)object ivar:(NSString *)ivarName {
|
||||
return [[self alloc] initWithObject:object ivarName:ivarName showSummary:YES retained:NO];
|
||||
+ (instancetype)referencing:(id)object showSummary:(BOOL)showSummary {
|
||||
return [[self alloc] initWithObject:object ivarName:nil showSummary:showSummary];
|
||||
}
|
||||
|
||||
+ (instancetype)retained:(id)object {
|
||||
return [self referencing:object showSummary:YES retained:YES];
|
||||
+ (instancetype)referencing:(id)object ivar:(NSString *)ivarName {
|
||||
return [[self alloc] initWithObject:object ivarName:ivarName showSummary:YES];
|
||||
}
|
||||
|
||||
+ (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 {
|
||||
+ (NSArray<FLEXObjectRef *> *)referencingAll:(NSArray *)objects {
|
||||
return [objects flex_mapped:^id(id obj, NSUInteger idx) {
|
||||
return [self referencing:obj showSummary:YES retained:retain];
|
||||
return [self referencing:obj showSummary:YES];
|
||||
}];
|
||||
}
|
||||
|
||||
+ (NSArray<FLEXObjectRef *> *)referencingClasses:(NSArray<Class> *)classes {
|
||||
return [classes flex_mapped:^id(id obj, NSUInteger idx) {
|
||||
return [self referencing:obj showSummary:NO retained:NO];
|
||||
return [self referencing:obj showSummary:NO];
|
||||
}];
|
||||
}
|
||||
|
||||
- (id)initWithObject:(__unsafe_unretained id)object
|
||||
ivarName:(NSString *)ivar
|
||||
showSummary:(BOOL)showSummary
|
||||
retained:(BOOL)retain {
|
||||
- (id)initWithObject:(id)object ivarName:(NSString *)ivar showSummary:(BOOL)showSummary {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_object = object;
|
||||
_wantsSummary = showSummary;
|
||||
|
||||
if (retain) {
|
||||
_retainer = object;
|
||||
}
|
||||
|
||||
NSString *class = [FLEXRuntimeUtility safeClassNameForObject:object];
|
||||
if (ivar) {
|
||||
@@ -99,20 +73,4 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (void)retainObject {
|
||||
if (!_retainer) {
|
||||
_retainer = _object;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)releaseObject {
|
||||
_retainer = nil;
|
||||
}
|
||||
|
||||
- (NSString *)debugDescription {
|
||||
return [NSString stringWithFormat:@"<%@: %@>",
|
||||
[self class], self.reference
|
||||
];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -8,6 +8,27 @@
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
#if TARGET_OS_TV
|
||||
@protocol KBWebViewDelegate;
|
||||
|
||||
@interface KBWebView: UIView
|
||||
@property (nullable, nonatomic, assign) id <KBWebViewDelegate> delegate;
|
||||
|
||||
- (void)loadRequest:(NSURLRequest *)request;
|
||||
- (void)loadHTMLString:(NSString *)string baseURL:(nullable NSURL *)baseURL;
|
||||
- (void)loadData:(NSData *)data MIMEType:(NSString *)MIMEType textEncodingName:(NSString *)textEncodingName baseURL:(NSURL *)baseURL;
|
||||
|
||||
@property (nullable, nonatomic, readonly, strong) NSURLRequest *request;
|
||||
|
||||
- (void)reload;
|
||||
- (void)stopLoading;
|
||||
|
||||
- (void)goBack;
|
||||
- (void)goForward;
|
||||
@end
|
||||
|
||||
#endif
|
||||
|
||||
@interface FLEXWebViewController : UIViewController
|
||||
|
||||
- (id)initWithURL:(NSURL *)url;
|
||||
|
||||
@@ -8,11 +8,20 @@
|
||||
|
||||
#import "FLEXWebViewController.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import <TargetConditionals.h>
|
||||
#if !TARGET_OS_TV
|
||||
#import <WebKit/WebKit.h>
|
||||
|
||||
@interface FLEXWebViewController () <WKNavigationDelegate>
|
||||
#else
|
||||
@interface FLEXWebViewController ()
|
||||
#endif
|
||||
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
@property (nonatomic) WKWebView *webView;
|
||||
#else
|
||||
@property (nonatomic) KBWebView *webView;
|
||||
#endif
|
||||
@property (nonatomic) NSString *originalText;
|
||||
|
||||
@end
|
||||
@@ -22,14 +31,20 @@
|
||||
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
|
||||
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
|
||||
if (self) {
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
WKWebViewConfiguration *configuration = [WKWebViewConfiguration new];
|
||||
|
||||
|
||||
if (@available(iOS 10.0, *)) {
|
||||
configuration.dataDetectorTypes = WKDataDetectorTypeLink;
|
||||
configuration.dataDetectorTypes = UIDataDetectorTypeLink;
|
||||
}
|
||||
|
||||
|
||||
self.webView = [[WKWebView alloc] initWithFrame:CGRectZero configuration:configuration];
|
||||
self.webView.navigationDelegate = self;
|
||||
#else
|
||||
self.webView = [[objc_getClass("UIWebView") alloc] initWithFrame:[UIScreen mainScreen].bounds];
|
||||
self.webView.delegate = self;
|
||||
#endif
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -38,26 +53,13 @@
|
||||
self = [self initWithNibName:nil bundle:nil];
|
||||
if (self) {
|
||||
self.originalText = text;
|
||||
|
||||
NSString *html = @"<head><style>:root{ color-scheme: light dark; }</style>"
|
||||
"<meta name='viewport' content='initial-scale=1.0'></head><body><pre>%@</pre></body>";
|
||||
|
||||
// Loading message for when input text takes a long time to escape
|
||||
NSString *loadingMessage = [NSString stringWithFormat:html, @"Loading..."];
|
||||
[self.webView loadHTMLString:loadingMessage baseURL:nil];
|
||||
|
||||
// Escape HTML on a background thread
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
NSString *escapedText = [FLEXUtility stringByEscapingHTMLEntitiesInString:text];
|
||||
NSString *htmlString = [NSString stringWithFormat:html, escapedText];
|
||||
|
||||
// Update webview on the main thread
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self.webView loadHTMLString:htmlString baseURL:nil];
|
||||
});
|
||||
});
|
||||
#if TARGET_OS_TV
|
||||
NSString *htmlString = [NSString stringWithFormat:@"<head><meta name='viewport' content='initial-scale=1.0'></head><body>%@</body>", [FLEXUtility stringByEscapingHTMLEntitiesInString:text]];
|
||||
#else
|
||||
NSString *htmlString = [NSString stringWithFormat:@"<head><meta name='viewport' content='initial-scale=1.0'></head><body><pre>%@</pre></body>", [FLEXUtility stringByEscapingHTMLEntitiesInString:text]];
|
||||
#endif
|
||||
[self.webView loadHTMLString:htmlString baseURL:nil];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
@@ -67,57 +69,97 @@
|
||||
NSURLRequest *request = [NSURLRequest requestWithURL:url];
|
||||
[self.webView loadRequest:request];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
// WKWebView's delegate is assigned so we need to clear it manually.
|
||||
#if !TARGET_OS_TV
|
||||
if (_webView.navigationDelegate == self) {
|
||||
_webView.navigationDelegate = nil;
|
||||
}
|
||||
#else
|
||||
if (_webView.delegate == self){
|
||||
_webView.delegate = nil;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
[self.view addSubview:self.webView];
|
||||
#if !TARGET_OS_TV
|
||||
self.webView.frame = self.view.bounds;
|
||||
self.webView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
||||
|
||||
#endif
|
||||
if (self.originalText.length > 0) {
|
||||
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc]
|
||||
initWithTitle:@"Copy" style:UIBarButtonItemStylePlain target:self action:@selector(copyButtonTapped:)
|
||||
];
|
||||
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Copy" style:UIBarButtonItemStylePlain target:self action:@selector(copyButtonTapped:)];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)copyButtonTapped:(id)sender {
|
||||
#if !TARGET_OS_TV
|
||||
[UIPasteboard.generalPasteboard setString:self.originalText];
|
||||
#endif
|
||||
}
|
||||
|
||||
#if TARGET_OS_TV
|
||||
|
||||
#pragma mark - KBWebView Delegate
|
||||
|
||||
-(void) webViewDidStartLoad:(KBWebView *)webView {
|
||||
LOG_SELF;
|
||||
}
|
||||
|
||||
- (BOOL)webView:(KBWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(NSInteger)navigationType {
|
||||
FXLog(@"navtype: %lu", navigationType);
|
||||
FXLog(@"urL: %@", request.URL);
|
||||
FXLog(@"scheme: %@", request.URL.scheme);
|
||||
FXLog(@"navigationType: %lu", navigationType);
|
||||
if (navigationType == 5){//
|
||||
return YES;
|
||||
} else {
|
||||
FLEXWebViewController *webVC = [[[self class] alloc] initWithURL:[request URL]];
|
||||
webVC.title = [[request URL] absoluteString];
|
||||
[self.navigationController pushViewController:webVC animated:YES];
|
||||
return NO;//? maybe?
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
-(void) webViewDidFinishLoad:(KBWebView *)webView {
|
||||
LOG_SELF;
|
||||
|
||||
}
|
||||
|
||||
#else
|
||||
#pragma mark - WKWebView Delegate
|
||||
|
||||
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction
|
||||
decisionHandler:(void (^)(WKNavigationActionPolicy))handler {
|
||||
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
|
||||
WKNavigationActionPolicy policy = WKNavigationActionPolicyCancel;
|
||||
if (navigationAction.navigationType == WKNavigationTypeOther) {
|
||||
// Allow the initial load
|
||||
policy = WKNavigationActionPolicyAllow;
|
||||
} else {
|
||||
// For clicked links, push another web view controller onto the navigation stack
|
||||
// so that hitting the back button works as expected.
|
||||
// For clicked links, push another web view controller onto the navigation stack so that hitting the back button works as expected.
|
||||
// Don't allow the current web view to handle the navigation.
|
||||
NSURLRequest *request = navigationAction.request;
|
||||
FLEXWebViewController *webVC = [[[self class] alloc] initWithURL:request.URL];
|
||||
webVC.title = request.URL.absoluteString;
|
||||
FLEXWebViewController *webVC = [[[self class] alloc] initWithURL:[request URL]];
|
||||
webVC.title = [[request URL] absoluteString];
|
||||
[self.navigationController pushViewController:webVC animated:YES];
|
||||
}
|
||||
|
||||
handler(policy);
|
||||
decisionHandler(policy);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#pragma mark - Class Helpers
|
||||
|
||||
+ (BOOL)supportsPathExtension:(NSString *)extension {
|
||||
BOOL supported = NO;
|
||||
NSSet<NSString *> *supportedExtensions = [self webViewSupportedPathExtensions];
|
||||
if ([supportedExtensions containsObject:extension.lowercaseString]) {
|
||||
if ([supportedExtensions containsObject:[extension lowercaseString]]) {
|
||||
supported = YES;
|
||||
}
|
||||
return supported;
|
||||
@@ -129,14 +171,11 @@
|
||||
dispatch_once(&onceToken, ^{
|
||||
// Note that this is not exhaustive, but all these extensions should work well in the web view.
|
||||
// See https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/CreatingContentforSafarioniPhone/CreatingContentforSafarioniPhone.html#//apple_ref/doc/uid/TP40006482-SW7
|
||||
pathExtensions = [NSSet<NSString *> setWithArray:@[
|
||||
@"jpg", @"jpeg", @"png", @"gif", @"pdf", @"svg", @"tiff", @"3gp", @"3gpp", @"3g2",
|
||||
@"3gp2", @"aiff", @"aif", @"aifc", @"cdda", @"amr", @"mp3", @"swa", @"mp4", @"mpeg",
|
||||
@"mpg", @"mp3", @"wav", @"bwf", @"m4a", @"m4b", @"m4p", @"mov", @"qt", @"mqv", @"m4v"
|
||||
]];
|
||||
pathExtensions = [NSSet<NSString *> setWithArray:@[@"jpg", @"jpeg", @"png", @"gif", @"pdf", @"svg", @"tiff", @"3gp", @"3gpp", @"3g2",
|
||||
@"3gp2", @"aiff", @"aif", @"aifc", @"cdda", @"amr", @"mp3", @"swa", @"mp4", @"mpeg",
|
||||
@"mpg", @"mp3", @"wav", @"bwf", @"m4a", @"m4b", @"m4p", @"mov", @"qt", @"mqv", @"m4v"]];
|
||||
|
||||
});
|
||||
|
||||
return pathExtensions;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
//
|
||||
// FLEXActivityViewController.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 5/26/22.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/// Wraps UIActivityViewController so that it can't dismiss other view controllers
|
||||
@interface FLEXActivityViewController : UIActivityViewController
|
||||
|
||||
/// @param source A \c UIVIew, \c UIBarButtonItem, or \c NSValue representing a source rect.
|
||||
+ (id)sharing:(NSArray *)items source:(nullable id)source;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -1,42 +0,0 @@
|
||||
//
|
||||
// FLEXActivityViewController.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 5/26/22.
|
||||
//
|
||||
|
||||
#import "FLEXActivityViewController.h"
|
||||
#import "FLEXMacros.h"
|
||||
|
||||
@interface FLEXActivityViewController ()
|
||||
@end
|
||||
|
||||
@implementation FLEXActivityViewController
|
||||
|
||||
+ (id)sharing:(NSArray *)items source:(id)sender {
|
||||
UIViewController *shareSheet = [[UIActivityViewController alloc]
|
||||
initWithActivityItems:items applicationActivities:nil
|
||||
];
|
||||
|
||||
if (sender && UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad) {
|
||||
UIPopoverPresentationController *popover = shareSheet.popoverPresentationController;
|
||||
|
||||
// Source view
|
||||
if ([sender isKindOfClass:UIView.self]) {
|
||||
popover.sourceView = sender;
|
||||
}
|
||||
// Source bar item
|
||||
if ([sender isKindOfClass:UIBarButtonItem.self]) {
|
||||
popover.barButtonItem = sender;
|
||||
}
|
||||
// Source rect
|
||||
if ([sender isKindOfClass:NSValue.self]) {
|
||||
CGRect rect = [sender CGRectValue];
|
||||
popover.sourceRect = rect;
|
||||
}
|
||||
}
|
||||
|
||||
return shareSheet;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
#import "FLEXTableViewController.h"
|
||||
#import "FLEXGlobalsEntry.h"
|
||||
#import "FLEXFileBrowserSearchOperation.h"
|
||||
|
||||
@interface FLEXFileBrowserController : FLEXTableViewController <FLEXGlobalsEntry>
|
||||
|
||||
|
||||
@@ -9,13 +9,12 @@
|
||||
#import "FLEXFileBrowserController.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXWebViewController.h"
|
||||
#import "FLEXActivityViewController.h"
|
||||
#import "FLEXImagePreviewViewController.h"
|
||||
#import "FLEXTableListViewController.h"
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
#import "FLEXObjectExplorerViewController.h"
|
||||
#import <mach-o/loader.h>
|
||||
#import "FLEXFileBrowserSearchOperation.h"
|
||||
#import "FLEXTV.h"
|
||||
|
||||
@interface FLEXFileBrowserTableViewCell : UITableViewCell
|
||||
@end
|
||||
@@ -34,7 +33,9 @@ typedef NS_ENUM(NSUInteger, FLEXFileBrowserSortAttribute) {
|
||||
@property (nonatomic) NSNumber *recursiveSize;
|
||||
@property (nonatomic) NSNumber *searchPathsSize;
|
||||
@property (nonatomic) NSOperationQueue *operationQueue;
|
||||
#if !TARGET_OS_TV
|
||||
@property (nonatomic) UIDocumentInteractionController *documentController;
|
||||
#endif
|
||||
@property (nonatomic) FLEXFileBrowserSortAttribute sortAttribute;
|
||||
|
||||
@end
|
||||
@@ -56,8 +57,9 @@ typedef NS_ENUM(NSUInteger, FLEXFileBrowserSortAttribute) {
|
||||
self.title = [path lastPathComponent];
|
||||
self.operationQueue = [NSOperationQueue new];
|
||||
|
||||
// Compute path size
|
||||
weakify(self)
|
||||
|
||||
//computing path size
|
||||
FLEXFileBrowserController *__weak weakSelf = self;
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
NSFileManager *fileManager = NSFileManager.defaultManager;
|
||||
NSDictionary<NSString *, id> *attributes = [fileManager attributesOfItemAtPath:path error:NULL];
|
||||
@@ -67,15 +69,16 @@ typedef NS_ENUM(NSUInteger, FLEXFileBrowserSortAttribute) {
|
||||
attributes = [fileManager attributesOfItemAtPath:[path stringByAppendingPathComponent:fileName] error:NULL];
|
||||
totalSize += [attributes fileSize];
|
||||
|
||||
// Bail if the interested view controller has gone away
|
||||
if (!self) {
|
||||
// Bail if the interested view controller has gone away.
|
||||
if (!weakSelf) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{ strongify(self)
|
||||
self.recursiveSize = @(totalSize);
|
||||
[self.tableView reloadData];
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
FLEXFileBrowserController *__strong strongSelf = weakSelf;
|
||||
strongSelf.recursiveSize = @(totalSize);
|
||||
[strongSelf.tableView reloadData];
|
||||
});
|
||||
});
|
||||
|
||||
@@ -97,6 +100,9 @@ typedef NS_ENUM(NSUInteger, FLEXFileBrowserSortAttribute) {
|
||||
target:self
|
||||
action:@selector(sortDidTouchUpInside:)]
|
||||
]];
|
||||
#if TARGET_OS_TV
|
||||
[self addlongPressGestureRecognizer];
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)sortDidTouchUpInside:(UIBarButtonItem *)sortButton {
|
||||
@@ -231,6 +237,77 @@ typedef NS_ENUM(NSUInteger, FLEXFileBrowserSortAttribute) {
|
||||
return cell;
|
||||
}
|
||||
|
||||
- (void)addlongPressGestureRecognizer {
|
||||
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(longPress:)];
|
||||
longPress.allowedPressTypes = @[[NSNumber numberWithInteger:UIPressTypePlayPause],[NSNumber numberWithInteger:UIPressTypeSelect]];
|
||||
[self.tableView addGestureRecognizer:longPress];
|
||||
UITapGestureRecognizer *rightTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(longPress:)];
|
||||
rightTap.allowedPressTypes = @[[NSNumber numberWithInteger:UIPressTypePlayPause],[NSNumber numberWithInteger:UIPressTypeRightArrow]];
|
||||
[self.tableView addGestureRecognizer:rightTap];
|
||||
}
|
||||
|
||||
- (void)longPress:(UILongPressGestureRecognizer*)gesture {
|
||||
if ( gesture.state == UIGestureRecognizerStateEnded) {
|
||||
|
||||
UITableViewCell *cell = [gesture.view valueForKey:@"_focusedCell"];
|
||||
[self showActionForCell:cell];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)fileBrowserOpen:(UITableViewCell *)cell {
|
||||
NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];
|
||||
NSString *fullPath = [self filePathAtIndexPath:indexPath];
|
||||
|
||||
BOOL stillExists = [NSFileManager.defaultManager fileExistsAtPath:self.path isDirectory:NULL];
|
||||
if (stillExists) {
|
||||
[FLEXAlert makeAlert:^(FLEXAlert *make) {
|
||||
make.title([NSString stringWithFormat:@"Open %@?", fullPath.lastPathComponent]);
|
||||
make.button(@"OK").handler(^(NSArray<NSString *> *strings) {
|
||||
FXLog(@"TODO: implement opening files here!");
|
||||
NSURL *fileURL = [NSURL fileURLWithPath:fullPath];
|
||||
BOOL canOpenURL = [[UIApplication sharedApplication] canOpenURL:fileURL];
|
||||
FXLog(@"can open file: %d", canOpenURL);
|
||||
if (canOpenURL){
|
||||
NSString *laws = [@[@"LS",@"Application",@"Workspace"] componentsJoinedByString:@""];
|
||||
NSString *df = [@[@"default",@"Workspace"] componentsJoinedByString:@""];
|
||||
id ws = [NSClassFromString(laws) valueForKey:df];
|
||||
[ws openURL:fileURL];
|
||||
}
|
||||
});
|
||||
make.button(@"Cancel").cancelStyle();
|
||||
} showFrom:self];
|
||||
} else {
|
||||
[FLEXAlert showAlert:@"File Removed" message:@"The file at the specified path no longer exists." from:self];
|
||||
}
|
||||
}
|
||||
|
||||
//this should only ever be used on tvOS so that #if TARGET_OS is a suffucient fix to keep this from having errors building on iOS
|
||||
- (void)showActionForCell:(UITableViewCell *)cell {
|
||||
#if TARGET_OS_TV
|
||||
NSIndexPath *indexPath = [self.tableView indexPathForCell:cell];
|
||||
NSString *fullPath = [self filePathAtIndexPath:indexPath];
|
||||
FXLog(@"showActionForCell: %@", fullPath);
|
||||
[FLEXAlert makeAlert:^(FLEXAlert *make) {
|
||||
make.title(@"Choose an action for this file");
|
||||
make.button(@"Open").handler(^(NSArray<NSString *> *strings) {
|
||||
[self fileBrowserOpen:cell];
|
||||
});
|
||||
make.button(@"Rename").destructiveStyle().handler(^(NSArray<NSString *> *strings) {
|
||||
[self fileBrowserRename:cell];
|
||||
});
|
||||
make.button(@"Delete").destructiveStyle().handler(^(NSArray<NSString *> *strings) {
|
||||
[self fileBrowserDelete:cell];
|
||||
});
|
||||
if ([FLEXUtility airdropAvailable]){
|
||||
make.button(@"Share").handler(^(NSArray<NSString *> *strings) {
|
||||
[self fileBrowserShare:cell];
|
||||
});
|
||||
}
|
||||
make.button(@"Cancel").cancelStyle();
|
||||
} showFrom:self];
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
[self.tableView deselectRowAtIndexPath:indexPath animated:YES];
|
||||
|
||||
@@ -266,19 +343,14 @@ typedef NS_ENUM(NSUInteger, FLEXFileBrowserSortAttribute) {
|
||||
if ([pathExtension isEqualToString:@"json"]) {
|
||||
prettyString = [FLEXUtility prettyJSONStringFromData:fileData];
|
||||
} else {
|
||||
// Try to decode an archived object, regardless of file extension
|
||||
NSKeyedUnarchiver *unarchiver = ({
|
||||
NSKeyedUnarchiver *obj = nil;
|
||||
if (@available(iOS 12.0, *)) {
|
||||
obj = [[NSKeyedUnarchiver alloc] initForReadingFromData:fileData error:nil];
|
||||
} else {
|
||||
obj = [[NSKeyedUnarchiver alloc] initForReadingWithData:fileData];
|
||||
}
|
||||
obj.requiresSecureCoding = NO;
|
||||
obj;
|
||||
});
|
||||
id object = [unarchiver decodeObjectForKey:NSKeyedArchiveRootObjectKey];
|
||||
|
||||
// Regardless of file extension...
|
||||
|
||||
id object = nil;
|
||||
@try {
|
||||
// Try to decode an archived object regardless of file extension
|
||||
object = [NSKeyedUnarchiver unarchiveObjectWithData:fileData];
|
||||
} @catch (NSException *e) { }
|
||||
|
||||
// Try to decode other things instead
|
||||
object = object ?: [NSPropertyListSerialization
|
||||
propertyListWithData:fileData
|
||||
@@ -340,6 +412,7 @@ typedef NS_ENUM(NSUInteger, FLEXFileBrowserSortAttribute) {
|
||||
}
|
||||
|
||||
- (BOOL)tableView:(UITableView *)tableView shouldShowMenuForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
#if !TARGET_OS_TV
|
||||
UIMenuItem *rename = [[UIMenuItem alloc] initWithTitle:@"Rename" action:@selector(fileBrowserRename:)];
|
||||
UIMenuItem *delete = [[UIMenuItem alloc] initWithTitle:@"Delete" action:@selector(fileBrowserDelete:)];
|
||||
UIMenuItem *copyPath = [[UIMenuItem alloc] initWithTitle:@"Copy Path" action:@selector(fileBrowserCopyPath:)];
|
||||
@@ -348,6 +421,9 @@ typedef NS_ENUM(NSUInteger, FLEXFileBrowserSortAttribute) {
|
||||
UIMenuController.sharedMenuController.menuItems = @[rename, delete, copyPath, share];
|
||||
|
||||
return YES;
|
||||
#else
|
||||
return NO;
|
||||
#endif
|
||||
}
|
||||
|
||||
- (BOOL)tableView:(UITableView *)tableView canPerformAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender {
|
||||
@@ -363,49 +439,52 @@ typedef NS_ENUM(NSUInteger, FLEXFileBrowserSortAttribute) {
|
||||
// Since our actions are outside of that protocol, we need to manually handle the action forwarding from the cells.
|
||||
}
|
||||
|
||||
- (UIContextMenuConfiguration *)tableView:(UITableView *)tableView
|
||||
contextMenuConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath
|
||||
point:(CGPoint)point __IOS_AVAILABLE(13.0) {
|
||||
weakify(self)
|
||||
return [UIContextMenuConfiguration configurationWithIdentifier:nil previewProvider:nil
|
||||
actionProvider:^UIMenu *(NSArray<UIMenuElement *> *suggestedActions) {
|
||||
UITableViewCell * const cell = [tableView cellForRowAtIndexPath:indexPath];
|
||||
UIAction *rename = [UIAction actionWithTitle:@"Rename" image:nil identifier:@"Rename"
|
||||
handler:^(UIAction *action) { strongify(self)
|
||||
[self fileBrowserRename:cell];
|
||||
}
|
||||
];
|
||||
UIAction *delete = [UIAction actionWithTitle:@"Delete" image:nil identifier:@"Delete"
|
||||
handler:^(UIAction *action) { strongify(self)
|
||||
[self fileBrowserDelete:cell];
|
||||
}
|
||||
];
|
||||
UIAction *copyPath = [UIAction actionWithTitle:@"Copy Path" image:nil identifier:@"Copy Path"
|
||||
handler:^(UIAction *action) { strongify(self)
|
||||
[self fileBrowserCopyPath:cell];
|
||||
}
|
||||
];
|
||||
UIAction *share = [UIAction actionWithTitle:@"Share" image:nil identifier:@"Share"
|
||||
handler:^(UIAction *action) { strongify(self)
|
||||
[self fileBrowserShare:cell];
|
||||
}
|
||||
];
|
||||
|
||||
return [UIMenu menuWithTitle:@"Manage File" image:nil
|
||||
identifier:@"Manage File"
|
||||
options:UIMenuOptionsDisplayInline
|
||||
children:@[rename, delete, copyPath, share]
|
||||
];
|
||||
}
|
||||
];
|
||||
#if FLEX_AT_LEAST_IOS13_SDK
|
||||
#if !TARGET_OS_TV
|
||||
- (UIContextMenuConfiguration *)tableView:(UITableView *)tableView contextMenuConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath point:(CGPoint)point __IOS_AVAILABLE(13.0) {
|
||||
__weak __typeof__(self) weakSelf = self;
|
||||
return [UIContextMenuConfiguration configurationWithIdentifier:nil
|
||||
previewProvider:nil
|
||||
actionProvider:^UIMenu * _Nullable(NSArray<UIMenuElement *> * _Nonnull suggestedActions) {
|
||||
UITableViewCell * const cell = [tableView cellForRowAtIndexPath:indexPath];
|
||||
UIAction *rename = [UIAction actionWithTitle:@"Rename"
|
||||
image:nil
|
||||
identifier:@"Rename"
|
||||
handler:^(__kindof UIAction * _Nonnull action) {
|
||||
[weakSelf fileBrowserRename:cell];
|
||||
}];
|
||||
UIAction *delete = [UIAction actionWithTitle:@"Delete"
|
||||
image:nil
|
||||
identifier:@"Delete"
|
||||
handler:^(__kindof UIAction * _Nonnull action) {
|
||||
[weakSelf fileBrowserDelete:cell];
|
||||
}];
|
||||
UIAction *copyPath = [UIAction actionWithTitle:@"Copy Path"
|
||||
image:nil
|
||||
identifier:@"Copy Path"
|
||||
handler:^(__kindof UIAction * _Nonnull action) {
|
||||
[weakSelf fileBrowserCopyPath:cell];
|
||||
}];
|
||||
UIAction *share = [UIAction actionWithTitle:@"Share"
|
||||
image:nil
|
||||
identifier:@"Share"
|
||||
handler:^(__kindof UIAction * _Nonnull action) {
|
||||
[weakSelf fileBrowserShare:cell];
|
||||
}];
|
||||
return [UIMenu menuWithTitle:@"Manage File" image:nil identifier:@"Manage File" options:UIMenuOptionsDisplayInline children:@[rename, delete, copyPath, share]];
|
||||
}];
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
- (void)openFileController:(NSString *)fullPath {
|
||||
#if !TARGET_OS_TV
|
||||
UIDocumentInteractionController *controller = [UIDocumentInteractionController new];
|
||||
controller.URL = [NSURL fileURLWithPath:fullPath];
|
||||
|
||||
[controller presentOptionsMenuFromRect:self.view.bounds inView:self.view animated:YES];
|
||||
self.documentController = controller;
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)fileBrowserRename:(UITableViewCell *)sender {
|
||||
@@ -458,16 +537,30 @@ contextMenuConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath
|
||||
}
|
||||
|
||||
- (void)fileBrowserCopyPath:(UITableViewCell *)sender {
|
||||
#if !TARGET_OS_TV
|
||||
NSIndexPath *indexPath = [self.tableView indexPathForCell:sender];
|
||||
NSString *fullPath = [self filePathAtIndexPath:indexPath];
|
||||
|
||||
UIPasteboard.generalPasteboard.string = fullPath;
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)fileBrowserShare:(UITableViewCell *)sender {
|
||||
NSIndexPath *indexPath = [self.tableView indexPathForCell:sender];
|
||||
NSString *pathString = [self filePathAtIndexPath:indexPath];
|
||||
NSURL *filePath = [NSURL fileURLWithPath:pathString];
|
||||
|
||||
#if TARGET_OS_TV
|
||||
//This only helps on jailbroken AppleTV - it will allow you to share the files over AirDrop, no share option exists otherwise.
|
||||
if ([FLEXUtility airdropAvailable]){
|
||||
[FLEXUtility airDropFile:pathString];
|
||||
//NSURL *url = [NSURL URLWithString:[NSString stringWithFormat:@"airdropper://%@", pathString]];
|
||||
//UIApplication *application = [UIApplication sharedApplication];
|
||||
//[application openURL:url options:@{} completionHandler:nil];
|
||||
} else {
|
||||
[FLEXAlert showAlert:@"Oh no" message:@"A jailbroken AppleTV is required to share files through AirDrop, sorry!" from:self];
|
||||
}
|
||||
|
||||
#else
|
||||
BOOL isDirectory = NO;
|
||||
[NSFileManager.defaultManager fileExistsAtPath:pathString isDirectory:&isDirectory];
|
||||
|
||||
@@ -476,9 +569,12 @@ contextMenuConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath
|
||||
[self openFileController:pathString];
|
||||
} else {
|
||||
// Share sheet for files
|
||||
UIViewController *shareSheet = [FLEXActivityViewController sharing:@[filePath] source:sender];
|
||||
|
||||
UIActivityViewController *shareSheet = [[UIActivityViewController alloc] initWithActivityItems:@[filePath] applicationActivities:nil];
|
||||
[self presentViewController:shareSheet animated:true completion:nil];
|
||||
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)reloadDisplayedPaths {
|
||||
@@ -544,20 +640,20 @@ contextMenuConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath
|
||||
id target = [self.nextResponder targetForAction:action withSender:sender];
|
||||
[UIApplication.sharedApplication sendAction:action to:target from:self forEvent:nil];
|
||||
}
|
||||
|
||||
- (void)fileBrowserRename:(UIMenuController *)sender {
|
||||
//really UIMenuController but this is to silence warnings
|
||||
- (void)fileBrowserRename:(UIViewController *)sender {
|
||||
[self forwardAction:_cmd withSender:sender];
|
||||
}
|
||||
|
||||
- (void)fileBrowserDelete:(UIMenuController *)sender {
|
||||
- (void)fileBrowserDelete:(UIViewController *)sender {
|
||||
[self forwardAction:_cmd withSender:sender];
|
||||
}
|
||||
|
||||
- (void)fileBrowserCopyPath:(UIMenuController *)sender {
|
||||
- (void)fileBrowserCopyPath:(UIViewController *)sender {
|
||||
[self forwardAction:_cmd withSender:sender];
|
||||
}
|
||||
|
||||
- (void)fileBrowserShare:(UIMenuController *)sender {
|
||||
- (void)fileBrowserShare:(UIViewController *)sender {
|
||||
[self forwardAction:_cmd withSender:sender];
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ typedef NS_ENUM(NSUInteger, FLEXGlobalsRow) {
|
||||
FLEXGlobalsRowCookies,
|
||||
FLEXGlobalsRowBrowseRuntime,
|
||||
FLEXGlobalsRowAppKeychainItems,
|
||||
FLEXGlobalsRowPushNotifications,
|
||||
FLEXGlobalsRowAppDelegate,
|
||||
FLEXGlobalsRowRootViewController,
|
||||
FLEXGlobalsRowUserDefaults,
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
#import "FLEXObjcRuntimeViewController.h"
|
||||
#import "FLEXKeychainViewController.h"
|
||||
#import "FLEXAPNSViewController.h"
|
||||
#import "FLEXObjectExplorerViewController.h"
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
#import "FLEXLiveObjectsController.h"
|
||||
@@ -58,8 +57,6 @@
|
||||
switch (row) {
|
||||
case FLEXGlobalsRowAppKeychainItems:
|
||||
return [FLEXKeychainViewController flex_concreteGlobalsEntry:row];
|
||||
case FLEXGlobalsRowPushNotifications:
|
||||
return [FLEXAPNSViewController flex_concreteGlobalsEntry:row];
|
||||
case FLEXGlobalsRowAddressInspector:
|
||||
return [FLEXAddressExplorerCoordinator flex_concreteGlobalsEntry:row];
|
||||
case FLEXGlobalsRowBrowseRuntime:
|
||||
@@ -97,14 +94,13 @@
|
||||
case FLEXGlobalsRowMainThread:
|
||||
case FLEXGlobalsRowOperationQueue:
|
||||
return [FLEXObjectExplorerFactory flex_concreteGlobalsEntry:row];
|
||||
|
||||
case FLEXGlobalsRowCount: break;
|
||||
|
||||
default:
|
||||
@throw [NSException
|
||||
exceptionWithName:NSInternalInconsistencyException
|
||||
reason:@"Missing globals case in switch" userInfo:nil
|
||||
];
|
||||
}
|
||||
|
||||
@throw [NSException
|
||||
exceptionWithName:NSInternalInconsistencyException
|
||||
reason:@"Missing globals case in switch" userInfo:nil
|
||||
];
|
||||
}
|
||||
|
||||
+ (NSArray<FLEXGlobalsSection *> *)defaultGlobalSections {
|
||||
@@ -126,7 +122,6 @@
|
||||
[self globalsEntryForRow:FLEXGlobalsRowMainBundle],
|
||||
[self globalsEntryForRow:FLEXGlobalsRowUserDefaults],
|
||||
[self globalsEntryForRow:FLEXGlobalsRowAppKeychainItems],
|
||||
[self globalsEntryForRow:FLEXGlobalsRowPushNotifications],
|
||||
[self globalsEntryForRow:FLEXGlobalsRowApplication],
|
||||
[self globalsEntryForRow:FLEXGlobalsRowAppDelegate],
|
||||
[self globalsEntryForRow:FLEXGlobalsRowKeyWindow],
|
||||
@@ -134,13 +129,17 @@
|
||||
[self globalsEntryForRow:FLEXGlobalsRowCookies],
|
||||
],
|
||||
@(FLEXGlobalsSectionMisc) : @[
|
||||
#if !TARGET_OS_TV
|
||||
[self globalsEntryForRow:FLEXGlobalsRowPasteboard],
|
||||
#endif
|
||||
[self globalsEntryForRow:FLEXGlobalsRowMainScreen],
|
||||
[self globalsEntryForRow:FLEXGlobalsRowCurrentDevice],
|
||||
[self globalsEntryForRow:FLEXGlobalsRowURLSession],
|
||||
[self globalsEntryForRow:FLEXGlobalsRowURLCache],
|
||||
[self globalsEntryForRow:FLEXGlobalsRowNotificationCenter],
|
||||
#if !TARGET_OS_TV
|
||||
[self globalsEntryForRow:FLEXGlobalsRowMenuController],
|
||||
#endif
|
||||
[self globalsEntryForRow:FLEXGlobalsRowFileManager],
|
||||
[self globalsEntryForRow:FLEXGlobalsRowTimeZone],
|
||||
[self globalsEntryForRow:FLEXGlobalsRowLocale],
|
||||
@@ -170,8 +169,9 @@
|
||||
self.title = @"💪 FLEX";
|
||||
self.showsSearchBar = YES;
|
||||
self.searchBarDebounceInterval = kFLEXDebounceInstant;
|
||||
#if !TARGET_OS_TV
|
||||
self.navigationItem.backBarButtonItem = [UIBarButtonItem flex_backItemWithTitle:@"Back"];
|
||||
|
||||
#endif
|
||||
_manuallyDeselectOnAppear = NSProcessInfo.processInfo.operatingSystemVersion.majorVersion < 10;
|
||||
}
|
||||
|
||||
|
||||
@@ -34,9 +34,6 @@ extern NSString *const kFLEXKeychainClassKey;
|
||||
/// Item description.
|
||||
extern NSString *const kFLEXKeychainDescriptionKey;
|
||||
|
||||
/// Item group.
|
||||
extern NSString *const kFLEXKeychainGroupKey;
|
||||
|
||||
/// Item label.
|
||||
extern NSString *const kFLEXKeychainLabelKey;
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ NSString * const kFLEXKeychainAccountKey = @"acct";
|
||||
NSString * const kFLEXKeychainCreatedAtKey = @"cdat";
|
||||
NSString * const kFLEXKeychainClassKey = @"labl";
|
||||
NSString * const kFLEXKeychainDescriptionKey = @"desc";
|
||||
NSString * const kFLEXKeychainGroupKey = @"agrp";
|
||||
NSString * const kFLEXKeychainLabelKey = @"labl";
|
||||
NSString * const kFLEXKeychainLastModifiedKey = @"mdat";
|
||||
NSString * const kFLEXKeychainWhereKey = @"svce";
|
||||
|
||||
@@ -28,26 +28,25 @@
|
||||
if (status == errSecSuccess) {//item already exists, update it!
|
||||
query = [[NSMutableDictionary alloc]init];
|
||||
query[(__bridge id)kSecValueData] = self.passwordData;
|
||||
#if __IPHONE_4_0 && TARGET_OS_IPHONE
|
||||
#if __IPHONE_4_0 && TARGET_OS_IPHONE
|
||||
CFTypeRef accessibilityType = FLEXKeychain.accessibilityType;
|
||||
if (accessibilityType) {
|
||||
query[(__bridge id)kSecAttrAccessible] = (__bridge id)accessibilityType;
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
status = SecItemUpdate((__bridge CFDictionaryRef)(searchQuery), (__bridge CFDictionaryRef)(query));
|
||||
}
|
||||
else if (status == errSecItemNotFound){//item not found, create it!
|
||||
}else if (status == errSecItemNotFound){//item not found, create it!
|
||||
query = [self query];
|
||||
if (self.label) {
|
||||
query[(__bridge id)kSecAttrLabel] = self.label;
|
||||
}
|
||||
query[(__bridge id)kSecValueData] = self.passwordData;
|
||||
#if __IPHONE_4_0 && TARGET_OS_IPHONE
|
||||
#if __IPHONE_4_0 && TARGET_OS_IPHONE
|
||||
CFTypeRef accessibilityType = FLEXKeychain.accessibilityType;
|
||||
if (accessibilityType) {
|
||||
query[(__bridge id)kSecAttrAccessible] = (__bridge id)accessibilityType;
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
status = SecItemAdd((__bridge CFDictionaryRef)query, NULL);
|
||||
}
|
||||
|
||||
@@ -70,9 +69,9 @@
|
||||
}
|
||||
|
||||
NSMutableDictionary *query = [self query];
|
||||
#if TARGET_OS_IPHONE
|
||||
#if TARGET_OS_IPHONE
|
||||
status = SecItemDelete((__bridge CFDictionaryRef)query);
|
||||
#else
|
||||
#else
|
||||
// On Mac OS, SecItemDelete will not delete a key created in a different
|
||||
// app, nor in a different version of the same app.
|
||||
//
|
||||
@@ -89,7 +88,7 @@
|
||||
status = SecKeychainItemDelete((SecKeychainItemRef)result);
|
||||
CFRelease(result);
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
if (status != errSecSuccess && error != NULL) {
|
||||
*error = [self errorWithCode:status];
|
||||
@@ -103,12 +102,12 @@
|
||||
NSMutableDictionary *query = [self query];
|
||||
query[(__bridge id)kSecReturnAttributes] = @YES;
|
||||
query[(__bridge id)kSecMatchLimit] = (__bridge id)kSecMatchLimitAll;
|
||||
#if __IPHONE_4_0 && TARGET_OS_IPHONE
|
||||
#if __IPHONE_4_0 && TARGET_OS_IPHONE
|
||||
CFTypeRef accessibilityType = FLEXKeychain.accessibilityType;
|
||||
if (accessibilityType) {
|
||||
query[(__bridge id)kSecAttrAccessible] = (__bridge id)accessibilityType;
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
CFTypeRef result = NULL;
|
||||
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
|
||||
@@ -150,6 +149,19 @@
|
||||
|
||||
#pragma mark - Accessors
|
||||
|
||||
- (void)setPasswordObject:(id<NSCoding>)object {
|
||||
self.passwordData = [NSKeyedArchiver archivedDataWithRootObject:object];
|
||||
}
|
||||
|
||||
|
||||
- (id<NSCoding>)passwordObject {
|
||||
if (self.passwordData.length) {
|
||||
return [NSKeyedUnarchiver unarchiveObjectWithData:self.passwordData];
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
|
||||
- (void)setPassword:(NSString *)password {
|
||||
self.passwordData = [password dataUsingEncoding:NSUTF8StringEncoding];
|
||||
@@ -169,11 +181,11 @@
|
||||
|
||||
#ifdef FLEXKEYCHAIN_SYNCHRONIZATION_AVAILABLE
|
||||
+ (BOOL)isSynchronizationAvailable {
|
||||
#if TARGET_OS_IPHONE
|
||||
#if TARGET_OS_IPHONE
|
||||
return YES;
|
||||
#else
|
||||
#else
|
||||
return floor(NSFoundationVersionNumber) > NSFoundationVersionNumber10_8_4;
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -192,15 +204,15 @@
|
||||
dictionary[(__bridge id)kSecAttrAccount] = self.account;
|
||||
}
|
||||
|
||||
#ifdef FLEXKEYCHAIN_ACCESS_GROUP_AVAILABLE
|
||||
#if !TARGET_IPHONE_SIMULATOR
|
||||
#ifdef FLEXKEYCHAIN_ACCESS_GROUP_AVAILABLE
|
||||
#if !TARGET_IPHONE_SIMULATOR
|
||||
if (self.accessGroup) {
|
||||
dictionary[(__bridge id)kSecAttrAccessGroup] = self.accessGroup;
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifdef FLEXKEYCHAIN_SYNCHRONIZATION_AVAILABLE
|
||||
#ifdef FLEXKEYCHAIN_SYNCHRONIZATION_AVAILABLE
|
||||
if ([[self class] isSynchronizationAvailable]) {
|
||||
id value;
|
||||
|
||||
@@ -221,7 +233,7 @@
|
||||
|
||||
dictionary[(__bridge id)(kSecAttrSynchronizable)] = value;
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
return dictionary;
|
||||
}
|
||||
@@ -239,7 +251,7 @@
|
||||
case errSecSuccess: return nil;
|
||||
case FLEXKeychainErrorBadArguments: message = NSLocalizedStringFromTableInBundle(@"FLEXKeychainErrorBadArguments", @"FLEXKeychain", resourcesBundle, nil); break;
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
#if TARGET_OS_IPHONE
|
||||
case errSecUnimplemented: {
|
||||
message = NSLocalizedStringFromTableInBundle(@"errSecUnimplemented", @"FLEXKeychain", resourcesBundle, nil);
|
||||
break;
|
||||
@@ -279,10 +291,10 @@
|
||||
default: {
|
||||
message = NSLocalizedStringFromTableInBundle(@"errSecDefault", @"FLEXKeychain", resourcesBundle, nil);
|
||||
}
|
||||
#else
|
||||
#else
|
||||
default:
|
||||
message = (__bridge_transfer NSString *)SecCopyErrorMessageString(code, NULL);
|
||||
#endif
|
||||
#endif
|
||||
}
|
||||
|
||||
NSDictionary *userInfo = message ? @{ NSLocalizedDescriptionKey : message } : nil;
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
id service = item[kFLEXKeychainWhereKey];
|
||||
if ([service isKindOfClass:[NSString class]]) {
|
||||
cell.textLabel.text = service;
|
||||
cell.detailTextLabel.text = [item[kFLEXKeychainAccountKey] description];
|
||||
cell.detailTextLabel.text = item[kFLEXKeychainAccountKey];
|
||||
} else {
|
||||
cell.textLabel.text = [NSString stringWithFormat:
|
||||
@"[%@]\n\n%@",
|
||||
@@ -99,9 +99,8 @@
|
||||
NSDictionary *item = self.section.filteredList[idx];
|
||||
|
||||
FLEXKeychainQuery *query = [FLEXKeychainQuery new];
|
||||
query.service = [item[kFLEXKeychainWhereKey] description];
|
||||
query.account = [item[kFLEXKeychainAccountKey] description];
|
||||
query.accessGroup = [item[kFLEXKeychainGroupKey] description];
|
||||
query.service = item[kFLEXKeychainWhereKey];
|
||||
query.account = item[kFLEXKeychainAccountKey];
|
||||
[query fetch:nil];
|
||||
|
||||
return query;
|
||||
@@ -233,16 +232,21 @@
|
||||
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) {
|
||||
#if !TARGET_OS_TV
|
||||
[UIPasteboard.generalPasteboard flex_copy:query.service];
|
||||
#endif
|
||||
});
|
||||
make.button(@"Copy Account").handler(^(NSArray<NSString *> *strings) {
|
||||
#if !TARGET_OS_TV
|
||||
[UIPasteboard.generalPasteboard flex_copy:query.account];
|
||||
#endif
|
||||
});
|
||||
make.button(@"Copy Password").handler(^(NSArray<NSString *> *strings) {
|
||||
#if !TARGET_OS_TV
|
||||
[UIPasteboard.generalPasteboard flex_copy:query.password];
|
||||
#endif
|
||||
});
|
||||
make.button(@"Dismiss").cancelStyle();
|
||||
|
||||
|
||||
@@ -195,7 +195,7 @@ static inline NSString * TBWildcardMap(NSString *token, NSString *candidate, TBW
|
||||
"/System/Library/PrivateFrameworks/WebKitLegacy.framework/WebKitLegacy",
|
||||
RTLD_LAZY
|
||||
);
|
||||
void (*WebKitInitialize)(void) = dlsym(handle, "WebKitInitialize");
|
||||
void (*WebKitInitialize)() = dlsym(handle, "WebKitInitialize");
|
||||
if (WebKitInitialize) {
|
||||
NSAssert(NSThread.isMainThread,
|
||||
@"WebKitInitialize can only be called on the main thread"
|
||||
|
||||
@@ -33,11 +33,19 @@
|
||||
}
|
||||
|
||||
+ (instancetype)buttonWithTitle:(NSString *)title action:(FLEXKBToolbarAction)eventHandler {
|
||||
#if !TARGET_OS_TV
|
||||
return [self buttonWithTitle:title action:eventHandler forControlEvents:UIControlEventTouchUpInside];
|
||||
#else
|
||||
return [self buttonWithTitle:title action:eventHandler forControlEvents:UIControlEventPrimaryActionTriggered];
|
||||
#endif
|
||||
}
|
||||
|
||||
- (id)initWithTitle:(NSString *)title {
|
||||
#if TARGET_OS_TV
|
||||
self = [FLEXKBToolbarButton buttonWithType:UIButtonTypeSystem];
|
||||
#else
|
||||
self = [super init];
|
||||
#endif
|
||||
if (self) {
|
||||
_title = title;
|
||||
self.layer.shadowOffset = CGSizeMake(0, 1);
|
||||
@@ -51,7 +59,7 @@
|
||||
[self sizeToFit];
|
||||
|
||||
if (@available(iOS 13, *)) {
|
||||
self.appearance = UIKeyboardAppearanceDefault;
|
||||
self.appearance = UIKeyboardTypeDefault;
|
||||
} else {
|
||||
self.appearance = UIKeyboardAppearanceLight;
|
||||
}
|
||||
@@ -84,6 +92,7 @@
|
||||
switch (_appearance) {
|
||||
default:
|
||||
case UIKeyboardAppearanceDefault:
|
||||
#if FLEX_AT_LEAST_IOS13_SDK
|
||||
if (@available(iOS 13, *)) {
|
||||
titleColor = UIColor.labelColor;
|
||||
|
||||
@@ -96,6 +105,7 @@
|
||||
}
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
case UIKeyboardAppearanceLight:
|
||||
titleColor = UIColor.blackColor;
|
||||
backgroundColor = lightColor;
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "FLEXRuntimeBrowserToolbar.h"
|
||||
#import "FLEXMethod.h"
|
||||
|
||||
#import <TargetConditionals.h>
|
||||
@protocol FLEXKeyPathSearchControllerDelegate <UITableViewDataSource>
|
||||
|
||||
@property (nonatomic, readonly) UITableView *tableView;
|
||||
@@ -34,5 +34,7 @@
|
||||
|
||||
- (void)didSelectKeyPathOption:(NSString *)text;
|
||||
- (void)didPressButton:(NSString *)text insertInto:(UISearchBar *)searchBar;
|
||||
|
||||
#if TARGET_OS_TV
|
||||
- (void)basicSearchWithText:(NSString *)text;
|
||||
#endif
|
||||
@end
|
||||
|
||||
@@ -61,7 +61,6 @@
|
||||
searchBar.keyboardType = UIKeyboardTypeWebSearch;
|
||||
searchBar.autocorrectionType = UITextAutocorrectionTypeNo;
|
||||
if (@available(iOS 11, *)) {
|
||||
searchBar.smartQuotesType = UITextSmartQuotesTypeNo;
|
||||
searchBar.smartInsertDeleteType = UITextSmartInsertDeleteTypeNo;
|
||||
}
|
||||
|
||||
@@ -95,6 +94,14 @@
|
||||
return classes;
|
||||
}
|
||||
|
||||
#if TARGET_OS_TV
|
||||
- (void)basicSearchWithText:(NSString *)text {
|
||||
self.keyPath = [FLEXRuntimeKeyPathTokenizer tokenizeString:text];
|
||||
[self searchBar:self.delegate.searchController.searchBar textDidChange:text];
|
||||
//[self updateTable];
|
||||
}
|
||||
#endif
|
||||
|
||||
#pragma mark Key path stuff
|
||||
|
||||
- (void)didSelectKeyPathOption:(NSString *)text {
|
||||
@@ -232,6 +239,7 @@
|
||||
}
|
||||
|
||||
- (BOOL)searchBar:(UISearchBar *)searchBar shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
|
||||
LOG_SELF;
|
||||
// Check if character is even legal
|
||||
if (![FLEXRuntimeKeyPathTokenizer allowedInKeyPath:text]) {
|
||||
return NO;
|
||||
@@ -258,6 +266,7 @@
|
||||
}
|
||||
|
||||
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText {
|
||||
LOG_SELF;
|
||||
[_timer invalidate];
|
||||
|
||||
// Schedule update timer
|
||||
@@ -311,7 +320,11 @@
|
||||
];
|
||||
|
||||
if (self.bundlesOrClasses.count) {
|
||||
#if !TARGET_OS_TV
|
||||
cell.accessoryType = UITableViewCellAccessoryDetailButton;
|
||||
#else
|
||||
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
|
||||
#endif
|
||||
cell.textLabel.text = self.bundlesOrClasses[indexPath.row];
|
||||
cell.detailTextLabel.text = nil;
|
||||
if (self.keyPath.classKey) {
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
||||
|
||||
if (@available(iOS 13, *)) {
|
||||
self.appearance = UIKeyboardAppearanceDefault;
|
||||
self.appearance = UIKeyboardTypeDefault;
|
||||
} else {
|
||||
self.appearance = UIKeyboardAppearanceLight;
|
||||
}
|
||||
@@ -106,9 +106,11 @@
|
||||
|
||||
switch (_appearance) {
|
||||
case UIKeyboardAppearanceDefault:
|
||||
#if FLEX_AT_LEAST_IOS13_SDK
|
||||
if (@available(iOS 13, *)) {
|
||||
#if !TARGET_OS_TV
|
||||
borderColor = UIColor.systemBackgroundColor;
|
||||
|
||||
#endif
|
||||
if (self.usingDarkMode) {
|
||||
// style = UIBlurEffectStyleSystemThickMaterial;
|
||||
backgroundColor = darkColor;
|
||||
@@ -118,6 +120,7 @@
|
||||
}
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
case UIKeyboardAppearanceLight: {
|
||||
borderColor = UIColor.clearColor;
|
||||
backgroundColor = lightColor;
|
||||
|
||||
@@ -10,12 +10,11 @@
|
||||
#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>
|
||||
#import "FLEXTV.h"
|
||||
|
||||
@interface FLEXObjcRuntimeViewController () <FLEXKeyPathSearchControllerDelegate>
|
||||
|
||||
@@ -45,12 +44,9 @@
|
||||
]
|
||||
];
|
||||
|
||||
[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.
|
||||
//
|
||||
@@ -64,12 +60,19 @@
|
||||
FLEXKeyPathSearchController *keyPathController = [FLEXKeyPathSearchController delegate:self];
|
||||
_keyPathController = keyPathController;
|
||||
_keyPathController.toolbar = [FLEXRuntimeBrowserToolbar toolbarWithHandler:^(NSString *text, BOOL suggestion) {
|
||||
LOG_SELF;
|
||||
if (suggestion) {
|
||||
[keyPathController didSelectKeyPathOption:text];
|
||||
} else {
|
||||
[keyPathController didPressButton:text insertInto:searchBar];
|
||||
}
|
||||
} suggestions:keyPathController.suggestions];
|
||||
#if TARGET_OS_TV
|
||||
KBSearchButton * searchButton = (KBSearchButton*)[self.navigationItem leftBarButtonItem].customView;
|
||||
if ([searchButton respondsToSelector:@selector(keyPathController)]){
|
||||
[searchButton setKeyPathController:keyPathController];
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated {
|
||||
@@ -77,66 +80,13 @@
|
||||
[self.tableView deselectRowAtIndexPath:self.tableView.indexPathForSelectedRow animated:YES];
|
||||
}
|
||||
|
||||
|
||||
#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];
|
||||
}
|
||||
|
||||
if (!dlopen(path.UTF8String, RTLD_NOW)) {
|
||||
[FLEXAlert makeAlert:^(FLEXAlert *make) {
|
||||
make.title(@"Error").message(@(dlerror()));
|
||||
make.button(@"Dismiss").cancelStyle();
|
||||
}];
|
||||
}
|
||||
});
|
||||
} showFrom:self];
|
||||
}
|
||||
|
||||
- (void)dlopenInvalidPath {
|
||||
[FLEXAlert makeAlert:^(FLEXAlert * _Nonnull make) {
|
||||
make.title(@"Path or Name Too Short");
|
||||
make.button(@"Dismiss").cancelStyle();
|
||||
} showFrom:self];
|
||||
- (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];
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -149,7 +99,9 @@
|
||||
make.message(path);
|
||||
|
||||
make.button(@"Copy Path").handler(^(NSArray<NSString *> *strings) {
|
||||
#if !TARGET_OS_TV
|
||||
UIPasteboard.generalPasteboard.string = path;
|
||||
#endif
|
||||
});
|
||||
make.button(@"Dismiss").cancelStyle();
|
||||
} showFrom:self];
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
*
|
||||
* See <os/object.h> for details.
|
||||
*/
|
||||
#if !TARGET_OS_MACCATALYST && !__has_include(<xpc/xpc.h>)
|
||||
#if !TARGET_OS_MACCATALYST
|
||||
#if OS_OBJECT_USE_OBJC
|
||||
OS_OBJECT_DECL(xpc_object);
|
||||
#else
|
||||
|
||||
@@ -168,10 +168,11 @@ 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
|
||||
// https://github.com/FLEXTool/FLEX/issues/564
|
||||
const char *messageText = OSLogCopyFormattedMessage(log_message) ?: "";
|
||||
|
||||
if (entry->log_message.format && !(strcmp(entry->log_message.format, messageText))) {
|
||||
messageText = (char *)entry->log_message.format;
|
||||
}
|
||||
// move messageText from stack to heap
|
||||
NSString *msg = [NSString stringWithUTF8String:messageText];
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
#import "FLEXSystemLogCell.h"
|
||||
#import "FLEXSystemLogMessage.h"
|
||||
#import "UIFont+FLEX.h"
|
||||
#import "NSDateFormatter+FLEX.h"
|
||||
|
||||
NSString *const kFLEXSystemLogCellIdentifier = @"FLEXSystemLogCellIdentifier";
|
||||
|
||||
@@ -27,8 +26,10 @@ NSString *const kFLEXSystemLogCellIdentifier = @"FLEXSystemLogCellIdentifier";
|
||||
|
||||
self.logMessageLabel = [UILabel new];
|
||||
self.logMessageLabel.numberOfLines = 0;
|
||||
#if !TARGET_OS_TV
|
||||
self.separatorInset = UIEdgeInsetsZero;
|
||||
self.selectionStyle = UITableViewCellSelectionStyleNone;
|
||||
#endif
|
||||
[self.contentView addSubview:self.logMessageLabel];
|
||||
}
|
||||
|
||||
@@ -107,7 +108,14 @@ static const UIEdgeInsets kFLEXLogMessageCellInsets = {10.0, 10.0, 10.0, 10.0};
|
||||
}
|
||||
|
||||
+ (NSString *)logTimeStringFromDate:(NSDate *)date {
|
||||
return [NSDateFormatter flex_stringFrom:date format:FLEXDateFormatVerbose];
|
||||
static NSDateFormatter *formatter = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
formatter = [NSDateFormatter new];
|
||||
formatter.dateFormat = @"yyyy-MM-dd HH:mm:ss.SSS";
|
||||
});
|
||||
|
||||
return [formatter stringFromDate:date];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -98,11 +98,12 @@ static BOOL my_os_log_shim_enabled(void *addr) {
|
||||
[super viewDidLoad];
|
||||
|
||||
self.showsSearchBar = YES;
|
||||
self.pinSearchBar = YES;
|
||||
self.showSearchBarInitially = NO;
|
||||
|
||||
weakify(self)
|
||||
id logHandler = ^(NSArray<FLEXSystemLogMessage *> *newMessages) { strongify(self)
|
||||
[self handleUpdateWithNewMessages:newMessages];
|
||||
__weak __typeof(self) weakSelf = self;
|
||||
id logHandler = ^(NSArray<FLEXSystemLogMessage *> *newMessages) {
|
||||
__strong __typeof(weakSelf) strongSelf = weakSelf;
|
||||
[strongSelf handleUpdateWithNewMessages:newMessages];
|
||||
};
|
||||
|
||||
if (FLEXOSLogAvailable() && !FLEXNSLogHookWorks) {
|
||||
@@ -110,8 +111,9 @@ static BOOL my_os_log_shim_enabled(void *addr) {
|
||||
} else {
|
||||
_logController = [FLEXASLLogController withUpdateHandler:logHandler];
|
||||
}
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
|
||||
#endif
|
||||
self.title = @"Waiting for Logs...";
|
||||
|
||||
// Toolbar buttons //
|
||||
@@ -136,18 +138,20 @@ static BOOL my_os_log_shim_enabled(void *addr) {
|
||||
[self.logController startMonitoring];
|
||||
}
|
||||
|
||||
- (NSArray<FLEXTableViewSection *> *)makeSections { weakify(self)
|
||||
- (NSArray<FLEXTableViewSection *> *)makeSections {
|
||||
__weak __typeof(self) weakSelf = self;
|
||||
_logMessages = [FLEXMutableListSection list:@[]
|
||||
cellConfiguration:^(FLEXSystemLogCell *cell, FLEXSystemLogMessage *message, NSInteger row) {
|
||||
strongify(self)
|
||||
|
||||
cell.logMessage = message;
|
||||
cell.highlightedText = self.filterText;
|
||||
__strong __typeof(self) strongSelf = weakSelf;
|
||||
if (strongSelf) {
|
||||
cell.logMessage = message;
|
||||
cell.highlightedText = strongSelf.filterText;
|
||||
|
||||
if (row % 2 == 0) {
|
||||
cell.backgroundColor = FLEXColor.primaryBackgroundColor;
|
||||
} else {
|
||||
cell.backgroundColor = FLEXColor.secondaryBackgroundColor;
|
||||
if (row % 2 == 0) {
|
||||
cell.backgroundColor = FLEXColor.primaryBackgroundColor;
|
||||
} else {
|
||||
cell.backgroundColor = FLEXColor.secondaryBackgroundColor;
|
||||
}
|
||||
}
|
||||
} filterMatcher:^BOOL(NSString *filterText, FLEXSystemLogMessage *message) {
|
||||
NSString *displayedText = [FLEXSystemLogCell displayedTextForLogMessage:message];
|
||||
@@ -175,15 +179,9 @@ static BOOL my_os_log_shim_enabled(void *addr) {
|
||||
[self.logMessages mutate:^(NSMutableArray *list) {
|
||||
[list addObjectsFromArray:newMessages];
|
||||
}];
|
||||
|
||||
// Re-filter messages to filter against new messages
|
||||
if (self.filterText.length) {
|
||||
[self updateSearchResults:self.filterText];
|
||||
}
|
||||
|
||||
// "Follow" the log as new messages stream in if we were previously near the bottom.
|
||||
UITableView *tv = self.tableView;
|
||||
BOOL wasNearBottom = tv.contentOffset.y >= tv.contentSize.height - tv.frame.size.height - 100.0;
|
||||
BOOL wasNearBottom = self.tableView.contentOffset.y >= self.tableView.contentSize.height - self.tableView.frame.size.height - 100.0;
|
||||
[self reloadData];
|
||||
if (wasNearBottom) {
|
||||
[self scrollToLastRow];
|
||||
@@ -193,8 +191,8 @@ static BOOL my_os_log_shim_enabled(void *addr) {
|
||||
- (void)scrollToLastRow {
|
||||
NSInteger numberOfRows = [self.tableView numberOfRowsInSection:0];
|
||||
if (numberOfRows > 0) {
|
||||
NSIndexPath *last = [NSIndexPath indexPathForRow:numberOfRows - 1 inSection:0];
|
||||
[self.tableView scrollToRowAtIndexPath:last atScrollPosition:UITableViewScrollPositionBottom animated:YES];
|
||||
NSIndexPath *lastIndexPath = [NSIndexPath indexPathForRow:numberOfRows - 1 inSection:0];
|
||||
[self.tableView scrollToRowAtIndexPath:lastIndexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,26 +266,32 @@ 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 ?: @"";
|
||||
#if !TARGET_OS_TV
|
||||
UIPasteboard.generalPasteboard.string = self.logMessages.filteredList[indexPath.row].messageText;
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
#if FLEX_AT_LEAST_IOS13_SDK
|
||||
#if !TARGET_OS_TV
|
||||
- (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]];
|
||||
}
|
||||
];
|
||||
__weak __typeof__(self) weakSelf = self;
|
||||
return [UIContextMenuConfiguration configurationWithIdentifier:nil
|
||||
previewProvider:nil
|
||||
actionProvider:^UIMenu *(NSArray<UIMenuElement *> *suggestedActions) {
|
||||
UIAction *copy = [UIAction actionWithTitle:@"Copy"
|
||||
image:nil
|
||||
identifier:@"Copy"
|
||||
handler:^(__kindof UIAction *action) {
|
||||
// We usually only want to copy the log message itself, not any metadata associated with it.
|
||||
UIPasteboard.generalPasteboard.string = weakSelf.logMessages.filteredList[indexPath.row].messageText ?: @"";
|
||||
}];
|
||||
return [UIMenu menuWithTitle:@"" image:nil identifier:nil options:UIMenuOptionsDisplayInline children:@[copy]];
|
||||
}];
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
@end
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
../../Classes/Utility/Categories/CALayer+FLEX.h
|
||||
@@ -1 +0,0 @@
|
||||
../../Classes/FLEX-Categories.h
|
||||
@@ -1 +0,0 @@
|
||||
../../Classes/FLEX-Core.h
|
||||
@@ -1 +0,0 @@
|
||||
../../Classes/FLEX-ObjectExploring.h
|
||||
@@ -1 +0,0 @@
|
||||
../../Classes/FLEX-Runtime.h
|
||||
@@ -1 +0,0 @@
|
||||
../../Classes/FLEX.h
|
||||
@@ -1 +0,0 @@
|
||||
../../Classes/Utility/FLEXAlert.h
|
||||
@@ -1 +0,0 @@
|
||||
../../Classes/Utility/Runtime/Objc/Reflection/FLEXBlockDescription.h
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user