Compare commits

..

15 Commits

Author SHA1 Message Date
Kevin Bradley 691ca1132e The scope carousel works properly on tvOS now, updated KBDatePicker to the latest 2021-01-10 00:28:05 -07:00
Kevin Bradley 5e005db93b obfuscate a few more private API calls 2021-01-09 23:36:13 -07:00
Kevin Bradley 23fc5ea533 Added support to explorer toolbar gesture recognizers to work on tvOS as applicable. Obscure a private API call, fixed runtime browser issues on tvOS. 2021-01-09 23:27:13 -07:00
Kevin Bradley 09300aa62e Added rudimentary support for bookmarks and tabs. 2021-01-08 01:19:14 -07:00
Kevin Bradley 7c5b2308f0 Make our switch stand out more with different background colors when off, tested with both light and dark mode 2021-01-07 13:24:06 -07:00
Kevin Bradley b0a9cd2707 Some aesthetic improvements to the UIFLEXSwitch class 2021-01-07 13:24:06 -07:00
Kevin Bradley b921bde7fb Class rename (fake->FLEXTV) and all the changes the were necessary because of that, seems like less of a stand-in name 2021-01-07 13:24:06 -07:00
Kevin Bradley 8b65f2a8a7 Clean up warnings and fixed a missing macro for the toolbar layout 2021-01-07 13:24:06 -07:00
Kevin Bradley 59d22e2c01 Found a missing file and fixed an iOS build issue. 2021-01-07 13:23:58 -07:00
Kevin Bradley 6f7969ab18 Fixed a crashing bug where it was allowing long/right clicks on accessories that didnt suport them, improved navigation selection index choices between view navigation. Bumped to latest release 4.3.0-6. 2021-01-07 13:23:44 -07:00
Kevin Bradley 1d9d6e80e3 Fix issue building for iOS. 2021-01-07 13:23:37 -07:00
Kevin Bradley 56e5d7ddf9 Making sure things build in Xcode 12 as well, this change should help. 2021-01-07 13:23:27 -07:00
Kevin Bradley 8542a74fb9 Removed duplicate macros. 2021-01-07 13:23:20 -07:00
Kevin Bradley 1a8759615a Added a few things I missed in the previous commit. 2021-01-07 13:23:09 -07:00
Kevin Bradley 8300a30ca3 Squashed all of the tvOS commits into one large commit based off a new branch. This should hopefully cover your ask in the related issue. I honestly had no desire to keep that huge and mostly useless commit history anyway. 2021-01-07 13:22:38 -07:00
327 changed files with 7294 additions and 7559 deletions
-3
View File
@@ -1,3 +0,0 @@
# These are supported funding model platforms
github: [NSExceptional]
+5 -2
View File
@@ -20,5 +20,8 @@ DerivedData
/Example/Pods
Podfile.lock
IDEWorkspaceChecks.plist
*.xcworkspace
.build
#tvOS specific
libflex_*
flexinjected/packages/*
layout/Library/Frameworks/*.framework
-5
View File
@@ -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
+4 -8
View File
@@ -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
-2
View File
@@ -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;
+3
View File
@@ -7,6 +7,7 @@
//
#import <UIKit/UIKit.h>
#import "FLEXMacros.h"
#import "NSArray+FLEX.h"
@class FLEXTableView;
@@ -100,6 +101,7 @@ NS_ASSUME_NONNULL_BEGIN
- (nullable void(^)(__kindof UIViewController *host))didPressInfoButtonAction:(NSInteger)row;
#pragma mark - Context Menus
#if FLEX_AT_LEAST_IOS13_SDK
/// By default, this is the title of the row.
/// @return The title of the context menu, if any.
@@ -119,6 +121,7 @@ NS_ASSUME_NONNULL_BEGIN
/// should be a description of what will be copied, and the values should be
/// the strings to copy. Return an empty string as a value to show a disabled action.
- (nullable NSArray<NSString *> *)copyMenuItemsForRow:(NSInteger)row API_AVAILABLE(ios(13.0));
#endif
#pragma mark - Cell Configuration
+7 -1
View File
@@ -64,6 +64,8 @@
return kFLEXDefaultCell;
}
#if FLEX_AT_LEAST_IOS13_SDK
- (NSString *)menuTitleForRow:(NSInteger)row {
NSString *title = [self titleForRow:row];
NSString *subtitle = [self menuSubtitleForRow:row];
@@ -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
+2 -1
View File
@@ -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;
+5
View File
@@ -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,
+16
View File
@@ -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;
+1 -2
View File
@@ -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
+68 -21
View File
@@ -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 {
+9 -2
View File
@@ -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;
+2 -12
View File
@@ -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
View File
@@ -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
View File
@@ -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>
+19 -11
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+4 -18
View File
@@ -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
+11 -53
View File
@@ -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
View File
@@ -1 +0,0 @@
../../Classes/Utility/Categories/CALayer+FLEX.h
-1
View File
@@ -1 +0,0 @@
../../Classes/FLEX-Categories.h
-1
View File
@@ -1 +0,0 @@
../../Classes/FLEX-Core.h
-1
View File
@@ -1 +0,0 @@
../../Classes/FLEX-ObjectExploring.h
-1
View File
@@ -1 +0,0 @@
../../Classes/FLEX-Runtime.h
-1
View File
@@ -1 +0,0 @@
../../Classes/FLEX.h
-1
View File
@@ -1 +0,0 @@
../../Classes/Utility/FLEXAlert.h
-1
View File
@@ -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