Compare commits
96 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 23c7cfbe6e | |||
| b45750eb1b | |||
| 550c9c1120 | |||
| d5dfb23cf2 | |||
| 90ee4f8f38 | |||
| c037e703a6 | |||
| 6bf746d4aa | |||
| ca005fb4d0 | |||
| 5f2bac9c2f | |||
| dc0e7dc7d3 | |||
| 740daab55b | |||
| 1953089653 | |||
| 25e0af042e | |||
| 0265334976 | |||
| 6aa9ec9ec1 | |||
| 459aa9b6f5 | |||
| 06655dde6a | |||
| 5c153b0c89 | |||
| 29dd970ea7 | |||
| f6701f8ec9 | |||
| 7f119ba0cc | |||
| e7e5115fc0 | |||
| 0e0bd5a890 | |||
| 9cc2470901 | |||
| b07da3e11d | |||
| 36e0e9fb1e | |||
| 0e85162c11 | |||
| 7329fd9272 | |||
| 30fb8f077a | |||
| f2f66489d1 | |||
| 3cb1366966 | |||
| 907b315601 | |||
| 877a1db87b | |||
| 5b6b50bf6a | |||
| 6c8fbbeaa8 | |||
| da67902cf5 | |||
| 8533689ca7 | |||
| 8f85b22866 | |||
| fa8a4d61ea | |||
| 89010395de | |||
| 5b969b6438 | |||
| 2dee6901f7 | |||
| fced419509 | |||
| 09ff0482df | |||
| d9986d879b | |||
| 15d7d07809 | |||
| a556ece626 | |||
| 35ce037288 | |||
| a32b4074f8 | |||
| c3da7e10f7 | |||
| 135c8d05c1 | |||
| 8afd1a1975 | |||
| dcdd638719 | |||
| c661d491a5 | |||
| 82f104d682 | |||
| f9e42aed74 | |||
| c8343500af | |||
| 5a96ed6af5 | |||
| 84cdc6a8e4 | |||
| 2b6ccb23e4 | |||
| be02b89d9b | |||
| b4f07a0f92 | |||
| 2093913b17 | |||
| 7705dac42b | |||
| d23f01dd87 | |||
| ec2b73ceda | |||
| b8f226ce45 | |||
| b232ddb075 | |||
| 87a821903d | |||
| 9645811baa | |||
| 2e2a550b1f | |||
| 98dff514e8 | |||
| 9332a87d98 | |||
| d031dab174 | |||
| 7ddb46ff9f | |||
| 4a4a08df16 | |||
| 4830daeab3 | |||
| dd05a6652c | |||
| e78947260d | |||
| 5dd856070e | |||
| 2e868eba39 | |||
| 9ba80a53cf | |||
| 1780968f50 | |||
| 9abd9cc933 | |||
| 18f20fbf3b | |||
| 3e6d18dd8c | |||
| 99d2ddd001 | |||
| 1647f7ab9f | |||
| 5ae64e17e6 | |||
| c5d4e959cd | |||
| c056a29375 | |||
| 02409d8051 | |||
| 805434239a | |||
| 179c968443 | |||
| cfc68017e0 | |||
| 2300d68321 |
@@ -17,3 +17,6 @@ DerivedData
|
||||
*.ipa
|
||||
*.xcuserstate
|
||||
.DS_Store
|
||||
/Example/Pods
|
||||
Podfile.lock
|
||||
IDEWorkspaceChecks.plist
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
//
|
||||
// FLEXFilteringTableViewController.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 3/9/20.
|
||||
// Copyright © 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXTableViewController.h"
|
||||
|
||||
#pragma mark - FLEXTableViewFiltering
|
||||
@protocol FLEXTableViewFiltering <FLEXSearchResultsUpdating>
|
||||
|
||||
/// An array of visible, "filtered" sections. For example,
|
||||
/// if you have 3 sections in \c allSections and the user searches
|
||||
/// for something that matches rows in only one section, then
|
||||
/// this property would only contain that on matching section.
|
||||
@property (nonatomic, copy) NSArray<FLEXTableViewSection *> *sections;
|
||||
/// An array of all possible sections. Empty sections are to be removed
|
||||
/// and the resulting array stored in the \c section property. Setting
|
||||
/// this property should immediately set \c sections to \c nonemptySections
|
||||
///
|
||||
/// Do not manually initialize this property, it will be
|
||||
/// initialized for you using the result of \c makeSections.
|
||||
@property (nonatomic, copy) NSArray<FLEXTableViewSection *> *allSections;
|
||||
|
||||
/// This computed property should filter \c allSections for assignment to \c sections
|
||||
@property (nonatomic, readonly) NSArray<FLEXTableViewSection *> *nonemptySections;
|
||||
|
||||
/// This should be able to re-initialize \c allSections
|
||||
- (NSArray<FLEXTableViewSection *> *)makeSections;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
#pragma mark - FLEXFilteringTableViewController
|
||||
/// A table view which implements \c UITableView* methods using arrays of
|
||||
/// \c FLEXTableViewSection objects provied by a special delegate.
|
||||
@interface FLEXFilteringTableViewController : FLEXTableViewController <FLEXTableViewFiltering>
|
||||
|
||||
/// Stores the current search query.
|
||||
@property (nonatomic, copy) NSString *filterText;
|
||||
|
||||
/// This property is set to \c self by default.
|
||||
///
|
||||
/// This property is used to power almost all of the table view's data source
|
||||
/// and delegate methods automatically, including row and section filtering
|
||||
/// when the user searches, 3D Touch context menus, row selection, etc.
|
||||
///
|
||||
/// Setting this property will also set \c searchDelegate to that object.
|
||||
@property (nonatomic, weak) id<FLEXTableViewFiltering> filterDelegate;
|
||||
|
||||
/// Defaults to \c NO. If enabled, all filtering will be done by calling
|
||||
/// \c onBackgroundQueue:thenOnMainQueue: with the UI updated on the main queue.
|
||||
@property (nonatomic) BOOL filterInBackground;
|
||||
|
||||
/// Defaults to \c NO. If enabled, one • will be supplied as an index title for each section.
|
||||
@property (nonatomic) BOOL wantsSectionIndexTitles;
|
||||
|
||||
/// Recalculates the non-empty sections and reloads the table view.
|
||||
///
|
||||
/// Subclasses may override to perform additional reloading logic,
|
||||
/// such as calling \c -reloadSections if needed. Be sure to call
|
||||
/// \c super after any logic that would affect the appearance of
|
||||
/// the table view, since the table view is reloaded last.
|
||||
///
|
||||
/// Called at the end of this class's implementation of \c updateSearchResults:
|
||||
- (void)reloadData;
|
||||
|
||||
/// Invoke this method to call \c -reloadData on each section
|
||||
/// in \c self.filterDelegate.allSections.
|
||||
- (void)reloadSections;
|
||||
|
||||
#pragma mark FLEXTableViewFiltering
|
||||
|
||||
@property (nonatomic, copy) NSArray<FLEXTableViewSection *> *sections;
|
||||
@property (nonatomic, copy) NSArray<FLEXTableViewSection *> *allSections;
|
||||
|
||||
/// Subclasses can override to hide specific sections under certain conditions
|
||||
/// if using \c self as the \c filterDelegate, as is the default.
|
||||
///
|
||||
/// For example, the object explorer hides the description section when searching.
|
||||
@property (nonatomic, readonly) NSArray<FLEXTableViewSection *> *nonemptySections;
|
||||
|
||||
/// If using \c self as the \c filterDelegate, as is the default,
|
||||
/// subclasses should override to provide the sections for the table view.
|
||||
- (NSArray<FLEXTableViewSection *> *)makeSections;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,203 @@
|
||||
//
|
||||
// FLEXFilteringTableViewController.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 3/9/20.
|
||||
// Copyright © 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXFilteringTableViewController.h"
|
||||
#import "FLEXTableViewSection.h"
|
||||
#import "NSArray+Functional.h"
|
||||
|
||||
@interface FLEXFilteringTableViewController ()
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXFilteringTableViewController
|
||||
@synthesize allSections = _allSections;
|
||||
|
||||
#pragma mark - View controller lifecycle
|
||||
|
||||
- (void)loadView {
|
||||
[super loadView];
|
||||
|
||||
if (!self.filterDelegate) {
|
||||
self.filterDelegate = self;
|
||||
} else {
|
||||
[self _registerCellsForReuse];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)_registerCellsForReuse {
|
||||
for (FLEXTableViewSection *section in self.filterDelegate.allSections) {
|
||||
if (section.cellRegistrationMapping) {
|
||||
[self.tableView registerCells:section.cellRegistrationMapping];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
- (void)setFilterDelegate:(id<FLEXTableViewFiltering>)filterDelegate {
|
||||
_filterDelegate = filterDelegate;
|
||||
filterDelegate.allSections = [filterDelegate makeSections];
|
||||
|
||||
if (self.isViewLoaded) {
|
||||
[self _registerCellsForReuse];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)reloadData {
|
||||
[self reloadData:self.nonemptySections];
|
||||
}
|
||||
|
||||
- (void)reloadData:(NSArray *)nonemptySections {
|
||||
// Recalculate displayed sections
|
||||
self.filterDelegate.sections = nonemptySections;
|
||||
|
||||
// Refresh table view
|
||||
if (self.isViewLoaded) {
|
||||
[self.tableView reloadData];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)reloadSections {
|
||||
for (FLEXTableViewSection *section in self.filterDelegate.allSections) {
|
||||
[section reloadData];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Search
|
||||
|
||||
- (void)updateSearchResults:(NSString *)newText {
|
||||
NSArray *(^filter)() = ^NSArray *{
|
||||
self.filterText = newText;
|
||||
|
||||
// Sections will adjust data based on this property
|
||||
for (FLEXTableViewSection *section in self.filterDelegate.allSections) {
|
||||
section.filterText = newText;
|
||||
}
|
||||
|
||||
return nil;
|
||||
};
|
||||
|
||||
if (self.filterInBackground) {
|
||||
[self onBackgroundQueue:filter thenOnMainQueue:^(NSArray *unused) {
|
||||
if ([self.searchText isEqualToString:newText]) {
|
||||
[self reloadData];
|
||||
}
|
||||
}];
|
||||
} else {
|
||||
filter();
|
||||
[self reloadData];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Filtering
|
||||
|
||||
- (NSArray<FLEXTableViewSection *> *)nonemptySections {
|
||||
return [self.filterDelegate.allSections flex_filtered:^BOOL(FLEXTableViewSection *section, NSUInteger idx) {
|
||||
return section.numberOfRows > 0;
|
||||
}];
|
||||
}
|
||||
|
||||
- (NSArray<FLEXTableViewSection *> *)makeSections {
|
||||
return @[];
|
||||
}
|
||||
|
||||
- (void)setAllSections:(NSArray<FLEXTableViewSection *> *)allSections {
|
||||
_allSections = allSections.copy;
|
||||
self.sections = self.nonemptySections;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - UITableViewDataSource
|
||||
|
||||
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
|
||||
return self.filterDelegate.sections.count;
|
||||
}
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
|
||||
return self.filterDelegate.sections[section].numberOfRows;
|
||||
}
|
||||
|
||||
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
|
||||
return self.filterDelegate.sections[section].title;
|
||||
}
|
||||
|
||||
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
NSString *reuse = [self.filterDelegate.sections[indexPath.section] reuseIdentifierForRow:indexPath.row];
|
||||
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:reuse forIndexPath:indexPath];
|
||||
[self.filterDelegate.sections[indexPath.section] configureCell:cell forRow:indexPath.row];
|
||||
return cell;
|
||||
}
|
||||
|
||||
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
return UITableViewAutomaticDimension;
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)sectionIndexTitlesForTableView:(UITableView *)tableView {
|
||||
if (self.wantsSectionIndexTitles) {
|
||||
return [NSArray flex_forEachUpTo:self.filterDelegate.sections.count map:^id(NSUInteger i) {
|
||||
return @"⦁";
|
||||
}];
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - UITableViewDelegate
|
||||
|
||||
- (BOOL)tableView:(UITableView *)tableView shouldHighlightRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
return [self.filterDelegate.sections[indexPath.section] canSelectRow:indexPath.row];
|
||||
}
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
FLEXTableViewSection *section = self.filterDelegate.sections[indexPath.section];
|
||||
|
||||
void (^action)(UIViewController *) = [section didSelectRowAction:indexPath.row];
|
||||
UIViewController *details = [section viewControllerToPushForRow:indexPath.row];
|
||||
|
||||
if (action) {
|
||||
action(self);
|
||||
[tableView deselectRowAtIndexPath:indexPath animated:YES];
|
||||
} else if (details) {
|
||||
[self.navigationController pushViewController:details animated:YES];
|
||||
} else {
|
||||
[NSException raise:NSInternalInconsistencyException
|
||||
format:@"Row is selectable but has no action or view controller"];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)tableView:(UITableView *)tableView accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)indexPath {
|
||||
[self.filterDelegate.sections[indexPath.section] didPressInfoButtonAction:indexPath.row](self);
|
||||
}
|
||||
|
||||
#if FLEX_AT_LEAST_IOS13_SDK
|
||||
|
||||
- (UIContextMenuConfiguration *)tableView:(UITableView *)tableView contextMenuConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath point:(CGPoint)point __IOS_AVAILABLE(13.0) {
|
||||
FLEXTableViewSection *section = self.filterDelegate.sections[indexPath.section];
|
||||
NSString *title = [section menuTitleForRow:indexPath.row];
|
||||
NSArray<UIMenuElement *> *menuItems = [section menuItemsForRow:indexPath.row sender:self];
|
||||
|
||||
if (menuItems.count) {
|
||||
return [UIContextMenuConfiguration
|
||||
configurationWithIdentifier:nil
|
||||
previewProvider:nil
|
||||
actionProvider:^UIMenu *(NSArray<UIMenuElement *> *suggestedActions) {
|
||||
return [UIMenu menuWithTitle:title children:menuItems];
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
@end
|
||||
@@ -7,6 +7,8 @@
|
||||
//
|
||||
|
||||
#import "FLEXNavigationController.h"
|
||||
#import "FLEXExplorerViewController.h"
|
||||
#import "FLEXTabList.h"
|
||||
|
||||
@interface UINavigationController (Private) <UIGestureRecognizerDelegate>
|
||||
- (void)_gestureRecognizedInteractiveHide:(UIGestureRecognizer *)sender;
|
||||
@@ -18,6 +20,8 @@
|
||||
@interface FLEXNavigationController ()
|
||||
@property (nonatomic, readonly) BOOL toolbarWasHidden;
|
||||
@property (nonatomic) BOOL waitingToAddTab;
|
||||
@property (nonatomic) BOOL didSetupPendingDismissButtons;
|
||||
@property (nonatomic) UISwipeGestureRecognizer *navigationBarSwipeGesture;
|
||||
@end
|
||||
|
||||
@implementation FLEXNavigationController
|
||||
@@ -30,6 +34,130 @@
|
||||
[super viewDidLoad];
|
||||
|
||||
self.waitingToAddTab = YES;
|
||||
|
||||
// Add gesture to reveal toolbar if hidden
|
||||
self.navigationBar.userInteractionEnabled = YES;
|
||||
[self.navigationBar addGestureRecognizer:[[UITapGestureRecognizer alloc]
|
||||
initWithTarget:self action:@selector(handleNavigationBarTap:)
|
||||
]];
|
||||
|
||||
// Add gesture to dismiss if not presented with a sheet style
|
||||
if (@available(iOS 13, *)) {
|
||||
switch (self.modalPresentationStyle) {
|
||||
case UIModalPresentationAutomatic:
|
||||
case UIModalPresentationPageSheet:
|
||||
case UIModalPresentationFormSheet:
|
||||
break;
|
||||
|
||||
default:
|
||||
[self addNavigationBarSwipeGesture];
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
[self addNavigationBarSwipeGesture];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated {
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
if (self.beingPresented && !self.didSetupPendingDismissButtons) {
|
||||
for (UIViewController *vc in self.viewControllers) {
|
||||
[self addNavigationBarItemsToViewController:vc.navigationItem];
|
||||
}
|
||||
|
||||
self.didSetupPendingDismissButtons = YES;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated {
|
||||
[super viewDidAppear:animated];
|
||||
|
||||
if (self.waitingToAddTab) {
|
||||
// Only add new tab if we're presented properly
|
||||
if ([self.presentingViewController isKindOfClass:[FLEXExplorerViewController class]]) {
|
||||
// New navigation controllers always add themselves as new tabs,
|
||||
// tabs are closed by FLEXExplorerViewController
|
||||
[FLEXTabList.sharedList addTab:self];
|
||||
self.waitingToAddTab = NO;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)pushViewController:(UIViewController *)viewController animated:(BOOL)animated {
|
||||
[super pushViewController:viewController animated:animated];
|
||||
[self addNavigationBarItemsToViewController:viewController.navigationItem];
|
||||
}
|
||||
|
||||
- (void)dismissAnimated {
|
||||
// Tabs are only closed if the done button is pressed; this
|
||||
// allows you to leave a tab open by dragging down to dismiss
|
||||
[FLEXTabList.sharedList closeTab:self];
|
||||
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (void)addNavigationBarItemsToViewController:(UINavigationItem *)navigationItem {
|
||||
if (!self.presentingViewController) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if a done item already exists
|
||||
for (UIBarButtonItem *item in navigationItem.rightBarButtonItems) {
|
||||
if (item.style == UIBarButtonItemStyleDone) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Give root view controllers a Done button if it does not already have one
|
||||
UIBarButtonItem *done = [[UIBarButtonItem alloc]
|
||||
initWithBarButtonSystemItem:UIBarButtonSystemItemDone
|
||||
target:self
|
||||
action:@selector(dismissAnimated)
|
||||
];
|
||||
|
||||
// Prepend the button if other buttons exist already
|
||||
NSArray *existingItems = navigationItem.rightBarButtonItems;
|
||||
if (existingItems.count) {
|
||||
navigationItem.rightBarButtonItems = [@[done] arrayByAddingObjectsFromArray:existingItems];
|
||||
} else {
|
||||
navigationItem.rightBarButtonItem = done;
|
||||
}
|
||||
|
||||
// Keeps us from calling this method again on
|
||||
// the same view controllers in -viewWillAppear:
|
||||
self.didSetupPendingDismissButtons = YES;
|
||||
}
|
||||
|
||||
- (void)addNavigationBarSwipeGesture {
|
||||
UISwipeGestureRecognizer *swipe = [[UISwipeGestureRecognizer alloc]
|
||||
initWithTarget:self action:@selector(handleNavigationBarSwipe:)
|
||||
];
|
||||
swipe.direction = UISwipeGestureRecognizerDirectionDown;
|
||||
swipe.delegate = self;
|
||||
self.navigationBarSwipeGesture = swipe;
|
||||
[self.navigationBar addGestureRecognizer:swipe];
|
||||
}
|
||||
|
||||
- (void)handleNavigationBarSwipe:(UISwipeGestureRecognizer *)sender {
|
||||
if (sender.state == UIGestureRecognizerStateRecognized) {
|
||||
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handleNavigationBarTap:(UIGestureRecognizer *)sender {
|
||||
if (sender.state == UIGestureRecognizerStateRecognized) {
|
||||
if (self.toolbarHidden) {
|
||||
[self setToolbarHidden:NO animated:YES];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)gestureRecognizer:(UIGestureRecognizer *)g1 shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)g2 {
|
||||
if (g1 == self.navigationBarSwipeGesture && g2 == self.barHideOnSwipeGestureRecognizer) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)_gestureRecognizedInteractiveHide:(UIPanGestureRecognizer *)sender {
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
@class FLEXScopeCarousel, FLEXWindow;
|
||||
#import "FLEXTableView.h"
|
||||
@class FLEXScopeCarousel, FLEXWindow, FLEXTableViewSection;
|
||||
|
||||
typedef CGFloat FLEXDebounceInterval;
|
||||
/// No delay, all events delivered
|
||||
@@ -20,19 +21,34 @@ extern CGFloat const kFLEXDebounceForAsyncSearch;
|
||||
extern CGFloat const kFLEXDebounceForExpensiveIO;
|
||||
|
||||
@protocol FLEXSearchResultsUpdating <NSObject>
|
||||
/// A method to handle search query update events.
|
||||
///
|
||||
/// \c searchBarDebounceInterval is used to reduce the frequency at which this
|
||||
/// method is called. This method is also called when the search bar becomes
|
||||
/// the first responder, and when the selected search bar scope index changes.
|
||||
- (void)updateSearchResults:(NSString *)newText;
|
||||
@end
|
||||
|
||||
@interface FLEXTableViewController : UITableViewController <
|
||||
UISearchResultsUpdating, UISearchControllerDelegate,
|
||||
UISearchBarDelegate, FLEXSearchResultsUpdating
|
||||
UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate
|
||||
>
|
||||
|
||||
/// A grouped table view. Inset on iOS 13.
|
||||
///
|
||||
/// Simply calls into initWithStyle:
|
||||
/// Simply calls into \c initWithStyle:
|
||||
- (id)init;
|
||||
|
||||
/// Subclasses may override to configure the controller before \c viewDidLoad:
|
||||
- (id)initWithStyle:(UITableViewStyle)style;
|
||||
|
||||
@property (nonatomic) FLEXTableView *tableView;
|
||||
|
||||
/// If your subclass conforms to \c FLEXSearchResultsUpdating
|
||||
/// then this property is assigned to \c self automatically.
|
||||
///
|
||||
/// Setting \c filterDelegate will also set this property to that object.
|
||||
@property (nonatomic, weak) id<FLEXSearchResultsUpdating> searchDelegate;
|
||||
|
||||
/// Defaults to NO.
|
||||
///
|
||||
/// Setting this to YES will initialize the carousel and the view.
|
||||
@@ -95,15 +111,39 @@ extern CGFloat const kFLEXDebounceForExpensiveIO;
|
||||
/// self.view.window as a \c FLEXWindow
|
||||
@property (nonatomic, readonly) FLEXWindow *window;
|
||||
|
||||
/// Subclasses should override to handle search query update events.
|
||||
///
|
||||
/// searchBarDebounceInterval is used to reduce the frequency at which this method is called.
|
||||
/// This method is also called when the search bar becomes the first responder,
|
||||
/// and when the selected search bar scope index changes.
|
||||
- (void)updateSearchResults:(NSString *)newText;
|
||||
|
||||
/// Convenient for doing some async processor-intensive searching
|
||||
/// in the background before updating the UI back on the main queue.
|
||||
- (void)onBackgroundQueue:(NSArray *(^)(void))backgroundBlock thenOnMainQueue:(void(^)(NSArray *))mainBlock;
|
||||
|
||||
/// Adds up to 3 additional items to the toolbar in right-to-left order.
|
||||
///
|
||||
/// That is, the first item in the given array will be the rightmost item behind
|
||||
/// any existing toolbar items. By default, buttons for bookmarks and tabs are shown.
|
||||
///
|
||||
/// If you wish to have more control over how the buttons are arranged or which
|
||||
/// buttons are displayed, you can access the properties for the pre-existing
|
||||
/// toolbar items directly and manually set \c self.toolbarItems by overriding
|
||||
/// the \c setupToolbarItems method below.
|
||||
- (void)addToolbarItems:(NSArray<UIBarButtonItem *> *)items;
|
||||
|
||||
/// Subclasses may override. You should not need to call this method directly.
|
||||
- (void)setupToolbarItems;
|
||||
|
||||
@property (nonatomic, readonly) UIBarButtonItem *shareToolbarItem;
|
||||
@property (nonatomic, readonly) UIBarButtonItem *bookmarksToolbarItem;
|
||||
@property (nonatomic, readonly) UIBarButtonItem *openTabsToolbarItem;
|
||||
|
||||
/// Whether or not to display the "share" icon in the middle of the toolbar. NO by default.
|
||||
///
|
||||
/// Turning this on after you have added custom toolbar items will
|
||||
/// push off the leftmost toolbar item and shift the others leftward.
|
||||
@property (nonatomic) BOOL showsShareToolbarItem;
|
||||
/// Called when the share button is pressed.
|
||||
/// Default implementation does nothign. Subclasses may override.
|
||||
- (void)shareButtonPressed;
|
||||
|
||||
/// Subclasses may call this to opt-out of all toolbar related behavior.
|
||||
/// This is necessary if you want to disable the gesture which reveals the toolbar.
|
||||
- (void)disableToolbar;
|
||||
|
||||
@end
|
||||
|
||||
@@ -7,9 +7,14 @@
|
||||
//
|
||||
|
||||
#import "FLEXTableViewController.h"
|
||||
#import "FLEXExplorerViewController.h"
|
||||
#import "FLEXBookmarksViewController.h"
|
||||
#import "FLEXTabsViewController.h"
|
||||
#import "FLEXScopeCarousel.h"
|
||||
#import "FLEXTableView.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXResources.h"
|
||||
#import "UIBarButtonItem+FLEX.h"
|
||||
#import <objc/runtime.h>
|
||||
|
||||
@interface Block : NSObject
|
||||
@@ -26,14 +31,23 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
|
||||
@property (nonatomic) BOOL didInitiallyRevealSearchBar;
|
||||
@property (nonatomic) UITableViewStyle style;
|
||||
|
||||
@property (nonatomic) BOOL hasAppeared;
|
||||
@property (nonatomic, readonly) UIView *tableHeaderViewContainer;
|
||||
|
||||
@property (nonatomic, readonly) BOOL manuallyDeactivateSearchOnDisappear;
|
||||
|
||||
@property (nonatomic) UIBarButtonItem *middleToolbarItem;
|
||||
@property (nonatomic) UIBarButtonItem *middleLeftToolbarItem;
|
||||
@property (nonatomic) UIBarButtonItem *leftmostToolbarItem;
|
||||
@end
|
||||
|
||||
@implementation FLEXTableViewController
|
||||
@dynamic tableView;
|
||||
@synthesize showsShareToolbarItem = _showsShareToolbarItem;
|
||||
@synthesize tableHeaderViewContainer = _tableHeaderViewContainer;
|
||||
@synthesize automaticallyShowsSearchBarCancelButton = _automaticallyShowsSearchBarCancelButton;
|
||||
|
||||
#pragma mark - Public
|
||||
#pragma mark - Initialization
|
||||
|
||||
- (id)init {
|
||||
#if FLEX_AT_LEAST_IOS13_SDK
|
||||
@@ -55,11 +69,22 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
|
||||
_searchBarDebounceInterval = kFLEXDebounceFast;
|
||||
_showSearchBarInitially = YES;
|
||||
_style = style;
|
||||
_manuallyDeactivateSearchOnDisappear = ({
|
||||
NSProcessInfo.processInfo.operatingSystemVersion.majorVersion < 11;
|
||||
});
|
||||
|
||||
// We will be our own search delegate if we implement this method
|
||||
if ([self respondsToSelector:@selector(updateSearchResults:)]) {
|
||||
self.searchDelegate = (id)self;
|
||||
}
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
- (FLEXWindow *)window {
|
||||
return (id)self.view.window;
|
||||
}
|
||||
@@ -105,7 +130,7 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
|
||||
FLEXScopeCarousel *carousel = [FLEXScopeCarousel new];
|
||||
carousel.selectedIndexChangedAction = ^(NSInteger idx) {
|
||||
__typeof(self) self = weakSelf;
|
||||
[self updateSearchResults:self.searchText];
|
||||
[self.searchDelegate updateSearchResults:self.searchText];
|
||||
};
|
||||
|
||||
// UITableView won't update the header size unless you reset the header view
|
||||
@@ -129,7 +154,7 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
|
||||
} else if (self.showsCarousel) {
|
||||
return self.carousel.selectedIndex;
|
||||
} else {
|
||||
return NSNotFound;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,7 +165,7 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
|
||||
self.carousel.selectedIndex = selectedScope;
|
||||
}
|
||||
|
||||
[self updateSearchResults:self.searchText];
|
||||
[self.searchDelegate updateSearchResults:self.searchText];
|
||||
}
|
||||
|
||||
- (NSString *)searchText {
|
||||
@@ -167,8 +192,6 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
|
||||
_automaticallyShowsSearchBarCancelButton = value;
|
||||
}
|
||||
|
||||
- (void)updateSearchResults:(NSString *)newText { }
|
||||
|
||||
- (void)onBackgroundQueue:(NSArray *(^)(void))backgroundBlock thenOnMainQueue:(void(^)(NSArray *))mainBlock {
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
NSArray *items = backgroundBlock();
|
||||
@@ -178,18 +201,48 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
|
||||
});
|
||||
}
|
||||
|
||||
- (void)setsShowsShareToolbarItem:(BOOL)showsShareToolbarItem {
|
||||
_showsShareToolbarItem = showsShareToolbarItem;
|
||||
if (self.isViewLoaded) {
|
||||
[self setupToolbarItems];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)disableToolbar {
|
||||
self.navigationController.toolbarHidden = YES;
|
||||
self.navigationController.hidesBarsOnSwipe = NO;
|
||||
self.toolbarItems = nil;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - View Controller Lifecycle
|
||||
|
||||
- (void)loadView {
|
||||
self.view = [FLEXTableView style:self.style];
|
||||
self.tableView.dataSource = self;
|
||||
self.tableView.delegate = self;
|
||||
|
||||
_shareToolbarItem = FLEXBarButtonItemSystem(Action, self, @selector(shareButtonPressed));
|
||||
_bookmarksToolbarItem = [UIBarButtonItem
|
||||
itemWithImage:FLEXResources.bookmarksIcon target:self action:@selector(showBookmarks)
|
||||
];
|
||||
_openTabsToolbarItem = [UIBarButtonItem
|
||||
itemWithImage:FLEXResources.openTabsIcon target:self action:@selector(showTabSwitcher)
|
||||
];
|
||||
|
||||
self.leftmostToolbarItem = UIBarButtonItem.flex_fixedSpace;
|
||||
self.middleLeftToolbarItem = UIBarButtonItem.flex_fixedSpace;
|
||||
self.middleToolbarItem = UIBarButtonItem.flex_fixedSpace;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
self.tableView.keyboardDismissMode = UIScrollViewKeyboardDismissModeOnDrag;
|
||||
|
||||
// Toolbar
|
||||
self.navigationController.toolbarHidden = NO;
|
||||
self.navigationController.hidesBarsOnSwipe = YES;
|
||||
|
||||
// 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
|
||||
@@ -203,11 +256,6 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)viewWillLayoutSubviews {
|
||||
[super viewWillLayoutSubviews];
|
||||
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated {
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
@@ -217,6 +265,8 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
|
||||
self.navigationItem.hidesSearchBarWhenScrolling = NO;
|
||||
}
|
||||
}
|
||||
|
||||
[self setupToolbarItems];
|
||||
}
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated {
|
||||
@@ -241,13 +291,106 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
|
||||
self.didInitiallyRevealSearchBar = YES;
|
||||
}
|
||||
|
||||
- (void)willMoveToParentViewController:(UIViewController *)parent {
|
||||
[super willMoveToParentViewController:parent];
|
||||
- (void)viewWillDisappear:(BOOL)animated {
|
||||
[super viewWillDisappear:animated];
|
||||
|
||||
if (self.manuallyDeactivateSearchOnDisappear && self.searchController.isActive) {
|
||||
self.searchController.active = NO;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)didMoveToParentViewController:(UIViewController *)parent {
|
||||
[super didMoveToParentViewController:parent];
|
||||
// Reset this since we are re-appearing under a new
|
||||
// parent view controller and need to show it again
|
||||
self.didInitiallyRevealSearchBar = NO;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Toolbar, Public
|
||||
|
||||
- (void)setupToolbarItems {
|
||||
if (!self.isViewLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.toolbarItems = @[
|
||||
self.leftmostToolbarItem,
|
||||
UIBarButtonItem.flex_flexibleSpace,
|
||||
self.middleLeftToolbarItem,
|
||||
UIBarButtonItem.flex_flexibleSpace,
|
||||
self.middleToolbarItem,
|
||||
UIBarButtonItem.flex_flexibleSpace,
|
||||
self.bookmarksToolbarItem,
|
||||
UIBarButtonItem.flex_flexibleSpace,
|
||||
self.openTabsToolbarItem,
|
||||
];
|
||||
|
||||
for (UIBarButtonItem *item in self.toolbarItems) {
|
||||
[item _setWidth:60];
|
||||
// This does not work for anything but fixed spaces for some reason
|
||||
// item.width = 60;
|
||||
}
|
||||
|
||||
// Disable tabs entirely when not presented by FLEXExplorerViewController
|
||||
UIViewController *presenter = self.navigationController.presentingViewController;
|
||||
if (![presenter isKindOfClass:[FLEXExplorerViewController class]]) {
|
||||
self.openTabsToolbarItem.enabled = NO;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)addToolbarItems:(NSArray<UIBarButtonItem *> *)items {
|
||||
if (self.showsShareToolbarItem) {
|
||||
// Share button is in the middle, skip middle button
|
||||
if (items.count > 0) {
|
||||
self.middleLeftToolbarItem = items[0];
|
||||
}
|
||||
if (items.count > 1) {
|
||||
self.leftmostToolbarItem = items[1];
|
||||
}
|
||||
} else {
|
||||
// Add buttons right-to-left
|
||||
if (items.count > 0) {
|
||||
self.middleToolbarItem = items[0];
|
||||
}
|
||||
if (items.count > 1) {
|
||||
self.middleLeftToolbarItem = items[1];
|
||||
}
|
||||
if (items.count > 2) {
|
||||
self.leftmostToolbarItem = items[2];
|
||||
}
|
||||
}
|
||||
|
||||
[self setupToolbarItems];
|
||||
}
|
||||
|
||||
- (void)setShowsShareToolbarItem:(BOOL)showShare {
|
||||
if (_showsShareToolbarItem != showShare) {
|
||||
_showsShareToolbarItem = showShare;
|
||||
|
||||
if (showShare) {
|
||||
// Push out leftmost item
|
||||
self.leftmostToolbarItem = self.middleLeftToolbarItem;
|
||||
self.middleLeftToolbarItem = self.middleToolbarItem;
|
||||
|
||||
// Use share for middle
|
||||
self.middleToolbarItem = self.shareToolbarItem;
|
||||
} else {
|
||||
// Remove share, shift custom items rightward
|
||||
self.middleToolbarItem = self.middleLeftToolbarItem;
|
||||
self.middleLeftToolbarItem = self.leftmostToolbarItem;
|
||||
self.leftmostToolbarItem = UIBarButtonItem.flex_fixedSpace;
|
||||
}
|
||||
}
|
||||
|
||||
[self setupToolbarItems];
|
||||
}
|
||||
|
||||
- (void)shareButtonPressed {
|
||||
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
- (void)debounce:(void(^)(void))block {
|
||||
@@ -270,7 +413,6 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
|
||||
}
|
||||
|
||||
self.tableView.tableHeaderView = self.tableView.tableHeaderView;
|
||||
[self.tableView layoutIfNeeded];
|
||||
}
|
||||
|
||||
- (void)addCarousel:(FLEXScopeCarousel *)carousel {
|
||||
@@ -366,6 +508,21 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
|
||||
return _tableHeaderViewContainer;
|
||||
}
|
||||
|
||||
- (void)showBookmarks {
|
||||
UINavigationController *nav = [[UINavigationController alloc]
|
||||
initWithRootViewController:[FLEXBookmarksViewController new]
|
||||
];
|
||||
[self presentViewController:nav animated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (void)showTabSwitcher {
|
||||
UINavigationController *nav = [[UINavigationController alloc]
|
||||
initWithRootViewController:[FLEXTabsViewController new]
|
||||
];
|
||||
[self presentViewController:nav animated:YES completion:nil];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Search Bar
|
||||
|
||||
#pragma mark UISearchResultsUpdating
|
||||
@@ -378,7 +535,7 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
|
||||
if (self.searchResultsUpdater) {
|
||||
[self.searchResultsUpdater updateSearchResults:text];
|
||||
} else {
|
||||
[self updateSearchResults:text];
|
||||
[self.searchDelegate updateSearchResults:text];
|
||||
}
|
||||
};
|
||||
|
||||
@@ -391,6 +548,7 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark UISearchControllerDelegate
|
||||
|
||||
- (void)willPresentSearchController:(UISearchController *)searchController {
|
||||
@@ -407,6 +565,7 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark UISearchBarDelegate
|
||||
|
||||
/// Not necessary in iOS 13; remove this when iOS 13 is the deployment target
|
||||
@@ -414,7 +573,8 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
|
||||
[self updateSearchResultsForSearchController:self.searchController];
|
||||
}
|
||||
|
||||
#pragma mark Table view
|
||||
|
||||
#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 {
|
||||
|
||||
@@ -6,7 +6,9 @@
|
||||
// Copyright © 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXUtility.h"
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "FLEXMacros.h"
|
||||
#import "NSArray+Functional.h"
|
||||
@class FLEXTableView;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
#import "FLEXTableViewSection.h"
|
||||
#import "FLEXTableView.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "UIMenu+FLEX.h"
|
||||
|
||||
#pragma clang diagnostic push
|
||||
@@ -67,7 +68,7 @@
|
||||
return @"";
|
||||
}
|
||||
|
||||
- (NSArray<UIMenuElement *> *)menuItemsForRow:(NSInteger)row sender:(UIViewController *)sender {
|
||||
- (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");
|
||||
|
||||
|
||||
@@ -25,8 +25,10 @@
|
||||
_selectionIndicatorStripe = [UIView new];
|
||||
|
||||
self.titleLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];
|
||||
self.titleLabel.adjustsFontForContentSizeCategory = YES;
|
||||
self.selectionIndicatorStripe.backgroundColor = self.tintColor;
|
||||
if (@available(iOS 10, *)) {
|
||||
self.titleLabel.adjustsFontForContentSizeCategory = YES;
|
||||
}
|
||||
|
||||
[self.contentView addSubview:self.titleLabel];
|
||||
[self.contentView addSubview:self.selectionIndicatorStripe];
|
||||
@@ -45,7 +47,7 @@
|
||||
if (self.selected) {
|
||||
self.titleLabel.textColor = self.tintColor;
|
||||
} else {
|
||||
self.titleLabel.textColor = [FLEXColor deemphasizedTextColor];
|
||||
self.titleLabel.textColor = FLEXColor.deemphasizedTextColor;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ NSString * const kCarouselCellReuseIdentifier = @"kCarouselCellReuseIdentifier";
|
||||
- (id)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
self.backgroundColor = [FLEXColor primaryBackgroundColor];
|
||||
self.backgroundColor = FLEXColor.primaryBackgroundColor;
|
||||
self.autoresizingMask = UIViewAutoresizingFlexibleWidth;
|
||||
self.translatesAutoresizingMaskIntoConstraints = YES;
|
||||
_dynamicTypeHandlers = [NSMutableArray new];
|
||||
@@ -104,7 +104,7 @@ NSString * const kCarouselCellReuseIdentifier = @"kCarouselCellReuseIdentifier";
|
||||
|
||||
// Draw hairline
|
||||
CGContextRef context = UIGraphicsGetCurrentContext();
|
||||
CGContextSetStrokeColorWithColor(context, [FLEXColor hairlineColor].CGColor);
|
||||
CGContextSetStrokeColorWithColor(context, FLEXColor.hairlineColor.CGColor);
|
||||
CGContextSetLineWidth(context, width);
|
||||
CGContextMoveToPoint(context, 0, rect.size.height - width);
|
||||
CGContextAddLineToPoint(context, rect.size.width, rect.size.height - width);
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
}
|
||||
|
||||
+ (UIEdgeInsets)labelInsets {
|
||||
return UIEdgeInsetsMake(10.0, 15.0, 10.0, 15.0);
|
||||
return UIEdgeInsetsMake(10.0, 16.0, 10.0, 8.0);
|
||||
}
|
||||
|
||||
+ (CGFloat)preferredHeightWithAttributedText:(NSAttributedString *)attributedText
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
UIFont *cellFont = UIFont.flex_defaultTableCellFont;
|
||||
self.titleLabel.font = cellFont;
|
||||
self.subtitleLabel.font = cellFont;
|
||||
self.subtitleLabel.textColor = [FLEXColor deemphasizedTextColor];
|
||||
self.subtitleLabel.textColor = FLEXColor.deemphasizedTextColor;
|
||||
|
||||
self.titleLabel.lineBreakMode = NSLineBreakByTruncatingMiddle;
|
||||
self.subtitleLabel.lineBreakMode = NSLineBreakByTruncatingMiddle;
|
||||
@@ -54,39 +54,4 @@
|
||||
return self.detailTextLabel;
|
||||
}
|
||||
|
||||
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
|
||||
return [self._tableView _canPerformAction:action forCell:self sender:sender];
|
||||
}
|
||||
|
||||
/// We use this to allow our table view to allow its delegate
|
||||
/// to handle any action it chooses to support, without
|
||||
/// explicitly implementing the method ourselves.
|
||||
///
|
||||
/// Alternative considered: override respondsToSelector
|
||||
/// to return NO. I decided against this for simplicity's
|
||||
/// sake. I see this as "fixing" a poorly designed API.
|
||||
/// That approach would require lots of boilerplate to
|
||||
/// make the menu appear above this cell.
|
||||
- (void)forwardInvocation:(NSInvocation *)invocation {
|
||||
// Must be unretained to avoid over-releasing
|
||||
__unsafe_unretained id sender;
|
||||
[invocation getArgument:&sender atIndex:2];
|
||||
SEL action = invocation.selector;
|
||||
|
||||
// [self._tableView _performAction:action forCell:[self retain] sender:[sender retain]];
|
||||
invocation.selector = @selector(_performAction:forCell:sender:);
|
||||
[invocation setArgument:&action atIndex:2];
|
||||
[invocation setArgument:(void *)&self atIndex:3];
|
||||
[invocation setArgument:(void *)&sender atIndex:4];
|
||||
[invocation invokeWithTarget:self._tableView];
|
||||
}
|
||||
|
||||
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
|
||||
if ([self canPerformAction:selector withSender:nil]) {
|
||||
return [self._tableView methodSignatureForSelector:@selector(_performAction:forCell:sender:)];
|
||||
}
|
||||
|
||||
return [super methodSignatureForSelector:selector];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
#pragma mark Reuse identifiers
|
||||
|
||||
typedef NSString * FLEXTableViewCellReuseIdentifier;
|
||||
@@ -42,3 +44,5 @@ extern FLEXTableViewCellReuseIdentifier const kFLEXCodeFontCell;
|
||||
- (void)registerCells:(NSDictionary<NSString *, Class> *)registrationMapping;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -41,30 +41,6 @@ FLEXTableViewCellReuseIdentifier const kFLEXCodeFontCell = @"kFLEXCodeFontCell";
|
||||
#endif
|
||||
}
|
||||
|
||||
- (CGFloat)_heightForHeaderInSection:(NSInteger)section {
|
||||
CGFloat height = [super _heightForHeaderInSection:section];
|
||||
if (section == 0) {
|
||||
NSString *title = [self _titleForHeaderInSection:section];
|
||||
if (self.tableHeaderView) {
|
||||
if (!@available(iOS 13, *)) {
|
||||
return height - self.tableHeaderView.frame.size.height + 8;
|
||||
}
|
||||
// On iOS 13, returning an empty title for the table view
|
||||
// messes with the height of the table view later on.
|
||||
// We return a space to work around this.
|
||||
else if ([title isEqualToString:@" "]) {
|
||||
return height - self.tableHeaderView.frame.size.height + 5;
|
||||
}
|
||||
} else {
|
||||
if (@available(iOS 13, *) && [title isEqualToString:@" "]) {
|
||||
return 5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return height;
|
||||
}
|
||||
|
||||
#pragma mark - Initialization
|
||||
|
||||
+ (id)groupedTableView {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/30/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputView.h"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/30/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputColorView.h"
|
||||
@@ -234,7 +234,7 @@
|
||||
[self updateWithColor:color];
|
||||
}
|
||||
} else {
|
||||
[self updateWithColor:[UIColor clearColor]];
|
||||
[self updateWithColor:UIColor.clearColor];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Daniel Rodriguez Troitino on 2/14/15.
|
||||
// Copyright (c) 2015 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputView.h"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Daniel Rodriguez Troitino on 2/14/15.
|
||||
// Copyright (c) 2015 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputDateView.h"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/28/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputView.h"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/28/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputFontView.h"
|
||||
|
||||
@@ -51,8 +51,8 @@
|
||||
}
|
||||
|
||||
- (void)createAvailableFonts {
|
||||
NSMutableArray<NSString *> *unsortedFontsArray = [NSMutableArray array];
|
||||
for (NSString *eachFontFamily in [UIFont familyNames]) {
|
||||
NSMutableArray<NSString *> *unsortedFontsArray = [NSMutableArray new];
|
||||
for (NSString *eachFontFamily in UIFont.familyNames) {
|
||||
for (NSString *eachFontName in [UIFont fontNamesForFamilyName:eachFontFamily]) {
|
||||
[unsortedFontsArray addObject:eachFontName];
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/18/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputTextView.h"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/18/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputNotSupportedView.h"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/15/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputTextView.h"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/15/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputNumberView.h"
|
||||
@@ -17,6 +17,7 @@
|
||||
self.inputTextView.keyboardType = UIKeyboardTypeNumbersAndPunctuation;
|
||||
self.targetSize = FLEXArgumentInputViewSizeSmall;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
@@ -33,24 +34,29 @@
|
||||
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
|
||||
NSParameterAssert(type);
|
||||
|
||||
static NSArray<NSString *> *primitiveTypes = nil;
|
||||
static NSArray<NSString *> *supportedTypes = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
primitiveTypes = @[@(@encode(char)),
|
||||
@(@encode(int)),
|
||||
@(@encode(short)),
|
||||
@(@encode(long)),
|
||||
@(@encode(long long)),
|
||||
@(@encode(unsigned char)),
|
||||
@(@encode(unsigned int)),
|
||||
@(@encode(unsigned short)),
|
||||
@(@encode(unsigned long)),
|
||||
@(@encode(unsigned long long)),
|
||||
@(@encode(float)),
|
||||
@(@encode(double)),
|
||||
@(@encode(long double))];
|
||||
supportedTypes = @[
|
||||
@FLEXEncodeClass(NSNumber),
|
||||
@FLEXEncodeClass(NSDecimalNumber),
|
||||
@(@encode(char)),
|
||||
@(@encode(int)),
|
||||
@(@encode(short)),
|
||||
@(@encode(long)),
|
||||
@(@encode(long long)),
|
||||
@(@encode(unsigned char)),
|
||||
@(@encode(unsigned int)),
|
||||
@(@encode(unsigned short)),
|
||||
@(@encode(unsigned long)),
|
||||
@(@encode(unsigned long long)),
|
||||
@(@encode(float)),
|
||||
@(@encode(double)),
|
||||
@(@encode(long double))
|
||||
];
|
||||
});
|
||||
return type && [primitiveTypes containsObject:@(type)];
|
||||
|
||||
return type && [supportedTypes containsObject:@(type)];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/15/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputTextView.h"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/15/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputObjectView.h"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/28/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputTextView.h"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/28/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputStringView.h"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/16/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputView.h"
|
||||
|
||||
@@ -3,12 +3,13 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/16/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputStructView.h"
|
||||
#import "FLEXArgumentInputViewFactory.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
#import "FLEXTypeEncodingParser.h"
|
||||
|
||||
@interface FLEXArgumentInputStructView ()
|
||||
|
||||
@@ -21,7 +22,7 @@
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
|
||||
self = [super initWithArgumentTypeEncoding:typeEncoding];
|
||||
if (self) {
|
||||
NSMutableArray<FLEXArgumentInputView *> *inputViews = [NSMutableArray array];
|
||||
NSMutableArray<FLEXArgumentInputView *> *inputViews = [NSMutableArray new];
|
||||
NSArray<NSString *> *customTitles = [[self class] customFieldTitlesForTypeEncoding:typeEncoding];
|
||||
[FLEXRuntimeUtility enumerateTypesInStructEncoding:typeEncoding usingBlock:^(NSString *structName,
|
||||
const char *fieldTypeEncoding,
|
||||
@@ -63,12 +64,8 @@
|
||||
const char *structTypeEncoding = [inputValue objCType];
|
||||
if (strcmp(self.typeEncoding.UTF8String, structTypeEncoding) == 0) {
|
||||
NSUInteger valueSize = 0;
|
||||
@try {
|
||||
// NSGetSizeAndAlignment barfs on type encoding for bitfields.
|
||||
NSGetSizeAndAlignment(structTypeEncoding, &valueSize, NULL);
|
||||
} @catch (NSException *exception) { }
|
||||
|
||||
if (valueSize > 0) {
|
||||
if (FLEXGetSizeAndAlignment(structTypeEncoding, &valueSize, NULL)) {
|
||||
void *unboxedValue = malloc(valueSize);
|
||||
[inputValue getValue:unboxedValue];
|
||||
[FLEXRuntimeUtility enumerateTypesInStructEncoding:structTypeEncoding usingBlock:^(NSString *structName,
|
||||
@@ -97,12 +94,8 @@
|
||||
NSValue *boxedStruct = nil;
|
||||
const char *structTypeEncoding = self.typeEncoding.UTF8String;
|
||||
NSUInteger structSize = 0;
|
||||
@try {
|
||||
// NSGetSizeAndAlignment barfs on type encoding for bitfields.
|
||||
NSGetSizeAndAlignment(structTypeEncoding, &structSize, NULL);
|
||||
} @catch (NSException *exception) { }
|
||||
|
||||
if (structSize > 0) {
|
||||
if (FLEXGetSizeAndAlignment(structTypeEncoding, &structSize, NULL)) {
|
||||
void *unboxedStruct = malloc(structSize);
|
||||
[FLEXRuntimeUtility enumerateTypesInStructEncoding:structTypeEncoding usingBlock:^(NSString *structName,
|
||||
const char *fieldTypeEncoding,
|
||||
@@ -182,15 +175,7 @@
|
||||
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
|
||||
NSParameterAssert(type);
|
||||
if (type[0] == FLEXTypeEncodingStructBegin) {
|
||||
// We cannot support anything with bitfields or structs,
|
||||
// and this will throw an exception if it does
|
||||
@try {
|
||||
NSGetSizeAndAlignment(type, nil, nil);
|
||||
} @catch (NSException *exception) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
return YES;
|
||||
return FLEXGetSizeAndAlignment(type, nil, nil);
|
||||
}
|
||||
|
||||
return NO;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/16/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputView.h"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/16/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputSwitchView.h"
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
if (self) {
|
||||
self.inputTextView = [UITextView new];
|
||||
self.inputTextView.font = [[self class] inputFont];
|
||||
self.inputTextView.backgroundColor = [FLEXColor secondaryGroupedBackgroundColor];
|
||||
self.inputTextView.backgroundColor = FLEXColor.secondaryGroupedBackgroundColor;
|
||||
self.inputTextView.layer.cornerRadius = 10.f;
|
||||
self.inputTextView.contentInset = UIEdgeInsetsMake(0, 5, 0, 0);
|
||||
self.inputTextView.autocapitalizationType = UITextAutocapitalizationTypeNone;
|
||||
@@ -36,12 +36,12 @@
|
||||
[self.inputTextView.layer setValue:@YES forKey:@"continuousCorners"];
|
||||
} else {
|
||||
self.inputTextView.layer.borderWidth = 1.f;
|
||||
self.inputTextView.layer.borderColor = [FLEXColor borderColor].CGColor;
|
||||
self.inputTextView.layer.borderColor = FLEXColor.borderColor.CGColor;
|
||||
}
|
||||
|
||||
self.placeholderLabel = [UILabel new];
|
||||
self.placeholderLabel.font = self.inputTextView.font;
|
||||
self.placeholderLabel.textColor = [FLEXColor deemphasizedTextColor];
|
||||
self.placeholderLabel.textColor = FLEXColor.deemphasizedTextColor;
|
||||
self.placeholderLabel.numberOfLines = 0;
|
||||
|
||||
[self addSubview:self.inputTextView];
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 5/30/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 5/30/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputView.h"
|
||||
@@ -54,7 +54,7 @@
|
||||
if (!_titleLabel) {
|
||||
_titleLabel = [UILabel new];
|
||||
_titleLabel.font = [[self class] titleFont];
|
||||
_titleLabel.textColor = [FLEXColor primaryTextColor];
|
||||
_titleLabel.textColor = FLEXColor.primaryTextColor;
|
||||
_titleLabel.numberOfLines = 0;
|
||||
[self addSubview:_titleLabel];
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 5/23/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXFieldEditorViewController.h"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 5/23/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXDefaultEditorViewController.h"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 5/16/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 5/16/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXFieldEditorView.h"
|
||||
|
||||
@@ -10,8 +10,10 @@
|
||||
#import "FLEXFieldEditorView.h"
|
||||
#import "FLEXArgumentInputViewFactory.h"
|
||||
#import "FLEXPropertyAttributes.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXColor.h"
|
||||
#import "UIBarButtonItem+FLEX.h"
|
||||
|
||||
@interface FLEXFieldEditorViewController () <FLEXArgumentInputViewDelegate>
|
||||
|
||||
@@ -35,14 +37,14 @@
|
||||
}
|
||||
|
||||
FLEXFieldEditorViewController *editor = [self target:target];
|
||||
editor.title = @"Property";
|
||||
editor.title = [@"Property: " stringByAppendingString:property.name];
|
||||
editor.property = property;
|
||||
return editor;
|
||||
}
|
||||
|
||||
+ (instancetype)target:(id)target ivar:(nonnull FLEXIvar *)ivar {
|
||||
FLEXFieldEditorViewController *editor = [self target:target];
|
||||
editor.title = @"Instance Variable";
|
||||
editor.title = [@"Ivar: " stringByAppendingString:ivar.name];
|
||||
editor.ivar = ivar;
|
||||
return editor;
|
||||
}
|
||||
@@ -52,7 +54,7 @@
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
self.view.backgroundColor = [FLEXColor groupedBackgroundColor];
|
||||
self.view.backgroundColor = FLEXColor.groupedBackgroundColor;
|
||||
|
||||
// Create getter button
|
||||
_getterButton = [[UIBarButtonItem alloc]
|
||||
@@ -61,7 +63,9 @@
|
||||
target:self
|
||||
action:@selector(getterButtonPressed:)
|
||||
];
|
||||
self.navigationItem.rightBarButtonItems = @[self.setterButton, self.getterButton];
|
||||
self.toolbarItems = @[
|
||||
UIBarButtonItem.flex_flexibleSpace, self.getterButton, self.actionButton
|
||||
];
|
||||
|
||||
// Configure input view
|
||||
self.fieldEditorView.fieldDescription = self.fieldDescription;
|
||||
@@ -72,7 +76,12 @@
|
||||
|
||||
// Don't show a "set" button for switches; we mutate when the switch is flipped
|
||||
if ([inputView isKindOfClass:[FLEXArgumentInputSwitchView class]]) {
|
||||
self.navigationItem.rightBarButtonItem = nil;
|
||||
self.actionButton.enabled = NO;
|
||||
self.actionButton.title = @"Flip the switch to call the setter";
|
||||
// Put getter button before setter button
|
||||
self.toolbarItems = @[
|
||||
UIBarButtonItem.flex_flexibleSpace, self.actionButton, self.getterButton
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 5/23/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXVariableEditorViewController.h"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 5/23/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXMethodCallingViewController.h"
|
||||
@@ -31,7 +31,8 @@
|
||||
self = [super initWithTarget:target];
|
||||
if (self) {
|
||||
self.method = method;
|
||||
self.title = method.isInstanceMethod ? @"Method" : @"Class Method";
|
||||
self.title = method.isInstanceMethod ? @"Method: " : @"Class Method: ";
|
||||
self.title = [self.title stringByAppendingString:method.selectorString];
|
||||
}
|
||||
|
||||
return self;
|
||||
@@ -40,7 +41,7 @@
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
self.setterButton.title = @"Call";
|
||||
self.actionButton.title = @"Call";
|
||||
|
||||
// Configure field editor view
|
||||
self.fieldEditorView.argumentInputViews = [self argumentInputViews];
|
||||
@@ -53,7 +54,7 @@
|
||||
- (NSArray<FLEXArgumentInputView *> *)argumentInputViews {
|
||||
Method method = self.method.objc_method;
|
||||
NSArray *methodComponents = [FLEXRuntimeUtility prettyArgumentComponentsForMethod:method];
|
||||
NSMutableArray<FLEXArgumentInputView *> *argumentInputViews = [NSMutableArray array];
|
||||
NSMutableArray<FLEXArgumentInputView *> *argumentInputViews = [NSMutableArray new];
|
||||
unsigned int argumentIndex = kFLEXNumberOfImplicitArgs;
|
||||
|
||||
for (NSString *methodComponent in methodComponents) {
|
||||
@@ -74,10 +75,10 @@
|
||||
[super actionButtonPressed:sender];
|
||||
|
||||
// Gather arguments
|
||||
NSMutableArray *arguments = [NSMutableArray array];
|
||||
NSMutableArray *arguments = [NSMutableArray new];
|
||||
for (FLEXArgumentInputView *inputView in self.fieldEditorView.argumentInputViews) {
|
||||
// Use NSNull as a nil placeholder; it will be interpreted as nil
|
||||
[arguments addObject:inputView.inputValue ?: [NSNull null]];
|
||||
[arguments addObject:inputView.inputValue ?: NSNull.null];
|
||||
}
|
||||
|
||||
// Call method
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 5/16/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
@@ -23,8 +23,8 @@
|
||||
// For subclass use only.
|
||||
@property (nonatomic, readonly) id target;
|
||||
@property (nonatomic, readonly) FLEXFieldEditorView *fieldEditorView;
|
||||
/// Subclasses can change the button title via the \c title property
|
||||
@property (nonatomic, readonly) UIBarButtonItem *setterButton;
|
||||
/// Subclasses can change the button title via the button's \c title property
|
||||
@property (nonatomic, readonly) UIBarButtonItem *actionButton;
|
||||
|
||||
- (void)actionButtonPressed:(id)sender;
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 5/16/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXColor.h"
|
||||
@@ -15,6 +15,7 @@
|
||||
#import "FLEXArgumentInputView.h"
|
||||
#import "FLEXArgumentInputViewFactory.h"
|
||||
#import "FLEXObjectExplorerViewController.h"
|
||||
#import "UIBarButtonItem+FLEX.h"
|
||||
|
||||
@interface FLEXVariableEditorViewController () <UIScrollViewDelegate>
|
||||
@property (nonatomic) UIScrollView *scrollView;
|
||||
@@ -92,13 +93,15 @@
|
||||
self.fieldEditorView.targetDescription = [NSString stringWithFormat:@"%@ %p", [self.target class], self.target];
|
||||
[self.scrollView addSubview:self.fieldEditorView];
|
||||
|
||||
_setterButton = [[UIBarButtonItem alloc]
|
||||
_actionButton = [[UIBarButtonItem alloc]
|
||||
initWithTitle:@"Set"
|
||||
style:UIBarButtonItemStyleDone
|
||||
target:self
|
||||
action:@selector(actionButtonPressed:)
|
||||
];
|
||||
self.navigationItem.rightBarButtonItem = self.setterButton;
|
||||
|
||||
self.navigationController.toolbarHidden = NO;
|
||||
self.toolbarItems = @[UIBarButtonItem.flex_flexibleSpace, self.actionButton];
|
||||
}
|
||||
|
||||
- (void)viewWillLayoutSubviews {
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// FLEXBookmarkManager.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 2/6/20.
|
||||
// Copyright © 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FLEXBookmarkManager : NSObject
|
||||
|
||||
@property (nonatomic, readonly, class) NSMutableArray *bookmarks;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,25 @@
|
||||
//
|
||||
// FLEXBookmarkManager.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 2/6/20.
|
||||
// Copyright © 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXBookmarkManager.h"
|
||||
|
||||
static NSMutableArray *kFLEXBookmarkManagerBookmarks = nil;
|
||||
|
||||
@implementation FLEXBookmarkManager
|
||||
|
||||
+ (void)initialize {
|
||||
if (self == [FLEXBookmarkManager class]) {
|
||||
kFLEXBookmarkManagerBookmarks = [NSMutableArray new];
|
||||
}
|
||||
}
|
||||
|
||||
+ (NSMutableArray *)bookmarks {
|
||||
return kFLEXBookmarkManagerBookmarks;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// FLEXBookmarksViewController.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 2/6/20.
|
||||
// Copyright © 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXTableViewController.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FLEXBookmarksViewController : FLEXTableViewController
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,235 @@
|
||||
//
|
||||
// FLEXBookmarksViewController.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 2/6/20.
|
||||
// Copyright © 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXBookmarksViewController.h"
|
||||
#import "FLEXExplorerViewController.h"
|
||||
#import "FLEXNavigationController.h"
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
#import "FLEXBookmarkManager.h"
|
||||
#import "UIBarButtonItem+FLEX.h"
|
||||
#import "FLEXColor.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
#import "FLEXTableView.h"
|
||||
|
||||
@interface FLEXBookmarksViewController ()
|
||||
@property (nonatomic, copy) NSArray *bookmarks;
|
||||
@property (nonatomic, readonly) FLEXExplorerViewController *corePresenter;
|
||||
@end
|
||||
|
||||
@implementation FLEXBookmarksViewController
|
||||
|
||||
#pragma mark - Initialization
|
||||
|
||||
- (id)init {
|
||||
return [self initWithStyle:UITableViewStylePlain];
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
self.navigationController.hidesBarsOnSwipe = NO;
|
||||
self.tableView.allowsMultipleSelectionDuringEditing = YES;
|
||||
|
||||
[self reloadData];
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated {
|
||||
[super viewWillAppear:animated];
|
||||
[self setupDefaultBarItems];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
- (void)reloadData {
|
||||
// We assume the bookmarks aren't going to change out from under us, since
|
||||
// presenting any other tool via keyboard shortcuts should dismiss us first
|
||||
self.bookmarks = FLEXBookmarkManager.bookmarks;
|
||||
self.title = [NSString stringWithFormat:@"Bookmarks (%@)", @(self.bookmarks.count)];
|
||||
}
|
||||
|
||||
- (void)setupDefaultBarItems {
|
||||
self.navigationItem.rightBarButtonItem = FLEXBarButtonItemSystem(Done, self, @selector(dismissAnimated));
|
||||
self.toolbarItems = @[
|
||||
UIBarButtonItem.flex_flexibleSpace,
|
||||
FLEXBarButtonItemSystem(Edit, self, @selector(toggleEditing)),
|
||||
];
|
||||
|
||||
// Disable editing if no bookmarks available
|
||||
self.toolbarItems.lastObject.enabled = self.bookmarks.count > 0;
|
||||
}
|
||||
|
||||
- (void)setupEditingBarItems {
|
||||
self.navigationItem.rightBarButtonItem = nil;
|
||||
self.toolbarItems = @[
|
||||
[UIBarButtonItem 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 doneStyleitemWithTitle:@"Done" target:self action:@selector(toggleEditing)]
|
||||
];
|
||||
|
||||
self.toolbarItems.firstObject.tintColor = FLEXColor.destructiveColor;
|
||||
}
|
||||
|
||||
- (FLEXExplorerViewController *)corePresenter {
|
||||
// We must be presented by a FLEXExplorerViewController, or presented
|
||||
// by another view controller that was presented by FLEXExplorerViewController
|
||||
FLEXExplorerViewController *presenter = (id)self.presentingViewController;
|
||||
presenter = (id)presenter.presentingViewController ?: presenter;
|
||||
presenter = (id)presenter.presentingViewController ?: presenter;
|
||||
NSAssert(
|
||||
[presenter isKindOfClass:[FLEXExplorerViewController class]],
|
||||
@"The bookmarks view controller expects to be presented by the explorer controller"
|
||||
);
|
||||
return presenter;
|
||||
}
|
||||
|
||||
#pragma mark Button Actions
|
||||
|
||||
- (void)dismissAnimated {
|
||||
[self dismissAnimated:nil];
|
||||
}
|
||||
|
||||
- (void)dismissAnimated:(id)selectedObject {
|
||||
if (selectedObject) {
|
||||
UIViewController *explorer = [FLEXObjectExplorerFactory
|
||||
explorerViewControllerForObject:selectedObject
|
||||
];
|
||||
if ([self.presentingViewController isKindOfClass:[FLEXNavigationController class]]) {
|
||||
// I am presented on an existing navigation stack, so
|
||||
// dismiss myself and push the bookmark there
|
||||
UINavigationController *presenter = (id)self.presentingViewController;
|
||||
[presenter dismissViewControllerAnimated:YES completion:^{
|
||||
[presenter pushViewController:explorer animated:YES];
|
||||
}];
|
||||
} else {
|
||||
// Dismiss myself and present explorer
|
||||
UIViewController *presenter = self.corePresenter;
|
||||
[presenter dismissViewControllerAnimated:YES completion:^{
|
||||
[presenter presentViewController:[FLEXNavigationController
|
||||
withRootViewController:explorer
|
||||
] animated:YES completion:nil];
|
||||
}];
|
||||
}
|
||||
} else {
|
||||
// Just dismiss myself
|
||||
[self dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)toggleEditing {
|
||||
NSArray<NSIndexPath *> *selected = self.tableView.indexPathsForSelectedRows;
|
||||
self.editing = !self.editing;
|
||||
|
||||
if (self.isEditing) {
|
||||
[self setupEditingBarItems];
|
||||
} else {
|
||||
[self setupDefaultBarItems];
|
||||
|
||||
// Get index set of bookmarks to close
|
||||
NSMutableIndexSet *indexes = [NSMutableIndexSet new];
|
||||
for (NSIndexPath *ip in selected) {
|
||||
[indexes addIndex:ip.row];
|
||||
}
|
||||
|
||||
if (selected.count) {
|
||||
// Close bookmarks and update data source
|
||||
[FLEXBookmarkManager.bookmarks removeObjectsAtIndexes:indexes];
|
||||
[self reloadData];
|
||||
|
||||
// Remove deleted rows
|
||||
[self.tableView deleteRowsAtIndexPaths:selected withRowAnimation:UITableViewRowAnimationAutomatic];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)closeAllButtonPressed {
|
||||
[FLEXAlert makeSheet:^(FLEXAlert *make) {
|
||||
NSInteger count = self.bookmarks.count;
|
||||
NSString *title = FLEXPluralFormatString(count, @"Remove %@ bookmarks", @"Remove %@ bookmark");
|
||||
make.button(title).destructiveStyle().handler(^(NSArray<NSString *> *strings) {
|
||||
[self closeAll];
|
||||
[self toggleEditing];
|
||||
});
|
||||
make.button(@"Cancel").cancelStyle();
|
||||
} showFrom:self];
|
||||
}
|
||||
|
||||
- (void)closeAll {
|
||||
NSInteger rowCount = self.bookmarks.count;
|
||||
|
||||
// Close bookmarks and update data source
|
||||
[FLEXBookmarkManager.bookmarks removeAllObjects];
|
||||
[self reloadData];
|
||||
|
||||
// Delete rows from table view
|
||||
NSArray<NSIndexPath *> *allRows = [NSArray flex_forEachUpTo:rowCount map:^id(NSUInteger row) {
|
||||
return [NSIndexPath indexPathForRow:row inSection:0];
|
||||
}];
|
||||
[self.tableView deleteRowsAtIndexPaths:allRows withRowAnimation:UITableViewRowAnimationAutomatic];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Table View Data Source
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
|
||||
return self.bookmarks.count;
|
||||
}
|
||||
|
||||
- (UITableViewCell *)tableView:(FLEXTableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kFLEXDetailCell forIndexPath:indexPath];
|
||||
|
||||
id object = self.bookmarks[indexPath.row];
|
||||
cell.textLabel.text = [FLEXRuntimeUtility safeDescriptionForObject:object];
|
||||
cell.detailTextLabel.text = [NSString stringWithFormat:@"%@ — %p", [object class], object];
|
||||
|
||||
return cell;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Table View Delegate
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
if (self.editing) {
|
||||
// Case: editing with multi-select
|
||||
self.toolbarItems.lastObject.title = @"Remove Selected";
|
||||
self.toolbarItems.lastObject.tintColor = FLEXColor.destructiveColor;
|
||||
} else {
|
||||
// Case: selected a bookmark
|
||||
[self dismissAnimated:self.bookmarks[indexPath.row]];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
NSParameterAssert(self.editing);
|
||||
|
||||
if (tableView.indexPathsForSelectedRows.count == 0) {
|
||||
self.toolbarItems.lastObject.title = @"Done";
|
||||
self.toolbarItems.lastObject.tintColor = self.view.tintColor;
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)tableView:(UITableView *)table
|
||||
commitEditingStyle:(UITableViewCellEditingStyle)edit
|
||||
forRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
NSParameterAssert(edit == UITableViewCellEditingStyleDelete);
|
||||
|
||||
// Remove bookmark and update data source
|
||||
[FLEXBookmarkManager.bookmarks removeObjectAtIndex:indexPath.row];
|
||||
[self reloadData];
|
||||
|
||||
// Delete row from table view
|
||||
[table deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -3,10 +3,10 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 4/4/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "FLEXExplorerToolbar.h"
|
||||
|
||||
@class FLEXWindow;
|
||||
@protocol FLEXExplorerViewControllerDelegate;
|
||||
@@ -17,6 +17,8 @@
|
||||
@property (nonatomic, weak) id <FLEXExplorerViewControllerDelegate> delegate;
|
||||
@property (nonatomic, readonly) BOOL wantsWindowToBecomeKey;
|
||||
|
||||
@property (nonatomic, readonly) FLEXExplorerToolbar *explorerToolbar;
|
||||
|
||||
- (BOOL)shouldReceiveTouchAtWindowPoint:(CGPoint)pointInWindowCoordinates;
|
||||
|
||||
/// @brief Used to present (or dismiss) a modal view controller ("tool"), typically triggered by pressing a button in the toolbar.
|
||||
|
||||
@@ -3,22 +3,24 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 4/4/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXExplorerViewController.h"
|
||||
#import "FLEXExplorerToolbar.h"
|
||||
#import "FLEXToolbarItem.h"
|
||||
#import "FLEXExplorerToolbarItem.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXWindow.h"
|
||||
#import "FLEXTabList.h"
|
||||
#import "FLEXNavigationController.h"
|
||||
#import "FLEXHierarchyViewController.h"
|
||||
#import "FLEXGlobalsViewController.h"
|
||||
#import "FLEXObjectExplorerViewController.h"
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
#import "FLEXNetworkHistoryTableViewController.h"
|
||||
|
||||
static NSString *const kFLEXToolbarTopMarginDefaultsKey = @"com.flex.FLEXToolbar.topMargin";
|
||||
#import "FLEXNetworkMITMViewController.h"
|
||||
#import "FLEXTabsViewController.h"
|
||||
#import "FLEXWindowManagerController.h"
|
||||
#import "FLEXViewControllersViewController.h"
|
||||
#import "NSUserDefaults+FLEX.h"
|
||||
|
||||
typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
FLEXExplorerModeDefault,
|
||||
@@ -28,8 +30,6 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
|
||||
@interface FLEXExplorerViewController () <FLEXHierarchyDelegate, UIAdaptivePresentationControllerDelegate>
|
||||
|
||||
@property (nonatomic) FLEXExplorerToolbar *explorerToolbar;
|
||||
|
||||
/// Tracks the currently active tool/mode
|
||||
@property (nonatomic) FLEXExplorerMode currentMode;
|
||||
|
||||
@@ -74,7 +74,7 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
|
||||
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
|
||||
if (self) {
|
||||
self.observedViews = [NSMutableSet set];
|
||||
self.observedViews = [NSMutableSet new];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
@@ -89,11 +89,10 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
[super viewDidLoad];
|
||||
|
||||
// Toolbar
|
||||
self.explorerToolbar = [FLEXExplorerToolbar new];
|
||||
_explorerToolbar = [FLEXExplorerToolbar new];
|
||||
|
||||
// Start the toolbar off below any bars that may be at the top of the view.
|
||||
id toolbarOriginYDefault = [[NSUserDefaults standardUserDefaults] objectForKey:kFLEXToolbarTopMarginDefaultsKey];
|
||||
CGFloat toolbarOriginY = toolbarOriginYDefault ? [toolbarOriginYDefault doubleValue] : 100;
|
||||
CGFloat toolbarOriginY = NSUserDefaults.standardUserDefaults.flex_toolbarTopMargin;
|
||||
|
||||
CGRect safeArea = [self viewSafeArea];
|
||||
CGSize toolbarSize = [self.explorerToolbar sizeThatFits:CGSizeMake(
|
||||
@@ -145,8 +144,13 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
}
|
||||
|
||||
- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
|
||||
// Commenting this out until I can figure out a better way to solve this
|
||||
// if (self.window.isKeyWindow) {
|
||||
// [self.window resignKeyWindow];
|
||||
// }
|
||||
|
||||
UIViewController *viewControllerToAsk = [self viewControllerForRotationAndOrientation];
|
||||
UIInterfaceOrientationMask supportedOrientations = [FLEXUtility infoPlistSupportedInterfaceOrientationsMask];
|
||||
UIInterfaceOrientationMask supportedOrientations = FLEXUtility.infoPlistSupportedInterfaceOrientationsMask;
|
||||
if (viewControllerToAsk && ![viewControllerToAsk isKindOfClass:[self class]]) {
|
||||
supportedOrientations = [viewControllerToAsk supportedInterfaceOrientations];
|
||||
}
|
||||
@@ -365,24 +369,25 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
|
||||
- (void)setupToolbarActions {
|
||||
FLEXExplorerToolbar *toolbar = self.explorerToolbar;
|
||||
NSDictionary<NSString *, FLEXToolbarItem *> *actionsToItems = @{
|
||||
NSDictionary<NSString *, FLEXExplorerToolbarItem *> *actionsToItems = @{
|
||||
NSStringFromSelector(@selector(selectButtonTapped:)): toolbar.selectItem,
|
||||
NSStringFromSelector(@selector(hierarchyButtonTapped:)): toolbar.hierarchyItem,
|
||||
NSStringFromSelector(@selector(recentButtonTapped:)): toolbar.recentItem,
|
||||
NSStringFromSelector(@selector(moveButtonTapped:)): toolbar.moveItem,
|
||||
NSStringFromSelector(@selector(globalsButtonTapped:)): toolbar.globalsItem,
|
||||
NSStringFromSelector(@selector(closeButtonTapped:)): toolbar.closeItem,
|
||||
};
|
||||
|
||||
[actionsToItems enumerateKeysAndObjectsUsingBlock:^(NSString *sel, FLEXToolbarItem *item, BOOL *stop) {
|
||||
[actionsToItems enumerateKeysAndObjectsUsingBlock:^(NSString *sel, FLEXExplorerToolbarItem *item, BOOL *stop) {
|
||||
[item addTarget:self action:NSSelectorFromString(sel) forControlEvents:UIControlEventTouchUpInside];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)selectButtonTapped:(FLEXToolbarItem *)sender {
|
||||
- (void)selectButtonTapped:(FLEXExplorerToolbarItem *)sender {
|
||||
[self toggleSelectTool];
|
||||
}
|
||||
|
||||
- (void)hierarchyButtonTapped:(FLEXToolbarItem *)sender {
|
||||
- (void)hierarchyButtonTapped:(FLEXExplorerToolbarItem *)sender {
|
||||
[self toggleViewsTool];
|
||||
}
|
||||
|
||||
@@ -391,48 +396,85 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
return [UIApplication.sharedApplication valueForKey:statusBarString];
|
||||
}
|
||||
|
||||
- (void)moveButtonTapped:(FLEXToolbarItem *)sender {
|
||||
- (void)recentButtonTapped:(FLEXExplorerToolbarItem *)sender {
|
||||
NSAssert(FLEXTabList.sharedList.activeTab, @"Must have active tab");
|
||||
[self presentViewController:FLEXTabList.sharedList.activeTab animated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (void)moveButtonTapped:(FLEXExplorerToolbarItem *)sender {
|
||||
[self toggleMoveTool];
|
||||
}
|
||||
|
||||
- (void)globalsButtonTapped:(FLEXToolbarItem *)sender {
|
||||
- (void)globalsButtonTapped:(FLEXExplorerToolbarItem *)sender {
|
||||
[self toggleMenuTool];
|
||||
}
|
||||
|
||||
- (void)closeButtonTapped:(FLEXToolbarItem *)sender {
|
||||
- (void)closeButtonTapped:(FLEXExplorerToolbarItem *)sender {
|
||||
self.currentMode = FLEXExplorerModeDefault;
|
||||
[self.delegate explorerViewControllerDidFinish:self];
|
||||
}
|
||||
|
||||
- (void)updateButtonStates {
|
||||
// Move and details only active when an object is selected.
|
||||
FLEXExplorerToolbar *toolbar = self.explorerToolbar;
|
||||
|
||||
toolbar.selectItem.selected = self.currentMode == FLEXExplorerModeSelect;
|
||||
|
||||
// Move only enabled when an object is selected.
|
||||
BOOL hasSelectedObject = self.selectedView != nil;
|
||||
self.explorerToolbar.moveItem.enabled = hasSelectedObject;
|
||||
self.explorerToolbar.selectItem.selected = self.currentMode == FLEXExplorerModeSelect;
|
||||
self.explorerToolbar.moveItem.selected = self.currentMode == FLEXExplorerModeMove;
|
||||
toolbar.moveItem.enabled = hasSelectedObject;
|
||||
toolbar.moveItem.selected = self.currentMode == FLEXExplorerModeMove;
|
||||
|
||||
// Recent only enabled when we have a last active tab
|
||||
toolbar.recentItem.enabled = FLEXTabList.sharedList.activeTab != nil;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Toolbar Dragging
|
||||
|
||||
- (void)setupToolbarGestures {
|
||||
FLEXExplorerToolbar *toolbar = self.explorerToolbar;
|
||||
|
||||
// Pan gesture for dragging.
|
||||
UIPanGestureRecognizer *panGR = [[UIPanGestureRecognizer alloc]
|
||||
[toolbar.dragHandle addGestureRecognizer:[[UIPanGestureRecognizer alloc]
|
||||
initWithTarget:self action:@selector(handleToolbarPanGesture:)
|
||||
];
|
||||
[self.explorerToolbar.dragHandle addGestureRecognizer:panGR];
|
||||
]];
|
||||
|
||||
// Tap gesture for hinting.
|
||||
UITapGestureRecognizer *hintTapGR = [[UITapGestureRecognizer alloc]
|
||||
[toolbar.dragHandle addGestureRecognizer:[[UITapGestureRecognizer alloc]
|
||||
initWithTarget:self action:@selector(handleToolbarHintTapGesture:)
|
||||
];
|
||||
[self.explorerToolbar.dragHandle addGestureRecognizer:hintTapGR];
|
||||
]];
|
||||
|
||||
// Tap gesture for showing additional details
|
||||
self.detailsTapGR = [[UITapGestureRecognizer alloc]
|
||||
initWithTarget:self action:@selector(handleToolbarDetailsTapGesture:)
|
||||
];
|
||||
[self.explorerToolbar.selectedViewDescriptionContainer addGestureRecognizer:self.detailsTapGR];
|
||||
[toolbar.selectedViewDescriptionContainer addGestureRecognizer:self.detailsTapGR];
|
||||
// Swipe gestures for selecting deeper / higher views at a point
|
||||
UISwipeGestureRecognizer *leftSwipe = [[UISwipeGestureRecognizer alloc]
|
||||
initWithTarget:self action:@selector(handleChangeViewAtPointGesture:)
|
||||
];
|
||||
UISwipeGestureRecognizer *rightSwipe = [[UISwipeGestureRecognizer alloc]
|
||||
initWithTarget:self action:@selector(handleChangeViewAtPointGesture:)
|
||||
];
|
||||
leftSwipe.direction = UISwipeGestureRecognizerDirectionLeft;
|
||||
rightSwipe.direction = UISwipeGestureRecognizerDirectionRight;
|
||||
[toolbar.selectedViewDescriptionContainer addGestureRecognizer:leftSwipe];
|
||||
[toolbar.selectedViewDescriptionContainer addGestureRecognizer:rightSwipe];
|
||||
|
||||
// Long press gesture to present tabs manager
|
||||
[toolbar.globalsItem addGestureRecognizer:[[UILongPressGestureRecognizer alloc]
|
||||
initWithTarget:self action:@selector(handleToolbarShowTabsGesture:)
|
||||
]];
|
||||
|
||||
// Long press gesture to present window manager
|
||||
[toolbar.selectItem addGestureRecognizer:[[UILongPressGestureRecognizer alloc]
|
||||
initWithTarget:self action:@selector(handleToolbarWindowManagerGesture:)
|
||||
]];
|
||||
|
||||
// Long press gesture to present view controllers at tap
|
||||
[toolbar.hierarchyItem addGestureRecognizer:[[UILongPressGestureRecognizer alloc]
|
||||
initWithTarget:self action:@selector(handleToolbarShowViewControllersGesture:)
|
||||
]];
|
||||
}
|
||||
|
||||
- (void)handleToolbarPanGesture:(UIPanGestureRecognizer *)panGR {
|
||||
@@ -473,10 +515,7 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
}
|
||||
|
||||
self.explorerToolbar.frame = unconstrainedFrame;
|
||||
|
||||
[NSUserDefaults.standardUserDefaults
|
||||
setDouble:unconstrainedFrame.origin.y forKey:kFLEXToolbarTopMarginDefaultsKey
|
||||
];
|
||||
NSUserDefaults.standardUserDefaults.flex_toolbarTopMargin = unconstrainedFrame.origin.y;
|
||||
}
|
||||
|
||||
- (void)handleToolbarHintTapGesture:(UITapGestureRecognizer *)tapGR {
|
||||
@@ -501,12 +540,49 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
- (void)handleToolbarDetailsTapGesture:(UITapGestureRecognizer *)tapGR {
|
||||
if (tapGR.state == UIGestureRecognizerStateRecognized && self.selectedView) {
|
||||
UIViewController *topStackVC = [FLEXObjectExplorerFactory explorerViewControllerForObject:self.selectedView];
|
||||
[self makeKeyAndPresentViewController:
|
||||
[self presentViewController:
|
||||
[FLEXNavigationController withRootViewController:topStackVC]
|
||||
animated:YES completion:nil];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handleToolbarShowTabsGesture:(UILongPressGestureRecognizer *)sender {
|
||||
if (sender.state == UIGestureRecognizerStateBegan) {
|
||||
// Back up the UIMenuController items since dismissViewController: will attempt to replace them
|
||||
self.appMenuItems = UIMenuController.sharedMenuController.menuItems;
|
||||
|
||||
// Don't use FLEXNavigationController because the tab viewer itself is not a tab
|
||||
[super presentViewController:[[UINavigationController alloc]
|
||||
initWithRootViewController:[FLEXTabsViewController new]
|
||||
] animated:YES completion:nil];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handleToolbarWindowManagerGesture:(UILongPressGestureRecognizer *)sender {
|
||||
if (sender.state == UIGestureRecognizerStateBegan) {
|
||||
// Back up the UIMenuController items since dismissViewController: will attempt to replace them
|
||||
self.appMenuItems = UIMenuController.sharedMenuController.menuItems;
|
||||
|
||||
[super presentViewController:[FLEXNavigationController
|
||||
withRootViewController:[FLEXWindowManagerController new]
|
||||
] animated:YES completion:nil];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handleToolbarShowViewControllersGesture:(UILongPressGestureRecognizer *)sender {
|
||||
if (sender.state == UIGestureRecognizerStateBegan && self.viewsAtTapPoint.count) {
|
||||
// Back up the UIMenuController items since dismissViewController: will attempt to replace them
|
||||
self.appMenuItems = UIMenuController.sharedMenuController.menuItems;
|
||||
|
||||
UIViewController *list = [FLEXViewControllersViewController
|
||||
controllersForViews:self.viewsAtTapPoint
|
||||
];
|
||||
[self presentViewController:
|
||||
[FLEXNavigationController withRootViewController:list
|
||||
] animated:YES completion:nil];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - View Selection
|
||||
|
||||
@@ -522,6 +598,22 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handleChangeViewAtPointGesture:(UISwipeGestureRecognizer *)sender {
|
||||
NSInteger max = self.viewsAtTapPoint.count - 1;
|
||||
NSInteger currentIdx = [self.viewsAtTapPoint indexOfObject:self.selectedView];
|
||||
switch (sender.direction) {
|
||||
case UISwipeGestureRecognizerDirectionLeft:
|
||||
self.selectedView = self.viewsAtTapPoint[MIN(max, currentIdx + 1)];
|
||||
break;
|
||||
case UISwipeGestureRecognizerDirectionRight:
|
||||
self.selectedView = self.viewsAtTapPoint[MAX(0, currentIdx - 1)];
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)updateOutlineViewsForSelectionPoint:(CGPoint)selectionPointInWindow {
|
||||
[self removeAndClearOutlineViews];
|
||||
|
||||
@@ -565,8 +657,8 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
}
|
||||
|
||||
- (NSArray<UIView *> *)viewsAtPoint:(CGPoint)tapPointInWindow skipHiddenViews:(BOOL)skipHidden {
|
||||
NSMutableArray<UIView *> *views = [NSMutableArray array];
|
||||
for (UIWindow *window in [FLEXUtility allWindows]) {
|
||||
NSMutableArray<UIView *> *views = [NSMutableArray new];
|
||||
for (UIWindow *window in FLEXUtility.allWindows) {
|
||||
// Don't include the explorer's own window or subviews.
|
||||
if (window != self.view.window && [window pointInside:tapPointInWindow withEvent:nil]) {
|
||||
[views addObject:window];
|
||||
@@ -582,8 +674,8 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
// Select in the window that would handle the touch, but don't just use the result of
|
||||
// hitTest:withEvent: so we can still select views with interaction disabled.
|
||||
// Default to the the application's key window if none of the windows want the touch.
|
||||
UIWindow *windowForSelection = [UIApplication.sharedApplication keyWindow];
|
||||
for (UIWindow *window in [FLEXUtility allWindows].reverseObjectEnumerator) {
|
||||
UIWindow *windowForSelection = UIApplication.sharedApplication.keyWindow;
|
||||
for (UIWindow *window in FLEXUtility.allWindows.reverseObjectEnumerator) {
|
||||
// Ignore the explorer's own window.
|
||||
if (window != self.view.window) {
|
||||
if ([window hitTest:tapPointInWindow withEvent:nil]) {
|
||||
@@ -600,7 +692,7 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
- (NSArray<UIView *> *)recursiveSubviewsAtPoint:(CGPoint)pointInView
|
||||
inView:(UIView *)view
|
||||
skipHiddenViews:(BOOL)skipHidden {
|
||||
NSMutableArray<UIView *> *subviewsAtPoint = [NSMutableArray array];
|
||||
NSMutableArray<UIView *> *subviewsAtPoint = [NSMutableArray new];
|
||||
for (UIView *subview in view.subviews) {
|
||||
BOOL isHidden = subview.hidden || subview.alpha < 0.01;
|
||||
if (skipHidden && isHidden) {
|
||||
@@ -737,20 +829,9 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Modal Dismissal
|
||||
|
||||
- (void)presentationControllerDidDismiss:(UIPresentationController *)presentationController {
|
||||
[self presentedViewControllerDidDismiss];
|
||||
}
|
||||
|
||||
- (void)presentedViewControllerDidDismiss {
|
||||
[self resignKeyAndDismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Modal Presentation and Window Management
|
||||
|
||||
- (void)makeKeyAndPresentViewController:(UINavigationController *)toPresent
|
||||
- (void)presentViewController:(UIViewController *)toPresent
|
||||
animated:(BOOL)animated
|
||||
completion:(void (^)(void))completion {
|
||||
// Make our window key to correctly handle input.
|
||||
@@ -762,38 +843,15 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
}
|
||||
|
||||
// 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;
|
||||
// Initialize custom menu items for explorer screen
|
||||
UIMenuItem *copyObjectAddress = [[UIMenuItem alloc]
|
||||
initWithTitle:@"Copy Address"
|
||||
action:NSSelectorFromString(@"copyObjectAddress:")
|
||||
];
|
||||
UIMenuController.sharedMenuController.menuItems = @[copyObjectAddress];
|
||||
[UIMenuController.sharedMenuController update];
|
||||
|
||||
// Add the "Done" button to the navigation controller's view controller if it doesn't have one
|
||||
// (the hierarchy screen adds it's own done button in order to pass data between us)
|
||||
if (!toPresent.topViewController.navigationItem.rightBarButtonItem) {
|
||||
toPresent.topViewController.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc]
|
||||
initWithBarButtonSystemItem:UIBarButtonSystemItemDone
|
||||
target:self
|
||||
action:@selector(presentedViewControllerDidDismiss)
|
||||
];
|
||||
}
|
||||
|
||||
// Make myself the delegate for sheets presented modally so we can do the
|
||||
// proper cleanup when sheets are dragged to dismiss without the done button
|
||||
if (@available(iOS 13, *)) {
|
||||
toPresent.presentationController.delegate = self;
|
||||
}
|
||||
|
||||
// Show the view controller.
|
||||
[self presentViewController:toPresent animated:animated completion:completion];
|
||||
// Show the view controller
|
||||
[super presentViewController:toPresent animated:animated completion:completion];
|
||||
}
|
||||
|
||||
- (void)resignKeyAndDismissViewControllerAnimated:(BOOL)animated
|
||||
completion:(void (^)(void))completion
|
||||
{
|
||||
- (void)dismissViewControllerAnimated:(BOOL)animated completion:(void (^)(void))completion {
|
||||
UIWindow *appWindow = self.window.previousKeyWindow;
|
||||
[appWindow makeKeyWindow];
|
||||
[appWindow.rootViewController setNeedsStatusBarAppearanceUpdate];
|
||||
@@ -809,7 +867,9 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
// scroll to top, but below FLEX otherwise for exploration.
|
||||
[self statusWindow].windowLevel = UIWindowLevelStatusBar;
|
||||
|
||||
[self dismissViewControllerAnimated:animated completion:completion];
|
||||
[self updateButtonStates];
|
||||
|
||||
[super dismissViewControllerAnimated:animated completion:completion];
|
||||
}
|
||||
|
||||
- (BOOL)wantsWindowToBecomeKey
|
||||
@@ -820,9 +880,9 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
- (void)toggleToolWithViewControllerProvider:(UINavigationController *(^)(void))future
|
||||
completion:(void(^)(void))completion {
|
||||
if (self.presentedViewController) {
|
||||
[self resignKeyAndDismissViewControllerAnimated:YES completion:completion];
|
||||
[self dismissViewControllerAnimated:YES completion:completion];
|
||||
} else if (future) {
|
||||
[self makeKeyAndPresentViewController:future() animated:YES completion:completion];
|
||||
[self presentViewController:future() animated:YES completion:completion];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -843,8 +903,8 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
|
||||
- (void)toggleMoveTool {
|
||||
if (self.currentMode == FLEXExplorerModeMove) {
|
||||
self.currentMode = FLEXExplorerModeDefault;
|
||||
} else {
|
||||
self.currentMode = FLEXExplorerModeSelect;
|
||||
} else if (self.currentMode == FLEXExplorerModeSelect && self.selectedView) {
|
||||
self.currentMode = FLEXExplorerModeMove;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// FLEXViewControllersViewController.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 2/13/20.
|
||||
// Copyright © 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXFilteringTableViewController.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FLEXViewControllersViewController : FLEXFilteringTableViewController
|
||||
|
||||
+ (instancetype)controllersForViews:(NSArray<UIView *> *)views;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,78 @@
|
||||
//
|
||||
// FLEXViewControllersViewController.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 2/13/20.
|
||||
// Copyright © 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXViewControllersViewController.h"
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
#import "FLEXMutableListSection.h"
|
||||
#import "FLEXUtility.h"
|
||||
|
||||
@interface FLEXViewControllersViewController ()
|
||||
@property (nonatomic, readonly) FLEXMutableListSection *section;
|
||||
@property (nonatomic, readonly) NSArray<UIViewController *> *controllers;
|
||||
@end
|
||||
|
||||
@implementation FLEXViewControllersViewController
|
||||
@dynamic sections, allSections;
|
||||
|
||||
#pragma mark - Initialization
|
||||
|
||||
+ (instancetype)controllersForViews:(NSArray<UIView *> *)views {
|
||||
return [[self alloc] initWithViews:views];
|
||||
}
|
||||
|
||||
- (id)initWithViews:(NSArray<UIView *> *)views {
|
||||
NSParameterAssert(views.count);
|
||||
|
||||
self = [self initWithStyle:UITableViewStylePlain];
|
||||
if (self) {
|
||||
_controllers = [views flex_mapped:^id(UIView *view, NSUInteger idx) {
|
||||
return [FLEXUtility viewControllerForView:view];
|
||||
}];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
self.title = @"View Controllers at Tap";
|
||||
[self disableToolbar];
|
||||
}
|
||||
|
||||
- (NSArray<FLEXTableViewSection *> *)makeSections {
|
||||
_section = [FLEXMutableListSection list:self.controllers
|
||||
cellConfiguration:^(UITableViewCell *cell, UIViewController *controller, NSInteger row) {
|
||||
cell.textLabel.text = [NSString
|
||||
stringWithFormat:@"%@ — %p", NSStringFromClass(controller.class), controller
|
||||
];
|
||||
cell.detailTextLabel.text = controller.view.description;
|
||||
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
|
||||
cell.textLabel.lineBreakMode = NSLineBreakByTruncatingTail;
|
||||
} filterMatcher:^BOOL(NSString *filterText, UIViewController *controller) {
|
||||
return [NSStringFromClass(controller.class) localizedCaseInsensitiveContainsString:filterText];
|
||||
}];
|
||||
|
||||
self.section.selectionHandler = ^(UIViewController *host, UIViewController *controller) {
|
||||
[host.navigationController pushViewController:
|
||||
[FLEXObjectExplorerFactory explorerViewControllerForObject:controller]
|
||||
animated:YES];
|
||||
};
|
||||
|
||||
self.section.customTitle = @"View Controllers";
|
||||
return @[self.section];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
- (void)dismissAnimated {
|
||||
[self dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 4/13/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 4/13/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXWindow.h"
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// FLEXWindowManagerController.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 2/6/20.
|
||||
// Copyright © 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXTableViewController.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FLEXWindowManagerController : FLEXTableViewController
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,302 @@
|
||||
//
|
||||
// FLEXWindowManagerController.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 2/6/20.
|
||||
// Copyright © 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXWindowManagerController.h"
|
||||
#import "FLEXManager+Private.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
|
||||
@interface FLEXWindowManagerController ()
|
||||
@property (nonatomic) UIWindow *keyWindow;
|
||||
@property (nonatomic, copy) NSString *keyWindowSubtitle;
|
||||
@property (nonatomic, copy) NSArray<UIWindow *> *windows;
|
||||
@property (nonatomic, copy) NSArray<NSString *> *windowSubtitles;
|
||||
@property (nonatomic, copy) NSArray<UIScene *> *scenes API_AVAILABLE(ios(13));
|
||||
@property (nonatomic, copy) NSArray<NSString *> *sceneSubtitles;
|
||||
@property (nonatomic, copy) NSArray<NSArray *> *sections;
|
||||
@end
|
||||
|
||||
@implementation FLEXWindowManagerController
|
||||
|
||||
#pragma mark - Initialization
|
||||
|
||||
- (id)init {
|
||||
return [self initWithStyle:UITableViewStylePlain];
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
self.title = @"Windows";
|
||||
if (@available(iOS 13, *)) {
|
||||
self.title = @"Windows and Scenes";
|
||||
}
|
||||
|
||||
[self disableToolbar];
|
||||
[self reloadData];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
- (void)reloadData {
|
||||
self.keyWindow = UIApplication.sharedApplication.keyWindow;
|
||||
self.windows = UIApplication.sharedApplication.windows;
|
||||
self.keyWindowSubtitle = self.windowSubtitles[[self.windows indexOfObject:self.keyWindow]];
|
||||
self.windowSubtitles = [self.windows flex_mapped:^id(UIWindow *window, NSUInteger idx) {
|
||||
return [NSString stringWithFormat:@"Level: %@ — Root: %@",
|
||||
@(window.windowLevel), window.rootViewController
|
||||
];
|
||||
}];
|
||||
|
||||
if (@available(iOS 13, *)) {
|
||||
self.scenes = UIApplication.sharedApplication.connectedScenes.allObjects;
|
||||
self.sceneSubtitles = [self.scenes flex_mapped:^id(UIScene *scene, NSUInteger idx) {
|
||||
return [self sceneDescription:scene];
|
||||
}];
|
||||
|
||||
self.sections = @[@[self.keyWindow], self.windows, self.scenes];
|
||||
} else {
|
||||
self.sections = @[@[self.keyWindow], self.windows];
|
||||
}
|
||||
|
||||
[self.tableView reloadData];
|
||||
}
|
||||
|
||||
- (void)dismissAnimated {
|
||||
[self dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (void)showRevertOrDismissAlert:(void(^)())revertBlock {
|
||||
[self.tableView deselectRowAtIndexPath:self.tableView.indexPathForSelectedRow animated:YES];
|
||||
[self reloadData];
|
||||
[self.tableView reloadData];
|
||||
|
||||
UIWindow *highestWindow = UIApplication.sharedApplication.keyWindow;
|
||||
UIWindowLevel maxLevel = 0;
|
||||
for (UIWindow *window in UIApplication.sharedApplication.windows) {
|
||||
if (window.windowLevel > maxLevel) {
|
||||
maxLevel = window.windowLevel;
|
||||
highestWindow = window;
|
||||
}
|
||||
}
|
||||
|
||||
[FLEXAlert makeAlert:^(FLEXAlert *make) {
|
||||
make.title(@"Keep Changes?");
|
||||
make.message(@"If you do not wish to keep these settings, choose 'Revert Changes' below.");
|
||||
|
||||
make.button(@"Keep Changes").destructiveStyle();
|
||||
make.button(@"Keep Changes and Dismiss").destructiveStyle().handler(^(NSArray<NSString *> *strings) {
|
||||
[self dismissAnimated];
|
||||
});
|
||||
make.button(@"Revert Changes").cancelStyle().handler(^(NSArray<NSString *> *strings) {
|
||||
revertBlock();
|
||||
[self reloadData];
|
||||
[self.tableView reloadData];
|
||||
});
|
||||
} showFrom:[FLEXUtility topViewControllerInWindow:highestWindow]];
|
||||
}
|
||||
|
||||
- (NSString *)sceneDescription:(UIScene *)scene API_AVAILABLE(ios(13)) {
|
||||
NSString *state = [self stringFromSceneState:scene.activationState];
|
||||
NSString *title = scene.title.length ? scene.title : nil;
|
||||
NSString *suffix = nil;
|
||||
|
||||
if ([scene isKindOfClass:[UIWindowScene class]]) {
|
||||
UIWindowScene *windowScene = (id)scene;
|
||||
suffix = FLEXPluralString(windowScene.windows.count, @"windows", @"window");
|
||||
}
|
||||
|
||||
NSMutableString *description = state.mutableCopy;
|
||||
if (title) {
|
||||
[description appendFormat:@" — %@", title];
|
||||
}
|
||||
if (suffix) {
|
||||
[description appendFormat:@" — %@", suffix];
|
||||
}
|
||||
|
||||
return description.copy;
|
||||
}
|
||||
|
||||
- (NSString *)stringFromSceneState:(UISceneActivationState)state API_AVAILABLE(ios(13)) {
|
||||
switch (state) {
|
||||
case UISceneActivationStateUnattached:
|
||||
return @"Unattached";
|
||||
case UISceneActivationStateForegroundActive:
|
||||
return @"Active";
|
||||
case UISceneActivationStateForegroundInactive:
|
||||
return @"Inactive";
|
||||
case UISceneActivationStateBackground:
|
||||
return @"Backgrounded";
|
||||
}
|
||||
|
||||
return [NSString stringWithFormat:@"Unknown state: %@", @(state)];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Table View Data Source
|
||||
|
||||
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
|
||||
return self.sections.count;
|
||||
}
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
|
||||
return self.sections[section].count;
|
||||
}
|
||||
|
||||
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
|
||||
switch (section) {
|
||||
case 0: return @"Key Window";
|
||||
case 1: return @"Windows";
|
||||
case 2: return @"Connected Scenes";
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kFLEXDetailCell forIndexPath:indexPath];
|
||||
cell.accessoryType = UITableViewCellAccessoryDetailButton;
|
||||
cell.textLabel.lineBreakMode = NSLineBreakByTruncatingTail;
|
||||
|
||||
UIWindow *window = nil;
|
||||
NSString *subtitle = nil;
|
||||
|
||||
switch (indexPath.section) {
|
||||
case 0:
|
||||
window = self.keyWindow;
|
||||
subtitle = self.keyWindowSubtitle;
|
||||
break;
|
||||
case 1:
|
||||
window = self.windows[indexPath.row];
|
||||
subtitle = self.windowSubtitles[indexPath.row];
|
||||
break;
|
||||
case 2:
|
||||
if (@available(iOS 13, *)) {
|
||||
UIScene *scene = self.scenes[indexPath.row];
|
||||
cell.textLabel.text = scene.description;
|
||||
cell.detailTextLabel.text = self.sceneSubtitles[indexPath.row];
|
||||
return cell;
|
||||
}
|
||||
}
|
||||
|
||||
cell.textLabel.text = window.description;
|
||||
cell.detailTextLabel.text = [NSString
|
||||
stringWithFormat:@"Level: %@ — Root: %@",
|
||||
@((NSInteger)window.windowLevel), window.rootViewController.class
|
||||
];
|
||||
|
||||
return cell;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Table View Delegate
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
UIWindow *window = nil;
|
||||
NSString *subtitle = nil;
|
||||
FLEXWindow *flex = FLEXManager.sharedManager.explorerWindow;
|
||||
|
||||
id cancelHandler = ^{
|
||||
[self.tableView deselectRowAtIndexPath:self.tableView.indexPathForSelectedRow animated:YES];
|
||||
};
|
||||
|
||||
switch (indexPath.section) {
|
||||
case 0:
|
||||
window = self.keyWindow;
|
||||
subtitle = self.keyWindowSubtitle;
|
||||
break;
|
||||
case 1:
|
||||
window = self.windows[indexPath.row];
|
||||
subtitle = self.windowSubtitles[indexPath.row];
|
||||
break;
|
||||
case 2:
|
||||
if (@available(iOS 13, *)) {
|
||||
UIScene *scene = self.scenes[indexPath.row];
|
||||
UIWindowScene *oldScene = flex.windowScene;
|
||||
BOOL isWindowScene = [scene isKindOfClass:[UIWindowScene class]];
|
||||
BOOL isFLEXScene = isWindowScene ? flex.windowScene == scene : NO;
|
||||
|
||||
[FLEXAlert makeAlert:^(FLEXAlert *make) {
|
||||
make.title(NSStringFromClass(scene.class));
|
||||
|
||||
if (isWindowScene) {
|
||||
if (isFLEXScene) {
|
||||
make.message(@"Already the FLEX window scene");
|
||||
}
|
||||
|
||||
make.button(@"Set as FLEX Window Scene")
|
||||
.handler(^(NSArray<NSString *> *strings) {
|
||||
flex.windowScene = (id)scene;
|
||||
[self showRevertOrDismissAlert:^{
|
||||
flex.windowScene = oldScene;
|
||||
}];
|
||||
}).enabled(!isFLEXScene);
|
||||
make.button(@"Cancel").cancelStyle();
|
||||
} else {
|
||||
make.message(@"Not a UIWindowScene");
|
||||
make.button(@"Dismiss").cancelStyle().handler(cancelHandler);
|
||||
}
|
||||
} showFrom:self];
|
||||
}
|
||||
}
|
||||
|
||||
__block UIWindow *targetWindow = nil, *oldKeyWindow = nil;
|
||||
__block UIWindowLevel oldLevel;
|
||||
__block BOOL wasVisible;
|
||||
|
||||
subtitle = [subtitle stringByAppendingString:
|
||||
@"\n\n1) Adjust the FLEX window level relative to this window,\n"
|
||||
"2) adjust this window's level relative to the FLEX window,\n"
|
||||
"3) set this window's level to a specific value, or\n"
|
||||
"4) make this window the key window if it isn't already."
|
||||
];
|
||||
|
||||
[FLEXAlert makeAlert:^(FLEXAlert *make) {
|
||||
make.title(NSStringFromClass(window.class)).message(subtitle);
|
||||
make.button(@"Adjust FLEX Window Level").handler(^(NSArray<NSString *> *strings) {
|
||||
targetWindow = flex; oldLevel = flex.windowLevel;
|
||||
flex.windowLevel = window.windowLevel + strings.firstObject.integerValue;
|
||||
|
||||
[self showRevertOrDismissAlert:^{ targetWindow.windowLevel = oldLevel; }];
|
||||
});
|
||||
make.button(@"Adjust This Window's Level").handler(^(NSArray<NSString *> *strings) {
|
||||
targetWindow = window; oldLevel = window.windowLevel;
|
||||
window.windowLevel = flex.windowLevel + strings.firstObject.integerValue;
|
||||
|
||||
[self showRevertOrDismissAlert:^{ targetWindow.windowLevel = oldLevel; }];
|
||||
});
|
||||
make.button(@"Set This Window's Level").handler(^(NSArray<NSString *> *strings) {
|
||||
targetWindow = window; oldLevel = window.windowLevel;
|
||||
window.windowLevel = strings.firstObject.integerValue;
|
||||
|
||||
[self showRevertOrDismissAlert:^{ targetWindow.windowLevel = oldLevel; }];
|
||||
});
|
||||
make.button(@"Make Key And Visible").handler(^(NSArray<NSString *> *strings) {
|
||||
oldKeyWindow = UIApplication.sharedApplication.keyWindow;
|
||||
wasVisible = window.hidden;
|
||||
[window makeKeyAndVisible];
|
||||
|
||||
[self showRevertOrDismissAlert:^{
|
||||
window.hidden = wasVisible;
|
||||
[oldKeyWindow makeKeyWindow];
|
||||
}];
|
||||
}).enabled(!window.isKeyWindow && !window.hidden);
|
||||
make.button(@"Cancel").cancelStyle().handler(cancelHandler);
|
||||
|
||||
make.textField(@"+/- window level, i.e. 5 or -10");
|
||||
} showFrom:self];
|
||||
}
|
||||
|
||||
- (void)tableView:(UITableView *)tableView accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)ip {
|
||||
[self.navigationController pushViewController:
|
||||
[FLEXObjectExplorerFactory explorerViewControllerForObject:self.sections[ip.section][ip.row]]
|
||||
animated:YES];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,45 @@
|
||||
//
|
||||
// FLEXTabList.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 2/1/20.
|
||||
// Copyright © 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FLEXTabList : NSObject
|
||||
|
||||
@property (nonatomic, readonly, class) FLEXTabList *sharedList;
|
||||
|
||||
@property (nonatomic, readonly, nullable) UINavigationController *activeTab;
|
||||
@property (nonatomic, readonly) NSArray<UINavigationController *> *openTabs;
|
||||
/// Snapshots of each tab when they were last active.
|
||||
@property (nonatomic, readonly) NSArray<UIImage *> *openTabSnapshots;
|
||||
/// \c NSNotFound if no tabs are present.
|
||||
/// Setting this property changes the active tab to one of the already open tabs.
|
||||
@property (nonatomic) NSInteger activeTabIndex;
|
||||
|
||||
/// Adds a new tab and sets the new tab as the active tab.
|
||||
- (void)addTab:(UINavigationController *)newTab;
|
||||
/// Closes the given tab. If this tab was the active tab,
|
||||
/// the most recent tab before that becomes the active tab.
|
||||
- (void)closeTab:(UINavigationController *)tab;
|
||||
/// Closes a tab at the given index. If this tab was the active tab,
|
||||
/// the most recent tab before that becomes the active tab.
|
||||
- (void)closeTabAtIndex:(NSInteger)idx;
|
||||
/// Closes all of the tabs at the given indexes. If the active tab
|
||||
/// is included, the most recent still-open tab becomes the active tab.
|
||||
- (void)closeTabsAtIndexes:(NSIndexSet *)indexes;
|
||||
/// A shortcut to close the active tab.
|
||||
- (void)closeActiveTab;
|
||||
/// A shortcut to close \e every tab.
|
||||
- (void)closeAllTabs;
|
||||
|
||||
- (void)updateSnapshotForActiveTab;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,133 @@
|
||||
//
|
||||
// FLEXTabList.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 2/1/20.
|
||||
// Copyright © 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXTabList.h"
|
||||
#import "FLEXUtility.h"
|
||||
|
||||
@interface FLEXTabList () {
|
||||
NSMutableArray *_openTabs;
|
||||
NSMutableArray *_openTabSnapshots;
|
||||
}
|
||||
@end
|
||||
#pragma mark -
|
||||
@implementation FLEXTabList
|
||||
|
||||
#pragma mark Initialization
|
||||
|
||||
+ (FLEXTabList *)sharedList {
|
||||
static FLEXTabList *sharedList = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
sharedList = [self new];
|
||||
});
|
||||
|
||||
return sharedList;
|
||||
}
|
||||
|
||||
- (id)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_openTabs = [NSMutableArray new];
|
||||
_openTabSnapshots = [NSMutableArray new];
|
||||
_activeTabIndex = NSNotFound;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Private
|
||||
|
||||
- (void)chooseNewActiveTab {
|
||||
if (self.openTabs.count) {
|
||||
self.activeTabIndex = self.openTabs.count - 1;
|
||||
} else {
|
||||
self.activeTabIndex = NSNotFound;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Public
|
||||
|
||||
- (void)setActiveTabIndex:(NSInteger)idx {
|
||||
NSParameterAssert(idx < self.openTabs.count || idx == NSNotFound);
|
||||
if (_activeTabIndex == idx) return;
|
||||
|
||||
_activeTabIndex = idx;
|
||||
_activeTab = (idx == NSNotFound) ? nil : self.openTabs[idx];
|
||||
}
|
||||
|
||||
- (void)addTab:(UINavigationController *)newTab {
|
||||
NSParameterAssert(newTab);
|
||||
|
||||
// Update snapshot of the last active tab
|
||||
if (self.activeTab) {
|
||||
[self updateSnapshotForActiveTab];
|
||||
}
|
||||
|
||||
// Add new tab and snapshot,
|
||||
// update active tab and index
|
||||
[_openTabs addObject:newTab];
|
||||
[_openTabSnapshots addObject:[FLEXUtility previewImageForView:newTab.view]];
|
||||
_activeTab = newTab;
|
||||
_activeTabIndex = self.openTabs.count - 1;
|
||||
}
|
||||
|
||||
- (void)closeTab:(UINavigationController *)tab {
|
||||
NSParameterAssert(tab);
|
||||
NSParameterAssert([self.openTabs containsObject:tab]);
|
||||
NSInteger idx = [self.openTabs indexOfObject:tab];
|
||||
|
||||
[self closeTabAtIndex:idx];
|
||||
}
|
||||
|
||||
- (void)closeTabAtIndex:(NSInteger)idx {
|
||||
NSParameterAssert(idx < self.openTabs.count);
|
||||
|
||||
// Remove old tab and snapshot
|
||||
[_openTabs removeObjectAtIndex:idx];
|
||||
[_openTabSnapshots removeObjectAtIndex:idx];
|
||||
|
||||
// Update active tab and index if needed
|
||||
if (self.activeTabIndex == idx) {
|
||||
[self chooseNewActiveTab];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)closeTabsAtIndexes:(NSIndexSet *)indexes {
|
||||
// Remove old tabs and snapshot
|
||||
[_openTabs removeObjectsAtIndexes:indexes];
|
||||
[_openTabSnapshots removeObjectsAtIndexes:indexes];
|
||||
|
||||
// Update active tab and index if needed
|
||||
if ([indexes containsIndex:self.activeTabIndex]) {
|
||||
[self chooseNewActiveTab];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)closeActiveTab {
|
||||
[self closeTab:self.activeTab];
|
||||
}
|
||||
|
||||
- (void)closeAllTabs {
|
||||
// Remove tabs and snapshots
|
||||
[_openTabs removeAllObjects];
|
||||
[_openTabSnapshots removeAllObjects];
|
||||
|
||||
// Update active tab index
|
||||
self.activeTabIndex = NSNotFound;
|
||||
}
|
||||
|
||||
- (void)updateSnapshotForActiveTab {
|
||||
if (self.activeTabIndex != NSNotFound) {
|
||||
UIImage *newSnapshot = [FLEXUtility previewImageForView:self.activeTab.view];
|
||||
[_openTabSnapshots replaceObjectAtIndex:self.activeTabIndex withObject:newSnapshot];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// FLEXTabsViewController.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 2/4/20.
|
||||
// Copyright © 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXTableViewController.h"
|
||||
|
||||
@interface FLEXTabsViewController : FLEXTableViewController
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,335 @@
|
||||
//
|
||||
// FLEXTabsViewController.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 2/4/20.
|
||||
// Copyright © 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXTabsViewController.h"
|
||||
#import "FLEXNavigationController.h"
|
||||
#import "FLEXTabList.h"
|
||||
#import "FLEXBookmarkManager.h"
|
||||
#import "FLEXTableView.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXColor.h"
|
||||
#import "UIBarButtonItem+FLEX.h"
|
||||
#import "FLEXExplorerViewController.h"
|
||||
#import "FLEXGlobalsViewController.h"
|
||||
#import "FLEXBookmarksViewController.h"
|
||||
|
||||
@interface FLEXTabsViewController ()
|
||||
@property (nonatomic, copy) NSArray<UINavigationController *> *openTabs;
|
||||
@property (nonatomic, copy) NSArray<UIImage *> *tabSnapshots;
|
||||
@property (nonatomic) NSInteger activeIndex;
|
||||
@property (nonatomic) BOOL presentNewActiveTabOnDismiss;
|
||||
|
||||
@property (nonatomic, readonly) FLEXExplorerViewController *corePresenter;
|
||||
@end
|
||||
|
||||
@implementation FLEXTabsViewController
|
||||
|
||||
#pragma mark - Initialization
|
||||
|
||||
- (id)init {
|
||||
return [self initWithStyle:UITableViewStylePlain];
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
self.title = @"Open Tabs";
|
||||
self.navigationController.hidesBarsOnSwipe = NO;
|
||||
self.tableView.allowsMultipleSelectionDuringEditing = YES;
|
||||
|
||||
[self reloadData:NO];
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated {
|
||||
[super viewWillAppear:animated];
|
||||
[self setupDefaultBarItems];
|
||||
}
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated {
|
||||
[super viewDidAppear:animated];
|
||||
|
||||
// Instead of updating the active snapshot before we present,
|
||||
// we update it after we present to avoid pre-presenation latency
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[FLEXTabList.sharedList updateSnapshotForActiveTab];
|
||||
[self reloadData:NO];
|
||||
[self.tableView reloadData];
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
/// @param trackActiveTabDelta whether to check if the active
|
||||
/// tab changed and needs to be presented upon "Done" dismissal.
|
||||
/// @return whether the active tab changed or not (if there are any tabs left)
|
||||
- (BOOL)reloadData:(BOOL)trackActiveTabDelta {
|
||||
BOOL activeTabDidChange = NO;
|
||||
FLEXTabList *list = FLEXTabList.sharedList;
|
||||
|
||||
// Flag to enable check to determine whether
|
||||
if (trackActiveTabDelta) {
|
||||
NSInteger oldActiveIndex = self.activeIndex;
|
||||
if (oldActiveIndex != list.activeTabIndex && list.activeTabIndex != NSNotFound) {
|
||||
self.presentNewActiveTabOnDismiss = YES;
|
||||
activeTabDidChange = YES;
|
||||
} else if (self.presentNewActiveTabOnDismiss) {
|
||||
// If we had something to present before, now we don't
|
||||
// (i.e. activeTabIndex == NSNotFound)
|
||||
self.presentNewActiveTabOnDismiss = NO;
|
||||
}
|
||||
}
|
||||
|
||||
// We assume the tabs aren't going to change out from under us, since
|
||||
// presenting any other tool via keyboard shortcuts should dismiss us first
|
||||
self.openTabs = list.openTabs;
|
||||
self.tabSnapshots = list.openTabSnapshots;
|
||||
self.activeIndex = list.activeTabIndex;
|
||||
|
||||
return activeTabDidChange;
|
||||
}
|
||||
|
||||
- (void)reloadActiveTabRowIfChanged:(BOOL)activeTabChanged {
|
||||
// Refresh the newly active tab row if needed
|
||||
if (activeTabChanged) {
|
||||
NSIndexPath *active = [NSIndexPath
|
||||
indexPathForRow:self.activeIndex inSection:0
|
||||
];
|
||||
[self.tableView reloadRowsAtIndexPaths:@[active] withRowAnimation:UITableViewRowAnimationNone];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setupDefaultBarItems {
|
||||
self.navigationItem.rightBarButtonItem = FLEXBarButtonItemSystem(Done, self, @selector(dismissAnimated));
|
||||
self.toolbarItems = @[
|
||||
UIBarButtonItem.flex_fixedSpace,
|
||||
UIBarButtonItem.flex_flexibleSpace,
|
||||
FLEXBarButtonItemSystem(Add, self, @selector(addTabButtonPressed)),
|
||||
UIBarButtonItem.flex_flexibleSpace,
|
||||
FLEXBarButtonItemSystem(Edit, self, @selector(toggleEditing)),
|
||||
];
|
||||
|
||||
// Disable editing if no tabs available
|
||||
self.toolbarItems.lastObject.enabled = self.openTabs.count > 0;
|
||||
}
|
||||
|
||||
- (void)setupEditingBarItems {
|
||||
self.navigationItem.rightBarButtonItem = nil;
|
||||
self.toolbarItems = @[
|
||||
[UIBarButtonItem itemWithTitle:@"Close All" target:self action:@selector(closeAllButtonPressed)],
|
||||
UIBarButtonItem.flex_flexibleSpace,
|
||||
[UIBarButtonItem disabledSystemItem:UIBarButtonSystemItemAdd],
|
||||
UIBarButtonItem.flex_flexibleSpace,
|
||||
// We use a non-system done item because we change its title dynamically
|
||||
[UIBarButtonItem doneStyleitemWithTitle:@"Done" target:self action:@selector(toggleEditing)]
|
||||
];
|
||||
|
||||
self.toolbarItems.firstObject.tintColor = FLEXColor.destructiveColor;
|
||||
}
|
||||
|
||||
- (FLEXExplorerViewController *)corePresenter {
|
||||
// We must be presented by a FLEXExplorerViewController, or presented
|
||||
// by another view controller that was presented by FLEXExplorerViewController
|
||||
FLEXExplorerViewController *presenter = (id)self.presentingViewController;
|
||||
presenter = (id)presenter.presentingViewController ?: presenter;
|
||||
NSAssert(
|
||||
[presenter isKindOfClass:[FLEXExplorerViewController class]],
|
||||
@"The tabs view controller expects to be presented by the explorer controller"
|
||||
);
|
||||
return presenter;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Button Actions
|
||||
|
||||
- (void)dismissAnimated {
|
||||
if (self.presentNewActiveTabOnDismiss) {
|
||||
// The active tab was closed so we need to present the new one
|
||||
UIViewController *activeTab = FLEXTabList.sharedList.activeTab;
|
||||
FLEXExplorerViewController *presenter = self.corePresenter;
|
||||
[presenter dismissViewControllerAnimated:YES completion:^{
|
||||
[presenter presentViewController:activeTab animated:YES completion:nil];
|
||||
}];
|
||||
} else if (self.activeIndex == NSNotFound) {
|
||||
// The only tab was closed, so dismiss everything
|
||||
[self.corePresenter dismissViewControllerAnimated:YES completion:nil];
|
||||
} else {
|
||||
// Simple dismiss with the same active tab, only dismiss myself
|
||||
[self dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)toggleEditing {
|
||||
NSArray<NSIndexPath *> *selected = self.tableView.indexPathsForSelectedRows;
|
||||
self.editing = !self.editing;
|
||||
|
||||
if (self.isEditing) {
|
||||
[self setupEditingBarItems];
|
||||
} else {
|
||||
[self setupDefaultBarItems];
|
||||
|
||||
// Get index set of tabs to close
|
||||
NSMutableIndexSet *indexes = [NSMutableIndexSet new];
|
||||
for (NSIndexPath *ip in selected) {
|
||||
[indexes addIndex:ip.row];
|
||||
}
|
||||
|
||||
if (selected.count) {
|
||||
// Close tabs and update data source
|
||||
[FLEXTabList.sharedList closeTabsAtIndexes:indexes];
|
||||
BOOL activeTabChanged = [self reloadData:YES];
|
||||
|
||||
// Remove deleted rows
|
||||
[self.tableView deleteRowsAtIndexPaths:selected withRowAnimation:UITableViewRowAnimationAutomatic];
|
||||
|
||||
// Refresh the newly active tab row if needed
|
||||
[self reloadActiveTabRowIfChanged:activeTabChanged];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)addTabButtonPressed {
|
||||
if (FLEXBookmarkManager.bookmarks.count) {
|
||||
[FLEXAlert makeSheet:^(FLEXAlert *make) {
|
||||
make.title(@"New Tab");
|
||||
make.button(@"Main Menu").handler(^(NSArray<NSString *> *strings) {
|
||||
[self addTabAndDismiss:[FLEXNavigationController
|
||||
withRootViewController:[FLEXGlobalsViewController new]
|
||||
]];
|
||||
});
|
||||
make.button(@"Choose from Bookmarks").handler(^(NSArray<NSString *> *strings) {
|
||||
[self presentViewController:[FLEXNavigationController
|
||||
withRootViewController:[FLEXBookmarksViewController new]
|
||||
] animated:YES completion:nil];
|
||||
});
|
||||
make.button(@"Cancel").cancelStyle();
|
||||
} showFrom:self];
|
||||
} else {
|
||||
// No bookmarks, just open the main menu
|
||||
[self addTabAndDismiss:[FLEXNavigationController
|
||||
withRootViewController:[FLEXGlobalsViewController new]
|
||||
]];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)addTabAndDismiss:(UINavigationController *)newTab {
|
||||
FLEXExplorerViewController *presenter = self.corePresenter;
|
||||
[presenter dismissViewControllerAnimated:YES completion:^{
|
||||
[presenter presentViewController:newTab animated:YES completion:nil];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)closeAllButtonPressed {
|
||||
[FLEXAlert makeSheet:^(FLEXAlert *make) {
|
||||
NSInteger count = self.openTabs.count;
|
||||
NSString *title = FLEXPluralFormatString(count, @"Close %@ tabs", @"Close %@ tab");
|
||||
make.button(title).destructiveStyle().handler(^(NSArray<NSString *> *strings) {
|
||||
[self closeAll];
|
||||
[self toggleEditing];
|
||||
});
|
||||
make.button(@"Cancel").cancelStyle();
|
||||
} showFrom:self];
|
||||
}
|
||||
|
||||
- (void)closeAll {
|
||||
NSInteger rowCount = self.openTabs.count;
|
||||
|
||||
// Close tabs and update data source
|
||||
[FLEXTabList.sharedList closeAllTabs];
|
||||
[self reloadData:YES];
|
||||
|
||||
// Delete rows from table view
|
||||
NSArray<NSIndexPath *> *allRows = [NSArray flex_forEachUpTo:rowCount map:^id(NSUInteger row) {
|
||||
return [NSIndexPath indexPathForRow:row inSection:0];
|
||||
}];
|
||||
[self.tableView deleteRowsAtIndexPaths:allRows withRowAnimation:UITableViewRowAnimationAutomatic];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Table View Data Source
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
|
||||
return self.openTabs.count;
|
||||
}
|
||||
|
||||
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kFLEXDetailCell forIndexPath:indexPath];
|
||||
|
||||
UINavigationController *tab = self.openTabs[indexPath.row];
|
||||
cell.imageView.image = self.tabSnapshots[indexPath.row];
|
||||
cell.textLabel.text = tab.topViewController.title;
|
||||
cell.detailTextLabel.text = FLEXPluralString(tab.viewControllers.count, @"pages", @"page");
|
||||
|
||||
if (!cell.tag) {
|
||||
cell.textLabel.lineBreakMode = NSLineBreakByTruncatingTail;
|
||||
cell.textLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline];
|
||||
cell.detailTextLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleSubheadline];
|
||||
cell.tag = 1;
|
||||
}
|
||||
|
||||
if (indexPath.row == self.activeIndex) {
|
||||
cell.backgroundColor = FLEXColor.secondaryBackgroundColor;
|
||||
} else {
|
||||
cell.backgroundColor = FLEXColor.primaryBackgroundColor;
|
||||
}
|
||||
|
||||
return cell;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Table View Delegate
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
if (self.editing) {
|
||||
// Case: editing with multi-select
|
||||
self.toolbarItems.lastObject.title = @"Close Selected";
|
||||
self.toolbarItems.lastObject.tintColor = FLEXColor.destructiveColor;
|
||||
} else {
|
||||
if (self.activeIndex == indexPath.row && self.corePresenter != self.presentingViewController) {
|
||||
// Case: selected the already active tab
|
||||
[self dismissAnimated];
|
||||
} else {
|
||||
// Case: selected a different tab,
|
||||
// or selected a tab when presented from the FLEX toolbar
|
||||
FLEXTabList.sharedList.activeTabIndex = indexPath.row;
|
||||
self.presentNewActiveTabOnDismiss = YES;
|
||||
[self dismissAnimated];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
NSParameterAssert(self.editing);
|
||||
|
||||
if (tableView.indexPathsForSelectedRows.count == 0) {
|
||||
self.toolbarItems.lastObject.title = @"Done";
|
||||
self.toolbarItems.lastObject.tintColor = self.view.tintColor;
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)tableView:(UITableView *)table
|
||||
commitEditingStyle:(UITableViewCellEditingStyle)edit
|
||||
forRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
NSParameterAssert(edit == UITableViewCellEditingStyleDelete);
|
||||
|
||||
// Close tab and update data source
|
||||
[FLEXTabList.sharedList closeTab:self.openTabs[indexPath.row]];
|
||||
BOOL activeTabChanged = [self reloadData:YES];
|
||||
|
||||
// Delete row from table view
|
||||
[table deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
|
||||
|
||||
// Refresh the newly active tab row if needed
|
||||
[self reloadActiveTabRowIfChanged:activeTabChanged];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// FLEX-Categories.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 3/12/20.
|
||||
// Copyright © 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
|
||||
#import <FLEX/UIBarButtonItem+FLEX.h>
|
||||
#import <FLEX/CALayer+FLEX.h>
|
||||
#import <FLEX/UIFont+FLEX.h>
|
||||
#import <FLEX/UIGestureRecognizer+Blocks.h>
|
||||
#import <FLEX/UIView+FLEX_Layout.h>
|
||||
#import <FLEX/UIPasteboard+FLEX.h>
|
||||
#import <FLEX/UIMenu+FLEX.h>
|
||||
#import <FLEX/UITextField+Range.h>
|
||||
|
||||
#import <FLEX/NSObject+Reflection.h>
|
||||
#import <FLEX/NSArray+Functional.h>
|
||||
#import <FLEX/NSDictionary+ObjcRuntime.h>
|
||||
#import <FLEX/NSString+ObjcRuntime.h>
|
||||
#import <FLEX/NSString+FLEX.h>
|
||||
#import <FLEX/NSUserDefaults+FLEX.h>
|
||||
#import <FLEX/NSMapTable+FLEX_Subscripting.h>
|
||||
#import <FLEX/NSTimer+Blocks.h>
|
||||
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// FLEX-Core.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 3/11/20.
|
||||
// Copyright © 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <FLEX/FLEXFilteringTableViewController.h>
|
||||
#import <FLEX/FLEXNavigationController.h>
|
||||
#import <FLEX/FLEXTableViewController.h>
|
||||
#import <FLEX/FLEXTableView.h>
|
||||
|
||||
#import <FLEX/FLEXSingleRowSection.h>
|
||||
#import <FLEX/FLEXTableViewSection.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>
|
||||
@@ -0,0 +1,30 @@
|
||||
//
|
||||
// FLEX-ObjectExploring.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 3/11/20.
|
||||
// Copyright © 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <FLEX/FLEXObjectExplorerFactory.h>
|
||||
#import <FLEX/FLEXObjectExplorerViewController.h>
|
||||
|
||||
#import <FLEX/FLEXObjectExplorer.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 <FLEX/FLEXCollectionContentSection.h>
|
||||
#import <FLEX/FLEXColorPreviewSection.h>
|
||||
#import <FLEX/FLEXDefaultsContentSection.h>
|
||||
#import <FLEX/FLEXMetadataSection.h>
|
||||
#import <FLEX/FLEXMutableListSection.h>
|
||||
#import <FLEX/FLEXObjectInfoSection.h>
|
||||
@@ -0,0 +1,25 @@
|
||||
//
|
||||
// FLEX-Runtime.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 3/11/20.
|
||||
// Copyright © 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <FLEX/FLEXObjcInternal.h>
|
||||
#import <FLEX/FLEXRuntimeSafety.h>
|
||||
#import <FLEX/FLEXBlockDescription.h>
|
||||
#import <FLEX/FLEXTypeEncodingParser.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 <FLEX/FLEXProtocolBuilder.h>
|
||||
#import <FLEX/FLEXClassBuilder.h>
|
||||
+15
-1
@@ -3,9 +3,23 @@
|
||||
// FLEX
|
||||
//
|
||||
// Created by Eric Horacek on 7/18/15.
|
||||
// Copyright (c) 2015 Flipboard. All rights reserved.
|
||||
// Modified by Tanner Bennett on 3/12/20.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <FLEX/FLEXManager.h>
|
||||
#import <FLEX/FLEXManager+Extensibility.h>
|
||||
#import <FLEX/FLEXManager+Networking.h>
|
||||
|
||||
#import <FLEX/FLEXExplorerToolbar.h>
|
||||
#import <FLEX/FLEXExplorerToolbarItem.h>
|
||||
#import <FLEX/FLEXGlobalsEntry.h>
|
||||
|
||||
#import <FLEX/FLEX-Core.h>
|
||||
#import <FLEX/FLEX-Runtime.h>
|
||||
#import <FLEX/FLEX-Categories.h>
|
||||
#import <FLEX/FLEX-ObjectExploring.h>
|
||||
|
||||
#import <FLEX/FLEXMacros.h>
|
||||
#import <FLEX/FLEXAlert.h>
|
||||
#import <FLEX/FLEXResources.h>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// FLEXDBQueryRowCell.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Peng Tao on 15/11/24.
|
||||
// Copyright © 2015年 f. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
extern NSString * const kFLEXDBQueryRowCellReuse;
|
||||
|
||||
|
||||
@interface FLEXDBQueryRowCell : UITableViewCell
|
||||
|
||||
/// An array of NSString, NSNumber, or NSData objects
|
||||
@property (nonatomic) NSArray *data;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,74 @@
|
||||
//
|
||||
// FLEXDBQueryRowCell.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Peng Tao on 15/11/24.
|
||||
// Copyright © 2015年 f. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXDBQueryRowCell.h"
|
||||
#import "FLEXMultiColumnTableView.h"
|
||||
#import "NSArray+Functional.h"
|
||||
#import "UIFont+FLEX.h"
|
||||
#import "FLEXColor.h"
|
||||
|
||||
NSString * const kFLEXDBQueryRowCellReuse = @"kFLEXDBQueryRowCellReuse";
|
||||
|
||||
@interface FLEXDBQueryRowCell ()
|
||||
@property (nonatomic) NSInteger columnCount;
|
||||
@property (nonatomic) NSArray<UILabel *> *labels;
|
||||
@end
|
||||
|
||||
@implementation FLEXDBQueryRowCell
|
||||
|
||||
- (void)setData:(NSArray *)data {
|
||||
_data = data;
|
||||
self.columnCount = data.count;
|
||||
|
||||
[self.labels flex_forEach:^(UILabel *label, NSUInteger idx) {
|
||||
id content = self.data[idx];
|
||||
|
||||
if ([content isKindOfClass:[NSString class]]) {
|
||||
label.text = content;
|
||||
} else if (content == NSNull.null) {
|
||||
label.text = @"<null>";
|
||||
label.textColor = FLEXColor.deemphasizedTextColor;
|
||||
} else {
|
||||
label.text = [content description];
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)setColumnCount:(NSInteger)columnCount {
|
||||
if (columnCount != _columnCount) {
|
||||
_columnCount = columnCount;
|
||||
|
||||
// Remove existing labels
|
||||
for (UILabel *l in self.labels) {
|
||||
[l removeFromSuperview];
|
||||
}
|
||||
|
||||
// Create new labels
|
||||
self.labels = [NSArray flex_forEachUpTo:columnCount map:^id(NSUInteger i) {
|
||||
UILabel *label = [UILabel new];
|
||||
label.font = UIFont.flex_defaultTableCellFont;
|
||||
label.textAlignment = NSTextAlignmentLeft;
|
||||
[self.contentView addSubview:label];
|
||||
|
||||
return label;
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
|
||||
CGFloat width = self.contentView.frame.size.width / self.labels.count;
|
||||
CGFloat height = self.contentView.frame.size.height;
|
||||
|
||||
[self.labels flex_forEach:^(UILabel *label, NSUInteger i) {
|
||||
label.frame = CGRectMake(width * i + 5, 0, (width - 10), height);
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -12,15 +12,23 @@
|
||||
// which Flying Meat Inc. licenses this file to you.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "FLEXSQLResult.h"
|
||||
|
||||
@protocol FLEXDatabaseManager <NSObject>
|
||||
|
||||
@required
|
||||
- (instancetype)initWithPath:(NSString*)path;
|
||||
|
||||
+ (instancetype)managerForDatabase:(NSString *)path;
|
||||
|
||||
- (BOOL)open;
|
||||
- (NSArray<NSDictionary<NSString *, id> *> *)queryAllTables;
|
||||
- (NSArray<NSString *> *)queryAllColumnsWithTableName:(NSString *)tableName;
|
||||
- (NSArray<NSDictionary<NSString *, id> *> *)queryAllDataWithTableName:(NSString *)tableName;
|
||||
|
||||
/// @return a list of all table names
|
||||
- (NSArray<NSString *> *)queryAllTables;
|
||||
- (NSArray<NSString *> *)queryAllColumnsOfTable:(NSString *)tableName;
|
||||
- (NSArray<NSArray *> *)queryAllDataInTable:(NSString *)tableName;
|
||||
|
||||
@optional
|
||||
|
||||
- (FLEXSQLResult *)executeStatement:(NSString *)SQLStatement;
|
||||
|
||||
@end
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
@protocol FLEXMultiColumnTableViewDelegate <NSObject>
|
||||
|
||||
@required
|
||||
- (void)multiColumnTableView:(FLEXMultiColumnTableView *)tableView didTapLabelWithText:(NSString *)text;
|
||||
- (void)multiColumnTableView:(FLEXMultiColumnTableView *)tableView didTapHeaderWithText:(NSString *)text sortType:(FLEXTableColumnHeaderSortType)sortType;
|
||||
- (void)multiColumnTableView:(FLEXMultiColumnTableView *)tableView didSelectRow:(NSInteger)row;
|
||||
- (void)multiColumnTableView:(FLEXMultiColumnTableView *)tableView didSelectHeaderForColumn:(NSInteger)column sortType:(FLEXTableColumnHeaderSortType)sortType;
|
||||
|
||||
@end
|
||||
|
||||
@@ -25,10 +25,9 @@
|
||||
|
||||
- (NSInteger)numberOfColumnsInTableView:(FLEXMultiColumnTableView *)tableView;
|
||||
- (NSInteger)numberOfRowsInTableView:(FLEXMultiColumnTableView *)tableView;
|
||||
- (NSString *)columnNameInColumn:(NSInteger)column;
|
||||
- (NSString *)rowNameInRow:(NSInteger)row;
|
||||
- (NSString *)contentAtColumn:(NSInteger)column row:(NSInteger)row;
|
||||
- (NSArray *)contentAtRow:(NSInteger)row;
|
||||
- (NSString *)columnTitle:(NSInteger)column;
|
||||
- (NSString *)rowTitle:(NSInteger)row;
|
||||
- (NSArray<NSString *> *)contentForRow:(NSInteger)row;
|
||||
|
||||
- (CGFloat)multiColumnTableView:(FLEXMultiColumnTableView *)tableView widthForContentCellInColumn:(NSInteger)column;
|
||||
- (CGFloat)multiColumnTableView:(FLEXMultiColumnTableView *)tableView heightForContentCellInRow:(NSInteger)row;
|
||||
@@ -40,8 +39,8 @@
|
||||
|
||||
@interface FLEXMultiColumnTableView : UIView
|
||||
|
||||
@property (nonatomic, weak) id<FLEXMultiColumnTableViewDataSource>dataSource;
|
||||
@property (nonatomic, weak) id<FLEXMultiColumnTableViewDelegate>delegate;
|
||||
@property (nonatomic, weak) id<FLEXMultiColumnTableViewDataSource> dataSource;
|
||||
@property (nonatomic, weak) id<FLEXMultiColumnTableViewDelegate> delegate;
|
||||
|
||||
- (void)reloadData;
|
||||
|
||||
|
||||
@@ -7,11 +7,13 @@
|
||||
//
|
||||
|
||||
#import "FLEXMultiColumnTableView.h"
|
||||
#import "FLEXTableContentCell.h"
|
||||
#import "FLEXDBQueryRowCell.h"
|
||||
#import "FLEXTableLeftCell.h"
|
||||
#import "FLEXColor.h"
|
||||
|
||||
@interface FLEXMultiColumnTableView ()
|
||||
<UITableViewDataSource, UITableViewDelegate,UIScrollViewDelegate, FLEXTableContentCellDelegate>
|
||||
@interface FLEXMultiColumnTableView () <
|
||||
UITableViewDataSource, UITableViewDelegate, UIScrollViewDelegate
|
||||
>
|
||||
|
||||
@property (nonatomic) UIScrollView *contentScrollView;
|
||||
@property (nonatomic) UIScrollView *headerScrollView;
|
||||
@@ -19,35 +21,49 @@
|
||||
@property (nonatomic) UITableView *contentTableView;
|
||||
@property (nonatomic) UIView *leftHeader;
|
||||
|
||||
@property (nonatomic) NSDictionary<NSString *, NSNumber *> *sortStatusDict;
|
||||
/// \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;
|
||||
@property (nonatomic, readonly) CGFloat leftHeaderWidth;
|
||||
@property (nonatomic, readonly) CGFloat columnMargin;
|
||||
|
||||
@end
|
||||
|
||||
static const CGFloat kColumnMargin = 1;
|
||||
|
||||
@implementation FLEXMultiColumnTableView
|
||||
|
||||
#pragma mark - Initialization
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
[self loadUI];
|
||||
self.autoresizingMask |= UIViewAutoresizingFlexibleWidth;
|
||||
self.autoresizingMask |= UIViewAutoresizingFlexibleHeight;
|
||||
self.autoresizingMask |= UIViewAutoresizingFlexibleTopMargin;
|
||||
self.backgroundColor = FLEXColor.groupedBackgroundColor;
|
||||
|
||||
[self loadHeaderScrollView];
|
||||
[self loadContentScrollView];
|
||||
[self loadLeftView];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)didMoveToSuperview {
|
||||
[super didMoveToSuperview];
|
||||
[self reloadData];
|
||||
}
|
||||
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
|
||||
CGFloat width = self.frame.size.width;
|
||||
CGFloat height = self.frame.size.height;
|
||||
CGFloat topheaderHeight = [self topHeaderHeight];
|
||||
CGFloat leftHeaderWidth = [self leftHeaderWidth];
|
||||
CGFloat topheaderHeight = self.topHeaderHeight;
|
||||
CGFloat leftHeaderWidth = self.leftHeaderWidth;
|
||||
CGFloat topInsets = 0.f;
|
||||
|
||||
if (@available (iOS 11.0, *)) {
|
||||
@@ -55,46 +71,45 @@ static const CGFloat kColumnMargin = 1;
|
||||
}
|
||||
|
||||
CGFloat contentWidth = 0.0;
|
||||
NSInteger rowsCount = [self numberOfColumns];
|
||||
NSInteger rowsCount = self.numberOfColumns;
|
||||
for (int i = 0; i < rowsCount; i++) {
|
||||
contentWidth += [self contentWidthForColumn:i];
|
||||
}
|
||||
|
||||
self.leftTableView.frame = CGRectMake(0, topheaderHeight + topInsets, leftHeaderWidth, height - topheaderHeight - topInsets);
|
||||
self.headerScrollView.frame = CGRectMake(leftHeaderWidth, topInsets, width - leftHeaderWidth, topheaderHeight);
|
||||
self.headerScrollView.contentSize = CGSizeMake( self.contentTableView.frame.size.width, self.headerScrollView.frame.size.height);
|
||||
self.contentTableView.frame = CGRectMake(0, 0, contentWidth + [self numberOfColumns] * [self columnMargin] , height - topheaderHeight - topInsets);
|
||||
self.contentScrollView.frame = CGRectMake(leftHeaderWidth, topheaderHeight + topInsets, width - leftHeaderWidth, height - topheaderHeight - topInsets);
|
||||
CGFloat contentHeight = height - topheaderHeight - topInsets;
|
||||
|
||||
self.leftHeader.frame = CGRectMake(0, topInsets, self.leftHeaderWidth, self.topHeaderHeight);
|
||||
self.leftTableView.frame = CGRectMake(
|
||||
0, topheaderHeight + topInsets, leftHeaderWidth, contentHeight
|
||||
);
|
||||
self.headerScrollView.frame = CGRectMake(
|
||||
leftHeaderWidth, topInsets, width - leftHeaderWidth, topheaderHeight
|
||||
);
|
||||
self.headerScrollView.contentSize = CGSizeMake(
|
||||
self.contentTableView.frame.size.width, self.headerScrollView.frame.size.height
|
||||
);
|
||||
self.contentTableView.frame = CGRectMake(
|
||||
0, 0, contentWidth + self.numberOfColumns * self.columnMargin , contentHeight
|
||||
);
|
||||
self.contentScrollView.frame = CGRectMake(
|
||||
leftHeaderWidth, topheaderHeight + topInsets, width - leftHeaderWidth, contentHeight
|
||||
);
|
||||
self.contentScrollView.contentSize = self.contentTableView.frame.size;
|
||||
self.leftHeader.frame = CGRectMake(0, topInsets, [self leftHeaderWidth], [self topHeaderHeight]);
|
||||
}
|
||||
|
||||
|
||||
- (void)loadUI {
|
||||
[self loadHeaderScrollView];
|
||||
[self loadContentScrollView];
|
||||
[self loadLeftView];
|
||||
}
|
||||
|
||||
- (void)reloadData {
|
||||
[self loadLeftViewData];
|
||||
[self loadContentData];
|
||||
[self loadHeaderData];
|
||||
}
|
||||
|
||||
#pragma mark - UI
|
||||
|
||||
- (void)loadHeaderScrollView {
|
||||
UIScrollView *headerScrollView = [UIScrollView new];
|
||||
headerScrollView.delegate = self;
|
||||
self.headerScrollView = headerScrollView;
|
||||
self.headerScrollView.backgroundColor = [UIColor colorWithWhite:0.803 alpha:0.850];
|
||||
UIScrollView *headerScrollView = [UIScrollView new];
|
||||
headerScrollView.delegate = self;
|
||||
headerScrollView.backgroundColor = FLEXColor.secondaryGroupedBackgroundColor;
|
||||
self.headerScrollView = headerScrollView;
|
||||
|
||||
[self addSubview:headerScrollView];
|
||||
}
|
||||
|
||||
- (void)loadContentScrollView {
|
||||
|
||||
UIScrollView *scrollView = [UIScrollView new];
|
||||
scrollView.bounces = NO;
|
||||
scrollView.delegate = self;
|
||||
@@ -103,82 +118,89 @@ static const CGFloat kColumnMargin = 1;
|
||||
tableView.delegate = self;
|
||||
tableView.dataSource = self;
|
||||
tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
|
||||
[tableView registerClass:[FLEXDBQueryRowCell class]
|
||||
forCellReuseIdentifier:kFLEXDBQueryRowCellReuse
|
||||
];
|
||||
|
||||
[self addSubview:scrollView];
|
||||
[scrollView addSubview:tableView];
|
||||
[self addSubview:scrollView];
|
||||
|
||||
self.contentScrollView = scrollView;
|
||||
self.contentTableView = tableView;
|
||||
|
||||
self.contentTableView = tableView;
|
||||
}
|
||||
|
||||
- (void)loadLeftView {
|
||||
UITableView *leftTableView = [UITableView new];
|
||||
UITableView *leftTableView = [UITableView new];
|
||||
leftTableView.delegate = self;
|
||||
leftTableView.dataSource = self;
|
||||
leftTableView.separatorStyle = UITableViewCellSeparatorStyleNone;
|
||||
self.leftTableView = leftTableView;
|
||||
[self addSubview:leftTableView];
|
||||
|
||||
UIView *leftHeader = [UIView new];
|
||||
leftHeader.backgroundColor = [UIColor colorWithWhite:0.950 alpha:0.668];
|
||||
UIView *leftHeader = [UIView new];
|
||||
leftHeader.backgroundColor = FLEXColor.secondaryBackgroundColor;
|
||||
self.leftHeader = leftHeader;
|
||||
[self addSubview:leftHeader];
|
||||
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Data
|
||||
|
||||
- (void)reloadData {
|
||||
[self loadLeftViewData];
|
||||
[self loadContentData];
|
||||
[self loadHeaderData];
|
||||
}
|
||||
|
||||
- (void)loadHeaderData {
|
||||
NSArray<UIView *> *subviews = self.headerScrollView.subviews;
|
||||
|
||||
for (UIView *subview in subviews) {
|
||||
// Remove existing headers, if any
|
||||
for (UIView *subview in self.headerScrollView.subviews) {
|
||||
[subview removeFromSuperview];
|
||||
}
|
||||
CGFloat x = 0.0;
|
||||
CGFloat w = 0.0;
|
||||
for (int i = 0; i < [self numberOfColumns] ; i++) {
|
||||
w = [self contentWidthForColumn:i] + [self columnMargin];
|
||||
|
||||
CGFloat xOffset = 0.0;
|
||||
for (NSInteger column = 0; column < self.numberOfColumns; column++) {
|
||||
CGFloat width = [self contentWidthForColumn:column] + self.columnMargin;
|
||||
|
||||
FLEXTableColumnHeader *cell = [[FLEXTableColumnHeader alloc] initWithFrame:CGRectMake(x, 0, w, [self topHeaderHeight] - 1)];
|
||||
cell.label.text = [self columnTitleForColumn:i];
|
||||
[self.headerScrollView addSubview:cell];
|
||||
FLEXTableColumnHeader *header = [[FLEXTableColumnHeader alloc]
|
||||
initWithFrame:CGRectMake(xOffset, 0, width, self.topHeaderHeight - 1)
|
||||
];
|
||||
header.titleLabel.text = [self columnTitle:column];
|
||||
|
||||
FLEXTableColumnHeaderSortType type = [self.sortStatusDict[[self columnTitleForColumn:i]] integerValue];
|
||||
[cell changeSortStatusWithType:type];
|
||||
if (column == self.sortColumn) {
|
||||
header.sortType = self.sortType;
|
||||
}
|
||||
|
||||
UITapGestureRecognizer *gesture = [[UITapGestureRecognizer alloc] initWithTarget:self
|
||||
action:@selector(contentHeaderTap:)];
|
||||
[cell addGestureRecognizer:gesture];
|
||||
cell.userInteractionEnabled = YES;
|
||||
// Header tap gesture
|
||||
UITapGestureRecognizer *gesture = [[UITapGestureRecognizer alloc]
|
||||
initWithTarget:self action:@selector(contentHeaderTap:)
|
||||
];
|
||||
[header addGestureRecognizer:gesture];
|
||||
header.userInteractionEnabled = YES;
|
||||
|
||||
x = x + w;
|
||||
[self.headerScrollView addSubview:header];
|
||||
xOffset += width;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)contentHeaderTap:(UIGestureRecognizer *)gesture {
|
||||
FLEXTableColumnHeader *header = (FLEXTableColumnHeader *)gesture.view;
|
||||
NSString *string = header.label.text;
|
||||
FLEXTableColumnHeaderSortType currentType = [self.sortStatusDict[string] integerValue];
|
||||
FLEXTableColumnHeaderSortType newType ;
|
||||
NSInteger newSortColumn = [self.headerScrollView.subviews indexOfObject:gesture.view];
|
||||
FLEXTableColumnHeaderSortType newType = FLEXNextTableColumnHeaderSortType(self.sortType);
|
||||
|
||||
switch (currentType) {
|
||||
case FLEXTableColumnHeaderSortTypeNone:
|
||||
newType = FLEXTableColumnHeaderSortTypeAsc;
|
||||
break;
|
||||
case FLEXTableColumnHeaderSortTypeAsc:
|
||||
newType = FLEXTableColumnHeaderSortTypeDesc;
|
||||
break;
|
||||
case FLEXTableColumnHeaderSortTypeDesc:
|
||||
newType = FLEXTableColumnHeaderSortTypeAsc;
|
||||
break;
|
||||
}
|
||||
// Reset old header
|
||||
FLEXTableColumnHeader *oldHeader = (id)self.headerScrollView.subviews[self.sortColumn];
|
||||
oldHeader.sortType = FLEXTableColumnHeaderSortTypeNone;
|
||||
|
||||
self.sortStatusDict = @{header.label.text : @(newType)};
|
||||
[header changeSortStatusWithType:newType];
|
||||
[self.delegate multiColumnTableView:self didTapHeaderWithText:string sortType:newType];
|
||||
// Update new header
|
||||
FLEXTableColumnHeader *newHeader = (id)self.headerScrollView.subviews[newSortColumn];
|
||||
newHeader.sortType = newType;
|
||||
|
||||
// Update self
|
||||
self.sortColumn = newSortColumn;
|
||||
self.sortType = newType;
|
||||
|
||||
// Notify delegate
|
||||
[self.delegate multiColumnTableView:self didSelectHeaderForColumn:newSortColumn sortType:newType];
|
||||
}
|
||||
|
||||
- (void)loadContentData {
|
||||
@@ -189,39 +211,30 @@ static const CGFloat kColumnMargin = 1;
|
||||
[self.leftTableView reloadData];
|
||||
}
|
||||
|
||||
- (UITableViewCell *)tableView:(UITableView *)tableView
|
||||
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
UIColor *backgroundColor = UIColor.whiteColor;
|
||||
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
// Alternating background color
|
||||
UIColor *backgroundColor = FLEXColor.primaryBackgroundColor;
|
||||
if (indexPath.row % 2 != 0) {
|
||||
backgroundColor = [UIColor colorWithWhite:0.950 alpha:0.750];
|
||||
backgroundColor = FLEXColor.secondaryBackgroundColor;
|
||||
}
|
||||
|
||||
if (tableView != self.leftTableView) {
|
||||
self.rowData = [self.dataSource contentAtRow:indexPath.row];
|
||||
FLEXTableContentCell *cell = [FLEXTableContentCell cellWithTableView:tableView
|
||||
columnNumber:[self numberOfColumns]];
|
||||
// Left side table view for row numbers
|
||||
if (tableView == self.leftTableView) {
|
||||
FLEXTableLeftCell *cell = [FLEXTableLeftCell cellWithTableView:tableView];
|
||||
cell.contentView.backgroundColor = backgroundColor;
|
||||
cell.delegate = self;
|
||||
|
||||
for (int i = 0 ; i < cell.labels.count; i++) {
|
||||
|
||||
UILabel *label = cell.labels[i];
|
||||
label.textColor = UIColor.blackColor;
|
||||
|
||||
NSString *content = [NSString stringWithFormat:@"%@",self.rowData[i]];
|
||||
if ([content isEqualToString:@"<null>"]) {
|
||||
label.textColor = UIColor.lightGrayColor;
|
||||
content = @"NULL";
|
||||
}
|
||||
label.text = content;
|
||||
label.backgroundColor = backgroundColor;
|
||||
}
|
||||
cell.titlelabel.text = [self rowTitle:indexPath.row];
|
||||
return cell;
|
||||
}
|
||||
// Right side table view for data
|
||||
else {
|
||||
FLEXTableLeftCell *cell = [FLEXTableLeftCell cellWithTableView:tableView];
|
||||
self.rowData = [self.dataSource contentForRow:indexPath.row];
|
||||
FLEXDBQueryRowCell *cell = [tableView
|
||||
dequeueReusableCellWithIdentifier:kFLEXDBQueryRowCellReuse forIndexPath:indexPath
|
||||
];
|
||||
|
||||
cell.contentView.backgroundColor = backgroundColor;
|
||||
cell.titlelabel.text = [self rowTitleForRow:indexPath.row];
|
||||
cell.data = [self.dataSource contentForRow:indexPath.row];
|
||||
NSAssert(cell.data.count == self.numberOfColumns, @"Count of data provided was incorrect");
|
||||
return cell;
|
||||
}
|
||||
}
|
||||
@@ -230,12 +243,11 @@ static const CGFloat kColumnMargin = 1;
|
||||
return [self.dataSource numberOfRowsInTableView:self];
|
||||
}
|
||||
|
||||
|
||||
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
return [self.dataSource multiColumnTableView:self heightForContentCellInRow:indexPath.row];
|
||||
}
|
||||
|
||||
|
||||
// Scroll all scroll views in sync
|
||||
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
|
||||
if (scrollView == self.contentScrollView) {
|
||||
self.headerScrollView.contentOffset = scrollView.contentOffset;
|
||||
@@ -251,23 +263,23 @@ static const CGFloat kColumnMargin = 1;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
#pragma mark UITableView Delegate
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
if (tableView == self.leftTableView) {
|
||||
[self.contentTableView selectRowAtIndexPath:indexPath
|
||||
animated:NO
|
||||
scrollPosition:UITableViewScrollPositionNone];
|
||||
[self.contentTableView
|
||||
selectRowAtIndexPath:indexPath
|
||||
animated:NO
|
||||
scrollPosition:UITableViewScrollPositionNone
|
||||
];
|
||||
}
|
||||
else if (tableView == self.contentTableView) {
|
||||
[self.leftTableView selectRowAtIndexPath:indexPath
|
||||
animated:NO
|
||||
scrollPosition:UITableViewScrollPositionNone];
|
||||
[self.delegate multiColumnTableView:self didSelectRow:indexPath.row];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
#pragma mark DataSource Accessor
|
||||
|
||||
- (NSInteger)numberOfRows {
|
||||
@@ -278,16 +290,12 @@ static const CGFloat kColumnMargin = 1;
|
||||
return [self.dataSource numberOfColumnsInTableView:self];
|
||||
}
|
||||
|
||||
- (NSString *)columnTitleForColumn:(NSInteger)column {
|
||||
return [self.dataSource columnNameInColumn:column];
|
||||
- (NSString *)columnTitle:(NSInteger)column {
|
||||
return [self.dataSource columnTitle:column];
|
||||
}
|
||||
|
||||
- (NSString *)rowTitleForRow:(NSInteger)row {
|
||||
return [self.dataSource rowNameInRow:row];
|
||||
}
|
||||
|
||||
- (NSString *)contentAtColumn:(NSInteger)column row:(NSInteger)row; {
|
||||
return [self.dataSource contentAtColumn:column row:row];
|
||||
- (NSString *)rowTitle:(NSInteger)row {
|
||||
return [self.dataSource rowTitle:row];
|
||||
}
|
||||
|
||||
- (CGFloat)contentWidthForColumn:(NSInteger)column {
|
||||
@@ -310,9 +318,4 @@ static const CGFloat kColumnMargin = 1;
|
||||
return kColumnMargin;
|
||||
}
|
||||
|
||||
|
||||
- (void)tableContentCell:(FLEXTableContentCell *)tableView labelDidTapWithText:(NSString *)text {
|
||||
[self.delegate multiColumnTableView:self didTapLabelWithText:text];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
//
|
||||
|
||||
#import "FLEXRealmDatabaseManager.h"
|
||||
#import "NSArray+Functional.h"
|
||||
#import "FLEXSQLResult.h"
|
||||
|
||||
#if __has_include(<Realm/Realm.h>)
|
||||
#import <Realm/Realm.h>
|
||||
@@ -22,88 +24,73 @@
|
||||
|
||||
@end
|
||||
|
||||
//#endif
|
||||
|
||||
@implementation FLEXRealmDatabaseManager
|
||||
static Class RLMRealmClass = nil;
|
||||
|
||||
- (instancetype)initWithPath:(NSString*)aPath {
|
||||
Class realmClass = NSClassFromString(@"RLMRealm");
|
||||
if (realmClass == nil) {
|
||||
+ (void)load {
|
||||
RLMRealmClass = NSClassFromString(@"RLMRealm");
|
||||
}
|
||||
|
||||
+ (instancetype)managerForDatabase:(NSString *)path {
|
||||
return [[self alloc] initWithPath:path];
|
||||
}
|
||||
|
||||
- (instancetype)initWithPath:(NSString *)path {
|
||||
if (!RLMRealmClass) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
self = [super init];
|
||||
|
||||
if (self) {
|
||||
_path = aPath;
|
||||
_path = path;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL)open {
|
||||
Class realmClass = NSClassFromString(@"RLMRealm");
|
||||
Class configurationClass = NSClassFromString(@"RLMRealmConfiguration");
|
||||
|
||||
if (realmClass == nil || configurationClass == nil) {
|
||||
if (!RLMRealmClass || !configurationClass) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSError *error = nil;
|
||||
id configuration = [configurationClass new];
|
||||
[(RLMRealmConfiguration *)configuration setFileURL:[NSURL fileURLWithPath:self.path]];
|
||||
self.realm = [realmClass realmWithConfiguration:configuration error:&error];
|
||||
self.realm = [RLMRealmClass realmWithConfiguration:configuration error:&error];
|
||||
|
||||
return (error == nil);
|
||||
}
|
||||
|
||||
- (NSArray<NSDictionary<NSString *, id> *> *)queryAllTables {
|
||||
NSMutableArray<NSDictionary<NSString *, id> *> *allTables = [NSMutableArray array];
|
||||
RLMSchema *schema = [self.realm schema];
|
||||
|
||||
for (RLMObjectSchema *objectSchema in schema.objectSchema) {
|
||||
if (objectSchema.className == nil) {
|
||||
continue;
|
||||
}
|
||||
|
||||
NSDictionary<NSString *, id> *dictionary = @{@"name":objectSchema.className};
|
||||
[allTables addObject:dictionary];
|
||||
}
|
||||
|
||||
return allTables;
|
||||
- (NSArray<NSString *> *)queryAllTables {
|
||||
// Map each schema to its name
|
||||
return [self.realm.schema.objectSchema flex_mapped:^id(RLMObjectSchema *schema, NSUInteger idx) {
|
||||
return schema.className ?: nil;
|
||||
}];
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)queryAllColumnsWithTableName:(NSString *)tableName {
|
||||
RLMObjectSchema *objectSchema = [[self.realm schema] schemaForClassName:tableName];
|
||||
if (objectSchema == nil) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSMutableArray<NSString *> *columnNames = [NSMutableArray array];
|
||||
for (RLMProperty *property in objectSchema.properties) {
|
||||
[columnNames addObject:property.name];
|
||||
}
|
||||
|
||||
return columnNames;
|
||||
- (NSArray<NSString *> *)queryAllColumnsOfTable:(NSString *)tableName {
|
||||
RLMObjectSchema *objectSchema = [self.realm.schema schemaForClassName:tableName];
|
||||
// Map each column to its name
|
||||
return [objectSchema.properties flex_mapped:^id(RLMProperty *property, NSUInteger idx) {
|
||||
return property.name;
|
||||
}];
|
||||
}
|
||||
|
||||
- (NSArray<NSDictionary<NSString *, id> *> *)queryAllDataWithTableName:(NSString *)tableName {
|
||||
RLMObjectSchema *objectSchema = [[self.realm schema] schemaForClassName:tableName];
|
||||
- (NSArray<NSArray *> *)queryAllDataInTable:(NSString *)tableName {
|
||||
RLMObjectSchema *objectSchema = [self.realm.schema schemaForClassName:tableName];
|
||||
RLMResults *results = [self.realm allObjects:tableName];
|
||||
if (results.count == 0 || objectSchema == nil) {
|
||||
if (results.count == 0 || !objectSchema) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSMutableArray<NSDictionary<NSString *, id> *> *allDataEntries = [NSMutableArray array];
|
||||
for (RLMObject *result in results) {
|
||||
NSMutableDictionary<NSString *, id> *entry = [NSMutableDictionary dictionary];
|
||||
for (RLMProperty *property in objectSchema.properties) {
|
||||
id value = [result valueForKey:property.name];
|
||||
entry[property.name] = (value) ? (value) : [NSNull null];
|
||||
}
|
||||
|
||||
[allDataEntries addObject:entry];
|
||||
}
|
||||
|
||||
return allDataEntries;
|
||||
// Map results to an array of rows
|
||||
return [NSArray flex_mapped:results block:^id(RLMObject *result, NSUInteger idx) {
|
||||
// Map each row to an array of the values of its properties
|
||||
return [objectSchema.properties flex_mapped:^id(RLMProperty *property, NSUInteger idx) {
|
||||
return [result valueForKey:property.name] ?: NSNull.null;
|
||||
}];
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
//
|
||||
// FLEXSQLResult.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 3/3/20.
|
||||
// Copyright © 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FLEXSQLResult : NSObject
|
||||
|
||||
/// Describes the result of a non-select query, or an error of any kind of query
|
||||
+ (instancetype)message:(NSString *)message;
|
||||
/// @param rowData A list of rows, where each element in the row
|
||||
/// corresponds to the column given in /c columnNames
|
||||
+ (instancetype)columns:(NSArray<NSString *> *)columnNames
|
||||
rows:(NSArray<NSArray<NSString *> *> *)rowData;
|
||||
|
||||
@property (nonatomic, readonly, nullable) NSString *message;
|
||||
|
||||
/// A list of column names
|
||||
@property (nonatomic, readonly, nullable) NSArray<NSString *> *columns;
|
||||
/// A list of rows, where each element in the row corresponds
|
||||
/// to the value of the column at the same index in \c columns.
|
||||
///
|
||||
/// That is, given a row, looping over the contents of the row and
|
||||
/// the contents of \c columns will give you key-value pairs of
|
||||
/// column names to column values for that row.
|
||||
@property (nonatomic, readonly, nullable) NSArray<NSArray<NSString *> *> *rows;
|
||||
/// A list of rows where the fields are paired to column names.
|
||||
///
|
||||
/// This property is lazily constructed by looping over
|
||||
/// the rows and columns present in the other two properties.
|
||||
@property (nonatomic, readonly, nullable) NSArray<NSDictionary<NSString *, id> *> *keyedRows;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,47 @@
|
||||
//
|
||||
// FLEXSQLResult.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 3/3/20.
|
||||
// Copyright © 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXSQLResult.h"
|
||||
#import "NSArray+Functional.h"
|
||||
|
||||
@implementation FLEXSQLResult
|
||||
@synthesize keyedRows = _keyedRows;
|
||||
|
||||
+ (instancetype)message:(NSString *)message {
|
||||
return [[self alloc] initWithmessage:message columns:nil rows:nil];
|
||||
}
|
||||
|
||||
+ (instancetype)columns:(NSArray<NSString *> *)columnNames rows:(NSArray<NSArray<NSString *> *> *)rowData {
|
||||
return [[self alloc] initWithmessage:nil columns:columnNames rows:rowData];
|
||||
}
|
||||
|
||||
- (id)initWithmessage:(NSString *)message columns:(NSArray *)columns rows:(NSArray<NSArray *> *)rows {
|
||||
NSParameterAssert(message || (columns && rows));
|
||||
NSParameterAssert(columns.count == rows.firstObject.count);
|
||||
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_message = message;
|
||||
_columns = columns;
|
||||
_rows = rows;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSArray<NSDictionary<NSString *,id> *> *)keyedRows {
|
||||
if (!_keyedRows) {
|
||||
_keyedRows = [self.rows flex_mapped:^id(NSArray<NSString *> *row, NSUInteger idx) {
|
||||
return [NSDictionary dictionaryWithObjects:row forKeys:self.columns];
|
||||
}];
|
||||
}
|
||||
|
||||
return _keyedRows;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -8,60 +8,68 @@
|
||||
|
||||
#import "FLEXSQLiteDatabaseManager.h"
|
||||
#import "FLEXManager.h"
|
||||
#import "NSArray+Functional.h"
|
||||
#import "FLEXSQLResult.h"
|
||||
#import <sqlite3.h>
|
||||
|
||||
static NSString * const QUERY_TABLENAMES = @"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name";
|
||||
|
||||
static NSString *const QUERY_TABLENAMES_SQL = @"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name";
|
||||
@interface FLEXSQLiteDatabaseManager ()
|
||||
@property (nonatomic) sqlite3 *db;
|
||||
@property (nonatomic, copy) NSString *path;
|
||||
@end
|
||||
|
||||
@implementation FLEXSQLiteDatabaseManager {
|
||||
sqlite3* _db;
|
||||
NSString* _databasePath;
|
||||
@implementation FLEXSQLiteDatabaseManager
|
||||
|
||||
#pragma mark - FLEXDatabaseManager
|
||||
|
||||
+ (instancetype)managerForDatabase:(NSString *)path {
|
||||
return [[self alloc] initWithPath:path];
|
||||
}
|
||||
|
||||
- (instancetype)initWithPath:(NSString*)aPath {
|
||||
- (instancetype)initWithPath:(NSString *)path {
|
||||
self = [super init];
|
||||
|
||||
if (self) {
|
||||
_databasePath = [aPath copy];
|
||||
self.path = path;;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL)open {
|
||||
if (_db) {
|
||||
if (self.db) {
|
||||
return YES;
|
||||
}
|
||||
int err = sqlite3_open(_databasePath.UTF8String, &_db);
|
||||
|
||||
int err = sqlite3_open(self.path.UTF8String, &_db);
|
||||
|
||||
#if SQLITE_HAS_CODEC
|
||||
NSString *defaultSqliteDatabasePassword = [FLEXManager sharedManager].defaultSqliteDatabasePassword;
|
||||
|
||||
NSString *defaultSqliteDatabasePassword = FLEXManager.sharedManager.defaultSqliteDatabasePassword;
|
||||
if (defaultSqliteDatabasePassword) {
|
||||
const char *key = defaultSqliteDatabasePassword.UTF8String;
|
||||
|
||||
sqlite3_key(_db, key, (int)strlen(key));
|
||||
}
|
||||
#endif
|
||||
|
||||
if(err != SQLITE_OK) {
|
||||
if (err != SQLITE_OK) {
|
||||
NSLog(@"error opening!: %d", err);
|
||||
return NO;
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
|
||||
- (BOOL)close {
|
||||
if (!_db) {
|
||||
if (!self.db) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
int rc;
|
||||
BOOL retry;
|
||||
BOOL triedFinalizingOpenStatements = NO;
|
||||
BOOL retry, triedFinalizingOpenStatements = NO;
|
||||
|
||||
do {
|
||||
retry = NO;
|
||||
rc = sqlite3_close(_db);
|
||||
retry = NO;
|
||||
rc = sqlite3_close(_db);
|
||||
if (SQLITE_BUSY == rc || SQLITE_LOCKED == rc) {
|
||||
if (!triedFinalizingOpenStatements) {
|
||||
triedFinalizingOpenStatements = YES;
|
||||
@@ -72,126 +80,127 @@ static NSString *const QUERY_TABLENAMES_SQL = @"SELECT name FROM sqlite_master W
|
||||
retry = YES;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (SQLITE_OK != rc) {
|
||||
} else if (SQLITE_OK != rc) {
|
||||
NSLog(@"error closing!: %d", rc);
|
||||
}
|
||||
}
|
||||
while (retry);
|
||||
} while (retry);
|
||||
|
||||
_db = nil;
|
||||
self.db = nil;
|
||||
return YES;
|
||||
}
|
||||
|
||||
|
||||
- (NSArray<NSDictionary<NSString *, id> *> *)queryAllTables {
|
||||
return [self executeQuery:QUERY_TABLENAMES_SQL];
|
||||
- (NSArray<NSString *> *)queryAllTables {
|
||||
return [[self executeStatement:QUERY_TABLENAMES].rows flex_mapped:^id(NSArray *table, NSUInteger idx) {
|
||||
return table.firstObject;
|
||||
}];
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)queryAllColumnsWithTableName:(NSString *)tableName {
|
||||
- (NSArray<NSString *> *)queryAllColumnsOfTable:(NSString *)tableName {
|
||||
NSString *sql = [NSString stringWithFormat:@"PRAGMA table_info('%@')",tableName];
|
||||
NSArray<NSDictionary<NSString *, id> *> *resultArray = [self executeQuery:sql];
|
||||
NSMutableArray<NSString *> *array = [NSMutableArray array];
|
||||
for (NSDictionary<NSString *, id> *dict in resultArray) {
|
||||
NSString *columnName = (NSString *)dict[@"name"] ?: @"";
|
||||
[array addObject:columnName];
|
||||
}
|
||||
return array;
|
||||
FLEXSQLResult *results = [self executeStatement:sql];
|
||||
|
||||
return [results.keyedRows flex_mapped:^id(NSDictionary *column, NSUInteger idx) {
|
||||
return column[@"name"];
|
||||
}];
|
||||
}
|
||||
|
||||
- (NSArray<NSDictionary<NSString *, id> *> *)queryAllDataWithTableName:(NSString *)tableName {
|
||||
NSString *sql = [NSString stringWithFormat:@"SELECT * FROM %@",tableName];
|
||||
return [self executeQuery:sql];
|
||||
- (NSArray<NSArray *> *)queryAllDataInTable:(NSString *)tableName {
|
||||
return [self executeStatement:[@"SELECT * FROM "
|
||||
stringByAppendingString:tableName
|
||||
]].rows;
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark - Private
|
||||
|
||||
- (NSArray<NSDictionary<NSString *, id> *> *)executeQuery:(NSString *)sql {
|
||||
- (FLEXSQLResult *)executeStatement:(NSString *)sql {
|
||||
[self open];
|
||||
NSMutableArray<NSDictionary<NSString *, id> *> *resultArray = [NSMutableArray array];
|
||||
|
||||
FLEXSQLResult *result = nil;
|
||||
|
||||
sqlite3_stmt *pstmt;
|
||||
if (sqlite3_prepare_v2(_db, sql.UTF8String, -1, &pstmt, 0) == SQLITE_OK) {
|
||||
while (sqlite3_step(pstmt) == SQLITE_ROW) {
|
||||
NSUInteger num_cols = (NSUInteger)sqlite3_data_count(pstmt);
|
||||
if (num_cols > 0) {
|
||||
NSMutableDictionary<NSString *, id> *dict = [NSMutableDictionary dictionaryWithCapacity:num_cols];
|
||||
|
||||
int columnCount = sqlite3_column_count(pstmt);
|
||||
|
||||
int columnIdx = 0;
|
||||
for (columnIdx = 0; columnIdx < columnCount; columnIdx++) {
|
||||
|
||||
NSString *columnName = [NSString stringWithUTF8String:sqlite3_column_name(pstmt, columnIdx)];
|
||||
id objectValue = [self objectForColumnIndex:columnIdx stmt:pstmt];
|
||||
[dict setObject:objectValue forKey:columnName];
|
||||
}
|
||||
[resultArray addObject:dict];
|
||||
NSMutableArray<NSArray *> *rows = [NSMutableArray new];
|
||||
|
||||
// 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));
|
||||
}];
|
||||
|
||||
// Execute statement
|
||||
int status;
|
||||
while ((status = sqlite3_step(pstmt)) == SQLITE_ROW) {
|
||||
// Grab rows if this is a selection query
|
||||
int dataCount = sqlite3_data_count(pstmt);
|
||||
if (dataCount > 0) {
|
||||
[rows addObject:[NSArray flex_forEachUpTo:columnCount map:^id(NSUInteger i) {
|
||||
return [self objectForColumnIndex:(int)i stmt:pstmt];
|
||||
}]];
|
||||
}
|
||||
}
|
||||
|
||||
if (status == SQLITE_DONE) {
|
||||
if (rows.count) {
|
||||
// We selected some rows
|
||||
result = [FLEXSQLResult columns:columns rows:rows];
|
||||
} else {
|
||||
// We executed a query like INSERT, UDPATE, or DELETE
|
||||
int rowsAffected = sqlite3_changes(_db);
|
||||
NSString *message = [NSString stringWithFormat:@"%d row(s) affected", rowsAffected];
|
||||
result = [FLEXSQLResult message:message];
|
||||
}
|
||||
} else {
|
||||
// An error occured executing the query
|
||||
result = [FLEXSQLResult message:@(sqlite3_errmsg(_db) ?: "(Execution: empty error)")];
|
||||
}
|
||||
} else {
|
||||
// An error occurred creating the prepared statement
|
||||
result = [FLEXSQLResult message:@(sqlite3_errmsg(_db) ?: "(Prepared statement: empty error)")];
|
||||
}
|
||||
|
||||
sqlite3_finalize(pstmt);
|
||||
[self close];
|
||||
return resultArray;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
- (id)objectForColumnIndex:(int)columnIdx stmt:(sqlite3_stmt*)stmt {
|
||||
int columnType = sqlite3_column_type(stmt, columnIdx);
|
||||
|
||||
id returnValue = nil;
|
||||
|
||||
if (columnType == SQLITE_INTEGER) {
|
||||
returnValue = [NSNumber numberWithLongLong:sqlite3_column_int64(stmt, columnIdx)];
|
||||
switch (columnType) {
|
||||
case SQLITE_INTEGER:
|
||||
return @(sqlite3_column_int64(stmt, columnIdx)).stringValue;
|
||||
case SQLITE_FLOAT:
|
||||
return @(sqlite3_column_double(stmt, columnIdx)).stringValue;
|
||||
case SQLITE_BLOB:
|
||||
return [NSString stringWithFormat:@"Data (%@ bytes)",
|
||||
@([self dataForColumnIndex:columnIdx stmt:stmt].length)
|
||||
];
|
||||
|
||||
default:
|
||||
// Default to a string for everything else
|
||||
return [self stringForColumnIndex:columnIdx stmt:stmt] ?: NSNull.null;
|
||||
}
|
||||
else if (columnType == SQLITE_FLOAT) {
|
||||
returnValue = [NSNumber numberWithDouble:sqlite3_column_double(stmt, columnIdx)];
|
||||
}
|
||||
else if (columnType == SQLITE_BLOB) {
|
||||
returnValue = [self dataForColumnIndex:columnIdx stmt:stmt];
|
||||
}
|
||||
else {
|
||||
//default to a string for everything else
|
||||
returnValue = [self stringForColumnIndex:columnIdx stmt:stmt];
|
||||
}
|
||||
|
||||
if (returnValue == nil) {
|
||||
returnValue = [NSNull null];
|
||||
}
|
||||
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)stringForColumnIndex:(int)columnIdx stmt:(sqlite3_stmt *)stmt {
|
||||
if (sqlite3_column_type(stmt, columnIdx) == SQLITE_NULL || columnIdx < 0) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
const char *text = (const char *)sqlite3_column_text(stmt, columnIdx);
|
||||
return text ? @(text) : nil;
|
||||
}
|
||||
|
||||
- (NSData *)dataForColumnIndex:(int)columnIdx stmt:(sqlite3_stmt *)stmt {
|
||||
if (sqlite3_column_type(stmt, columnIdx) == SQLITE_NULL || (columnIdx < 0)) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
const char *c = (const char *)sqlite3_column_text(stmt, columnIdx);
|
||||
const void *blob = sqlite3_column_blob(stmt, columnIdx);
|
||||
NSInteger size = (NSInteger)sqlite3_column_bytes(stmt, columnIdx);
|
||||
|
||||
if (!c) {
|
||||
// null row.
|
||||
return nil;
|
||||
}
|
||||
|
||||
return [NSString stringWithUTF8String:c];
|
||||
return blob ? [NSData dataWithBytes:blob length:size] : nil;
|
||||
}
|
||||
|
||||
- (NSData *)dataForColumnIndex:(int)columnIdx stmt:(sqlite3_stmt *)stmt{
|
||||
|
||||
if (sqlite3_column_type(stmt, columnIdx) == SQLITE_NULL || (columnIdx < 0)) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
const char *dataBuffer = sqlite3_column_blob(stmt, columnIdx);
|
||||
int dataSize = sqlite3_column_bytes(stmt, columnIdx);
|
||||
|
||||
if (dataBuffer == NULL) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return [NSData dataWithBytes:(const void *)dataBuffer length:(NSUInteger)dataSize];
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
|
||||
@@ -14,11 +14,25 @@ typedef NS_ENUM(NSUInteger, FLEXTableColumnHeaderSortType) {
|
||||
FLEXTableColumnHeaderSortTypeDesc,
|
||||
};
|
||||
|
||||
NS_INLINE FLEXTableColumnHeaderSortType FLEXNextTableColumnHeaderSortType(
|
||||
FLEXTableColumnHeaderSortType current) {
|
||||
switch (current) {
|
||||
case FLEXTableColumnHeaderSortTypeAsc:
|
||||
return FLEXTableColumnHeaderSortTypeDesc;
|
||||
case FLEXTableColumnHeaderSortTypeNone:
|
||||
case FLEXTableColumnHeaderSortTypeDesc:
|
||||
return FLEXTableColumnHeaderSortTypeAsc;
|
||||
}
|
||||
|
||||
return FLEXTableColumnHeaderSortTypeNone;
|
||||
}
|
||||
|
||||
@interface FLEXTableColumnHeader : UIView
|
||||
|
||||
@property (nonatomic) UILabel *label;
|
||||
@property (nonatomic) NSInteger index;
|
||||
@property (nonatomic, readonly) UILabel *titleLabel;
|
||||
|
||||
- (void)changeSortStatusWithType:(FLEXTableColumnHeaderSortType)type;
|
||||
@property (nonatomic) FLEXTableColumnHeaderSortType sortType;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -7,36 +7,41 @@
|
||||
//
|
||||
|
||||
#import "FLEXTableColumnHeader.h"
|
||||
#import "FLEXColor.h"
|
||||
#import "UIFont+FLEX.h"
|
||||
#import "FLEXUtility.h"
|
||||
|
||||
@implementation FLEXTableColumnHeader {
|
||||
UILabel *_arrowLabel;
|
||||
}
|
||||
@interface FLEXTableColumnHeader ()
|
||||
@property (nonatomic, readonly) UILabel *arrowLabel;
|
||||
@property (nonatomic, readonly) UIView *lineView;
|
||||
@end
|
||||
|
||||
@implementation FLEXTableColumnHeader
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
self.backgroundColor = UIColor.whiteColor;
|
||||
self.backgroundColor = FLEXColor.secondaryBackgroundColor;
|
||||
|
||||
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(5, 0, frame.size.width - 25, frame.size.height)];
|
||||
label.font = [UIFont systemFontOfSize:13.0];
|
||||
[self addSubview:label];
|
||||
self.label = label;
|
||||
_titleLabel = [UILabel new];
|
||||
_titleLabel.font = UIFont.flex_defaultTableCellFont;
|
||||
[self addSubview:_titleLabel];
|
||||
|
||||
|
||||
_arrowLabel = [[UILabel alloc] initWithFrame:CGRectMake(frame.size.width - 20, 0, 20, frame.size.height)];
|
||||
_arrowLabel.font = [UIFont systemFontOfSize:13.0];
|
||||
_arrowLabel = [UILabel new];
|
||||
_arrowLabel.font = UIFont.flex_defaultTableCellFont;
|
||||
[self addSubview:_arrowLabel];
|
||||
|
||||
UIView *line = [[UIView alloc] initWithFrame:CGRectMake(frame.size.width - 1, 2, 1, frame.size.height - 4)];
|
||||
line.backgroundColor = [UIColor colorWithWhite:0.803 alpha:0.850];
|
||||
[self addSubview:line];
|
||||
_lineView = [UIView new];
|
||||
_lineView.backgroundColor = FLEXColor.hairlineColor;
|
||||
[self addSubview:_lineView];
|
||||
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)changeSortStatusWithType:(FLEXTableColumnHeaderSortType)type {
|
||||
- (void)setSortType:(FLEXTableColumnHeaderSortType)type {
|
||||
_sortType = type;
|
||||
|
||||
switch (type) {
|
||||
case FLEXTableColumnHeaderSortTypeNone:
|
||||
_arrowLabel.text = @"";
|
||||
@@ -50,8 +55,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
|
||||
CGSize size = self.frame.size;
|
||||
|
||||
self.titleLabel.frame = CGRectMake(5, 0, size.width - 25, size.height);
|
||||
self.arrowLabel.frame = CGRectMake(size.width - 20, 0, 20, size.height);
|
||||
self.lineView.frame = CGRectMake(size.width - 1, 2, FLEXPointsToPixels(1), size.height - 4);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
//
|
||||
// FLEXTableContentCell.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Peng Tao on 15/11/24.
|
||||
// Copyright © 2015年 f. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@class FLEXTableContentCell;
|
||||
@protocol FLEXTableContentCellDelegate <NSObject>
|
||||
|
||||
@optional
|
||||
- (void)tableContentCell:(FLEXTableContentCell *)tableView labelDidTapWithText:(NSString *)text;
|
||||
|
||||
@end
|
||||
|
||||
@interface FLEXTableContentCell : UITableViewCell
|
||||
|
||||
@property (nonatomic) NSArray<UILabel *> *labels;
|
||||
|
||||
@property (nonatomic, weak) id<FLEXTableContentCellDelegate> delegate;
|
||||
|
||||
+ (instancetype)cellWithTableView:(UITableView *)tableView columnNumber:(NSInteger)number;
|
||||
|
||||
@end
|
||||
@@ -1,63 +0,0 @@
|
||||
//
|
||||
// FLEXTableContentCell.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Peng Tao on 15/11/24.
|
||||
// Copyright © 2015年 f. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXTableContentCell.h"
|
||||
#import "FLEXMultiColumnTableView.h"
|
||||
|
||||
@interface FLEXTableContentCell ()
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXTableContentCell
|
||||
|
||||
+ (instancetype)cellWithTableView:(UITableView *)tableView columnNumber:(NSInteger)number; {
|
||||
static NSString *identifier = @"FLEXTableContentCell";
|
||||
FLEXTableContentCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
|
||||
if (!cell) {
|
||||
cell = [[FLEXTableContentCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
|
||||
NSMutableArray<UILabel *> *labels = [NSMutableArray array];
|
||||
for (int i = 0; i < number ; i++) {
|
||||
UILabel *label = [[UILabel alloc] initWithFrame:CGRectZero];
|
||||
label.backgroundColor = UIColor.whiteColor;
|
||||
label.font = [UIFont systemFontOfSize:13.0];
|
||||
label.textAlignment = NSTextAlignmentLeft;
|
||||
label.backgroundColor = UIColor.greenColor;
|
||||
[labels addObject:label];
|
||||
|
||||
UITapGestureRecognizer *gesture = [[UITapGestureRecognizer alloc] initWithTarget:cell
|
||||
action:@selector(labelDidTap:)];
|
||||
[label addGestureRecognizer:gesture];
|
||||
label.userInteractionEnabled = YES;
|
||||
|
||||
[cell.contentView addSubview:label];
|
||||
cell.contentView.backgroundColor = UIColor.whiteColor;
|
||||
}
|
||||
cell.labels = labels;
|
||||
}
|
||||
return cell;
|
||||
}
|
||||
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
CGFloat labelWidth = self.contentView.frame.size.width / self.labels.count;
|
||||
CGFloat labelHeight = self.contentView.frame.size.height;
|
||||
for (int i = 0; i < self.labels.count; i++) {
|
||||
UILabel *label = self.labels[i];
|
||||
label.frame = CGRectMake(labelWidth * i + 5, 0, (labelWidth - 10), labelHeight);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (void)labelDidTap:(UIGestureRecognizer *)gesture {
|
||||
UILabel *label = (UILabel *)gesture.view;
|
||||
if ([self.delegate respondsToSelector:@selector(tableContentCell:labelDidTapWithText:)]) {
|
||||
[self.delegate tableContentCell:self labelDidTapWithText:label.text];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
@interface FLEXTableContentViewController : UIViewController
|
||||
|
||||
@property (nonatomic) NSArray<NSString *> *columnsArray;
|
||||
@property (nonatomic) NSArray<NSDictionary<NSString *, id> *> *contentsArray;
|
||||
+ (instancetype)columns:(NSArray<NSString *> *)columnNames
|
||||
rows:(NSArray<NSArray<NSString *> *> *)rowData;
|
||||
|
||||
@end
|
||||
|
||||
@@ -9,81 +9,73 @@
|
||||
#import "FLEXTableContentViewController.h"
|
||||
#import "FLEXMultiColumnTableView.h"
|
||||
#import "FLEXWebViewController.h"
|
||||
#import "FLEXUtility.h"
|
||||
|
||||
|
||||
@interface FLEXTableContentViewController ()<FLEXMultiColumnTableViewDataSource, FLEXMultiColumnTableViewDelegate>
|
||||
@interface FLEXTableContentViewController () <
|
||||
FLEXMultiColumnTableViewDataSource, FLEXMultiColumnTableViewDelegate
|
||||
>
|
||||
@property (nonatomic, readonly) NSArray<NSString *> *columns;
|
||||
@property (nonatomic, copy) NSArray<NSArray *> *rows;
|
||||
|
||||
@property (nonatomic) FLEXMultiColumnTableView *multiColumnView;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXTableContentViewController
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
self.edgesForExtendedLayout = UIRectEdgeNone;
|
||||
+ (instancetype)columns:(NSArray<NSString *> *)columnNames
|
||||
rows:(NSArray<NSArray<NSString *> *> *)rowData {
|
||||
FLEXTableContentViewController *controller = [self new];
|
||||
controller->_columns = columnNames;
|
||||
controller->_rows = rowData;
|
||||
return controller;
|
||||
}
|
||||
|
||||
- (void)loadView {
|
||||
[super loadView];
|
||||
|
||||
[self.view addSubview:self.multiColumnView];
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated {
|
||||
[super viewWillAppear:animated];
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
self.edgesForExtendedLayout = UIRectEdgeNone;
|
||||
[self.multiColumnView reloadData];
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
#pragma mark init SubView
|
||||
- (FLEXMultiColumnTableView *)multiColumnView {
|
||||
if (!_multiColumnView) {
|
||||
_multiColumnView = [[FLEXMultiColumnTableView alloc] initWithFrame:
|
||||
CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];
|
||||
_multiColumnView = [[FLEXMultiColumnTableView alloc]
|
||||
initWithFrame:FLEXRectSetSize(CGRectZero, self.view.frame.size)
|
||||
];
|
||||
|
||||
_multiColumnView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleTopMargin;
|
||||
_multiColumnView.backgroundColor = UIColor.whiteColor;
|
||||
_multiColumnView.dataSource = self;
|
||||
_multiColumnView.delegate = self;
|
||||
_multiColumnView.dataSource = self;
|
||||
_multiColumnView.delegate = self;
|
||||
}
|
||||
|
||||
return _multiColumnView;
|
||||
}
|
||||
|
||||
#pragma mark MultiColumnTableView DataSource
|
||||
|
||||
- (NSInteger)numberOfColumnsInTableView:(FLEXMultiColumnTableView *)tableView {
|
||||
return self.columnsArray.count;
|
||||
return self.columns.count;
|
||||
}
|
||||
|
||||
- (NSInteger)numberOfRowsInTableView:(FLEXMultiColumnTableView *)tableView {
|
||||
return self.contentsArray.count;
|
||||
return self.rows.count;
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)columnNameInColumn:(NSInteger)column {
|
||||
return self.columnsArray[column];
|
||||
- (NSString *)columnTitle:(NSInteger)column {
|
||||
return self.columns[column];
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)rowNameInRow:(NSInteger)row {
|
||||
return [NSString stringWithFormat:@"%ld",(long)row];
|
||||
- (NSString *)rowTitle:(NSInteger)row {
|
||||
return @(row).stringValue;
|
||||
}
|
||||
|
||||
- (NSString *)contentAtColumn:(NSInteger)column row:(NSInteger)row {
|
||||
if (self.contentsArray.count > row) {
|
||||
NSDictionary<NSString *, id> *dic = self.contentsArray[row];
|
||||
if (self.contentsArray.count > column) {
|
||||
return [NSString stringWithFormat:@"%@",[dic objectForKey:self.columnsArray[column]]];
|
||||
}
|
||||
}
|
||||
return @"";
|
||||
}
|
||||
|
||||
- (NSArray *)contentAtRow:(NSInteger)row {
|
||||
NSMutableArray *result = [NSMutableArray array];
|
||||
if (self.contentsArray.count > row) {
|
||||
NSDictionary<NSString *, id> *dic = self.contentsArray[row];
|
||||
for (int i = 0; i < self.columnsArray.count; i ++) {
|
||||
[result addObject:dic[self.columnsArray[i]]];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return nil;
|
||||
- (NSArray *)contentForRow:(NSInteger)row {
|
||||
return self.rows[row];
|
||||
}
|
||||
|
||||
- (CGFloat)multiColumnTableView:(FLEXMultiColumnTableView *)tableView
|
||||
@@ -101,48 +93,58 @@
|
||||
}
|
||||
|
||||
- (CGFloat)widthForLeftHeaderInTableView:(FLEXMultiColumnTableView *)tableView {
|
||||
NSString *str = [NSString stringWithFormat:@"%lu",(unsigned long)self.contentsArray.count];
|
||||
NSDictionary<NSString *, id> *attrs = @{@"NSFontAttributeName":[UIFont systemFontOfSize:17.0]};
|
||||
CGSize size = [str boundingRectWithSize:CGSizeMake(CGFLOAT_MAX, 14)
|
||||
options:NSStringDrawingUsesLineFragmentOrigin
|
||||
attributes:attrs context:nil].size;
|
||||
NSString *str = [NSString stringWithFormat:@"%lu",(unsigned long)self.rows.count];
|
||||
NSDictionary *attrs = @{ NSFontAttributeName : [UIFont systemFontOfSize:17.0] };
|
||||
CGSize size = [str boundingRectWithSize:CGSizeMake(CGFLOAT_MAX, 14)
|
||||
options:NSStringDrawingUsesLineFragmentOrigin
|
||||
attributes:attrs context:nil
|
||||
].size;
|
||||
|
||||
return size.width + 20;
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
#pragma mark MultiColumnTableView Delegate
|
||||
|
||||
|
||||
- (void)multiColumnTableView:(FLEXMultiColumnTableView *)tableView didTapLabelWithText:(NSString *)text {
|
||||
FLEXWebViewController * detailViewController = [[FLEXWebViewController alloc] initWithText:text];
|
||||
[self.navigationController pushViewController:detailViewController animated:YES];
|
||||
- (void)multiColumnTableView:(FLEXMultiColumnTableView *)tableView didSelectRow:(NSInteger)row {
|
||||
NSArray<NSString *> *fields = [self.rows[row] flex_mapped:^id(NSString *field, NSUInteger idx) {
|
||||
return [NSString stringWithFormat:@"%@:\n%@", self.columns[idx], field];
|
||||
}];
|
||||
|
||||
[FLEXAlert makeAlert:^(FLEXAlert *make) {
|
||||
make.title([@"Row " stringByAppendingString:@(row).stringValue]);
|
||||
make.message([fields componentsJoinedByString:@"\n\n"]);
|
||||
make.button(@"Dismiss").cancelStyle();
|
||||
} showFrom:self];
|
||||
}
|
||||
|
||||
- (void)multiColumnTableView:(FLEXMultiColumnTableView *)tableView didTapHeaderWithText:(NSString *)text sortType:(FLEXTableColumnHeaderSortType)sortType {
|
||||
- (void)multiColumnTableView:(FLEXMultiColumnTableView *)tableView
|
||||
didSelectHeaderForColumn:(NSInteger)column
|
||||
sortType:(FLEXTableColumnHeaderSortType)sortType {
|
||||
|
||||
NSArray<NSDictionary<NSString *, id> *> *sortContentData = [self.contentsArray sortedArrayUsingComparator:^NSComparisonResult(NSDictionary<NSString *, id> * obj1, NSDictionary<NSString *, id> * obj2) {
|
||||
|
||||
if ([obj1 objectForKey:text] == [NSNull null]) {
|
||||
return NSOrderedAscending;
|
||||
}
|
||||
if ([obj2 objectForKey:text] == [NSNull null]) {
|
||||
return NSOrderedDescending;
|
||||
}
|
||||
|
||||
if (![[obj1 objectForKey:text] respondsToSelector:@selector(compare:)] && ![[obj2 objectForKey:text] respondsToSelector:@selector(compare:)]) {
|
||||
NSArray<NSArray *> *sortContentData = [self.rows
|
||||
sortedArrayUsingComparator:^NSComparisonResult(NSArray *obj1, NSArray *obj2) {
|
||||
id a = obj1[column], b = obj2[column];
|
||||
if (a == NSNull.null) {
|
||||
return NSOrderedAscending;
|
||||
}
|
||||
if (b == NSNull.null) {
|
||||
return NSOrderedDescending;
|
||||
}
|
||||
|
||||
if ([a respondsToSelector:@selector(compare:)] && [b respondsToSelector:@selector(compare:)]) {
|
||||
return [a compare:b];
|
||||
}
|
||||
|
||||
return NSOrderedSame;
|
||||
}
|
||||
|
||||
NSComparisonResult result = [[obj1 objectForKey:text] compare:[obj2 objectForKey:text]];
|
||||
|
||||
return result;
|
||||
}];
|
||||
];
|
||||
|
||||
if (sortType == FLEXTableColumnHeaderSortTypeDesc) {
|
||||
NSEnumerator *contentReverseEnumerator = sortContentData.reverseObjectEnumerator;
|
||||
sortContentData = [NSArray arrayWithArray:contentReverseEnumerator.allObjects];
|
||||
sortContentData = sortContentData.reverseObjectEnumerator.allObjects.copy;
|
||||
}
|
||||
|
||||
self.contentsArray = sortContentData;
|
||||
self.rows = sortContentData;
|
||||
[self.multiColumnView reloadData];
|
||||
}
|
||||
|
||||
@@ -151,19 +153,18 @@
|
||||
|
||||
- (void)willTransitionToTraitCollection:(UITraitCollection *)newCollection
|
||||
withTransitionCoordinator:(id <UIViewControllerTransitionCoordinator>)coordinator {
|
||||
[super willTransitionToTraitCollection:newCollection
|
||||
withTransitionCoordinator:coordinator];
|
||||
[super willTransitionToTraitCollection:newCollection withTransitionCoordinator:coordinator];
|
||||
|
||||
[coordinator animateAlongsideTransition:^(id <UIViewControllerTransitionCoordinatorContext> context) {
|
||||
if (newCollection.verticalSizeClass == UIUserInterfaceSizeClassCompact) {
|
||||
|
||||
self->_multiColumnView.frame = CGRectMake(0, 32, self.view.frame.size.width, self.view.frame.size.height - 32);
|
||||
self.multiColumnView.frame = CGRectMake(0, 32, self.view.frame.size.width, self.view.frame.size.height - 32);
|
||||
}
|
||||
else {
|
||||
self->_multiColumnView.frame = CGRectMake(0, 64, self.view.frame.size.width, self.view.frame.size.height - 64);
|
||||
self.multiColumnView.frame = CGRectMake(0, 64, self.view.frame.size.width, self.view.frame.size.height - 64);
|
||||
}
|
||||
|
||||
[self.view setNeedsLayout];
|
||||
} completion:nil];
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
|
||||
@@ -16,13 +16,13 @@
|
||||
|
||||
if (!cell) {
|
||||
cell = [[FLEXTableLeftCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
|
||||
UILabel *textLabel = [[UILabel alloc] initWithFrame:CGRectZero];
|
||||
UILabel *textLabel = [UILabel new];
|
||||
textLabel.textAlignment = NSTextAlignmentCenter;
|
||||
textLabel.font = [UIFont systemFontOfSize:13.0];
|
||||
textLabel.backgroundColor = UIColor.clearColor;
|
||||
[cell.contentView addSubview:textLabel];
|
||||
cell.titlelabel = textLabel;
|
||||
}
|
||||
|
||||
return cell;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
// Copyright © 2015年 Peng Tao. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXTableViewController.h"
|
||||
#import "FLEXFilteringTableViewController.h"
|
||||
|
||||
@interface FLEXTableListViewController : FLEXTableViewController
|
||||
@interface FLEXTableListViewController : FLEXFilteringTableViewController
|
||||
|
||||
+ (BOOL)supportsExtension:(NSString *)extension;
|
||||
- (instancetype)initWithPath:(NSString *)path;
|
||||
|
||||
@@ -7,20 +7,19 @@
|
||||
//
|
||||
|
||||
#import "FLEXTableListViewController.h"
|
||||
|
||||
#import "FLEXDatabaseManager.h"
|
||||
#import "FLEXSQLiteDatabaseManager.h"
|
||||
#import "FLEXRealmDatabaseManager.h"
|
||||
|
||||
#import "FLEXTableContentViewController.h"
|
||||
#import "FLEXMutableListSection.h"
|
||||
#import "NSArray+Functional.h"
|
||||
#import "FLEXAlert.h"
|
||||
|
||||
@interface FLEXTableListViewController () {
|
||||
id<FLEXDatabaseManager> _dbm;
|
||||
NSString *_databasePath;
|
||||
}
|
||||
@interface FLEXTableListViewController ()
|
||||
@property (nonatomic, readonly) id<FLEXDatabaseManager> dbm;
|
||||
@property (nonatomic, readonly) NSString *path;
|
||||
|
||||
@property (nonatomic) NSArray<NSString *> *tables;
|
||||
@property (nonatomic) NSArray<NSString *> *filteredTables;
|
||||
@property (nonatomic, readonly) FLEXMutableListSection<NSString *> *tables;
|
||||
|
||||
+ (NSArray<NSString *> *)supportedSQLiteExtensions;
|
||||
+ (NSArray<NSString *> *)supportedRealmExtensions;
|
||||
@@ -32,102 +31,113 @@
|
||||
- (instancetype)initWithPath:(NSString *)path {
|
||||
self = [super initWithStyle:UITableViewStyleGrouped];
|
||||
if (self) {
|
||||
_databasePath = [path copy];
|
||||
_dbm = [self databaseManagerForFileAtPath:_databasePath];
|
||||
[_dbm open];
|
||||
[self getAllTables];
|
||||
_path = path.copy;
|
||||
_dbm = [self databaseManagerForFileAtPath:path];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
self.showsSearchBar = YES;
|
||||
|
||||
// Compose query button //
|
||||
|
||||
UIBarButtonItem *composeQuery = [[UIBarButtonItem alloc]
|
||||
initWithBarButtonSystemItem:UIBarButtonSystemItemCompose
|
||||
target:self
|
||||
action:@selector(queryButtonPressed)
|
||||
];
|
||||
// Cannot run custom queries on realm databases
|
||||
composeQuery.enabled = [self.dbm
|
||||
respondsToSelector:@selector(executeStatement:)
|
||||
];
|
||||
|
||||
[self addToolbarItems:@[composeQuery]];
|
||||
}
|
||||
|
||||
- (NSArray<FLEXTableViewSection *> *)makeSections {
|
||||
_tables = [FLEXMutableListSection list:[self.dbm queryAllTables]
|
||||
cellConfiguration:^(__kindof UITableViewCell *cell, NSString *tableName, NSInteger row) {
|
||||
cell.textLabel.text = tableName;
|
||||
} filterMatcher:^BOOL(NSString *filterText, NSString *tableName) {
|
||||
return [tableName localizedCaseInsensitiveContainsString:filterText];
|
||||
}
|
||||
];
|
||||
|
||||
self.tables.selectionHandler = ^(FLEXTableListViewController *host, NSString *tableName) {
|
||||
NSArray *rows = [host.dbm queryAllDataInTable:tableName];
|
||||
NSArray *columns = [host.dbm queryAllColumnsOfTable:tableName];
|
||||
|
||||
UIViewController *resultsScreen = [FLEXTableContentViewController columns:columns rows:rows];
|
||||
resultsScreen.title = tableName;
|
||||
[host.navigationController pushViewController:resultsScreen animated:YES];
|
||||
};
|
||||
|
||||
return @[self.tables];
|
||||
}
|
||||
|
||||
- (void)reloadData {
|
||||
self.tables.customTitle = [NSString
|
||||
stringWithFormat:@"Tables (%@)", @(self.tables.filteredList.count)
|
||||
];
|
||||
|
||||
[super reloadData];
|
||||
}
|
||||
|
||||
- (void)queryButtonPressed {
|
||||
FLEXSQLiteDatabaseManager *database = self.dbm;
|
||||
|
||||
[FLEXAlert makeAlert:^(FLEXAlert *make) {
|
||||
make.title(@"Execute an SQL query");
|
||||
make.textField(nil);
|
||||
make.button(@"Run").handler(^(NSArray<NSString *> *strings) {
|
||||
FLEXSQLResult *result = [database executeStatement:strings[0]];
|
||||
|
||||
if (result.message) {
|
||||
[FLEXAlert showAlert:@"Message" message:result.message from:self];
|
||||
} else {
|
||||
UIViewController *resultsScreen = [FLEXTableContentViewController
|
||||
columns:result.columns rows:result.rows
|
||||
];
|
||||
|
||||
[self.navigationController pushViewController:resultsScreen animated:YES];
|
||||
}
|
||||
});
|
||||
make.button(@"Cancel").cancelStyle();
|
||||
} showFrom:self];
|
||||
}
|
||||
|
||||
- (id<FLEXDatabaseManager>)databaseManagerForFileAtPath:(NSString *)path {
|
||||
NSString *pathExtension = path.pathExtension.lowercaseString;
|
||||
|
||||
NSArray<NSString *> *sqliteExtensions = [FLEXTableListViewController supportedSQLiteExtensions];
|
||||
NSArray<NSString *> *sqliteExtensions = FLEXTableListViewController.supportedSQLiteExtensions;
|
||||
if ([sqliteExtensions indexOfObject:pathExtension] != NSNotFound) {
|
||||
return [[FLEXSQLiteDatabaseManager alloc] initWithPath:path];
|
||||
return [FLEXSQLiteDatabaseManager managerForDatabase:path];
|
||||
}
|
||||
|
||||
NSArray<NSString *> *realmExtensions = [FLEXTableListViewController supportedRealmExtensions];
|
||||
NSArray<NSString *> *realmExtensions = FLEXTableListViewController.supportedRealmExtensions;
|
||||
if (realmExtensions != nil && [realmExtensions indexOfObject:pathExtension] != NSNotFound) {
|
||||
return [[FLEXRealmDatabaseManager alloc] initWithPath:path];
|
||||
return [FLEXRealmDatabaseManager managerForDatabase:path];
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (void)getAllTables {
|
||||
NSArray<NSDictionary<NSString *, id> *> *resultArray = [_dbm queryAllTables];
|
||||
NSMutableArray<NSString *> *array = [NSMutableArray array];
|
||||
for (NSDictionary<NSString *, id> *dict in resultArray) {
|
||||
NSString *columnName = (NSString *)dict[@"name"] ?: @"";
|
||||
[array addObject:columnName];
|
||||
}
|
||||
self.tables = array;
|
||||
self.filteredTables = array;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
self.showsSearchBar = YES;
|
||||
}
|
||||
|
||||
#pragma mark - Search bar
|
||||
|
||||
- (void)updateSearchResults:(NSString *)searchText {
|
||||
if (searchText.length > 0) {
|
||||
NSPredicate *searchPredicate = [NSPredicate predicateWithFormat:@"SELF CONTAINS[cd] %@", searchText];
|
||||
self.filteredTables = [self.tables filteredArrayUsingPredicate:searchPredicate];
|
||||
} else {
|
||||
self.filteredTables = self.tables;
|
||||
}
|
||||
[self.tableView reloadData];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Table View Data Source
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
|
||||
return self.filteredTables.count;
|
||||
}
|
||||
|
||||
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"FLEXTableListViewControllerCell"];
|
||||
if (!cell) {
|
||||
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
|
||||
reuseIdentifier:@"FLEXTableListViewControllerCell"];
|
||||
}
|
||||
cell.textLabel.text = self.filteredTables[indexPath.row];
|
||||
return cell;
|
||||
}
|
||||
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
FLEXTableContentViewController *contentViewController = [FLEXTableContentViewController new];
|
||||
|
||||
contentViewController.contentsArray = [_dbm queryAllDataWithTableName:self.filteredTables[indexPath.row]];
|
||||
contentViewController.columnsArray = [_dbm queryAllColumnsWithTableName:self.filteredTables[indexPath.row]];
|
||||
|
||||
contentViewController.title = self.filteredTables[indexPath.row];
|
||||
[self.navigationController pushViewController:contentViewController animated:YES];
|
||||
}
|
||||
|
||||
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
|
||||
return [NSString stringWithFormat:@"Tables (%lu)", (unsigned long)self.filteredTables.count];
|
||||
}
|
||||
|
||||
#pragma mark - FLEXTableListViewController
|
||||
|
||||
+ (BOOL)supportsExtension:(NSString *)extension {
|
||||
extension = extension.lowercaseString;
|
||||
|
||||
NSArray<NSString *> *sqliteExtensions = [FLEXTableListViewController supportedSQLiteExtensions];
|
||||
NSArray<NSString *> *sqliteExtensions = FLEXTableListViewController.supportedSQLiteExtensions;
|
||||
if (sqliteExtensions.count > 0 && [sqliteExtensions indexOfObject:extension] != NSNotFound) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
NSArray<NSString *> *realmExtensions = [FLEXTableListViewController supportedRealmExtensions];
|
||||
NSArray<NSString *> *realmExtensions = FLEXTableListViewController.supportedRealmExtensions;
|
||||
if (realmExtensions.count > 0 && [realmExtensions indexOfObject:extension] != NSNotFound) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
#import "FLEXUtility.h"
|
||||
|
||||
@interface FLEXGlobalsViewController (FLEXAddressExploration)
|
||||
@interface UITableViewController (FLEXAddressExploration)
|
||||
- (void)deselectSelectedRow;
|
||||
- (void)tryExploreAddress:(NSString *)addressString safely:(BOOL)safely;
|
||||
@end
|
||||
@@ -26,8 +26,8 @@
|
||||
return @"🔎 Address Explorer";
|
||||
}
|
||||
|
||||
+ (FLEXGlobalsTableViewControllerRowAction)globalsEntryRowAction:(FLEXGlobalsRow)row {
|
||||
return ^(FLEXGlobalsViewController *host) {
|
||||
+ (FLEXGlobalsEntryRowAction)globalsEntryRowAction:(FLEXGlobalsRow)row {
|
||||
return ^(UITableViewController *host) {
|
||||
|
||||
NSString *title = @"Explore Object at Address";
|
||||
NSString *message = @"Paste a hexadecimal address below, starting with '0x'. "
|
||||
@@ -59,7 +59,7 @@
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXGlobalsViewController (FLEXAddressExploration)
|
||||
@implementation UITableViewController (FLEXAddressExploration)
|
||||
|
||||
- (void)deselectSelectedRow {
|
||||
NSIndexPath *selected = self.tableView.indexPathForSelectedRow;
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
//
|
||||
|
||||
#import "FLEXGlobalsEntry.h"
|
||||
#import "FLEXTableViewController.h"
|
||||
#import "FLEXFilteringTableViewController.h"
|
||||
|
||||
@interface FLEXCookiesTableViewController : FLEXTableViewController <FLEXGlobalsEntry>
|
||||
@interface FLEXCookiesTableViewController : FLEXFilteringTableViewController <FLEXGlobalsEntry>
|
||||
|
||||
@end
|
||||
|
||||
@@ -8,77 +8,60 @@
|
||||
|
||||
#import "FLEXCookiesTableViewController.h"
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
#import "FLEXMutableListSection.h"
|
||||
#import "FLEXUtility.h"
|
||||
|
||||
@interface FLEXCookiesTableViewController ()
|
||||
@property (nonatomic, readonly) NSArray<NSHTTPCookie *> *cookies;
|
||||
@property (nonatomic, readonly) FLEXMutableListSection<NSHTTPCookie *> *cookies;
|
||||
@property (nonatomic) NSString *headerTitle;
|
||||
@end
|
||||
|
||||
@implementation FLEXCookiesTableViewController
|
||||
|
||||
#pragma mark - Overrides
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
self.title = @"Cookies";
|
||||
}
|
||||
|
||||
- (NSArray<FLEXTableViewSection *> *)makeSections {
|
||||
NSSortDescriptor *nameSortDescriptor = [[NSSortDescriptor alloc]
|
||||
initWithKey:@"name" ascending:YES selector:@selector(caseInsensitiveCompare:)
|
||||
];
|
||||
_cookies = [NSHTTPCookieStorage.sharedHTTPCookieStorage.cookies
|
||||
sortedArrayUsingDescriptors:@[nameSortDescriptor]
|
||||
NSArray *cookies = [NSHTTPCookieStorage.sharedHTTPCookieStorage.cookies
|
||||
sortedArrayUsingDescriptors:@[nameSortDescriptor]
|
||||
];
|
||||
|
||||
self.title = @"Cookies";
|
||||
[self updateHeaderTitle];
|
||||
}
|
||||
|
||||
- (void)updateHeaderTitle {
|
||||
self.headerTitle = [NSString stringWithFormat:@"%@ cookies", @(self.cookies.count)];
|
||||
// TODO update header title here when we can search cookies
|
||||
}
|
||||
|
||||
- (NSHTTPCookie *)cookieForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
return self.cookies[indexPath.row];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Table View Data Source
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
|
||||
return self.cookies.count;
|
||||
}
|
||||
|
||||
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
static NSString *CellIdentifier = @"Cell";
|
||||
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
|
||||
if (!cell) {
|
||||
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];
|
||||
cell.textLabel.font = UIFont.flex_defaultTableCellFont;
|
||||
cell.detailTextLabel.font = UIFont.flex_defaultTableCellFont;
|
||||
cell.detailTextLabel.textColor = UIColor.grayColor;
|
||||
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
|
||||
}
|
||||
|
||||
NSHTTPCookie *cookie = [self cookieForRowAtIndexPath:indexPath];
|
||||
cell.textLabel.text = [NSString stringWithFormat:@"%@ (%@)", cookie.name, cookie.value];
|
||||
cell.detailTextLabel.text = cookie.domain;
|
||||
_cookies = [FLEXMutableListSection list:cookies
|
||||
cellConfiguration:^(UITableViewCell *cell, NSHTTPCookie *cookie, NSInteger row) {
|
||||
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
|
||||
cell.textLabel.text = [cookie.name stringByAppendingFormat:@" (%@)", cookie.value];
|
||||
cell.detailTextLabel.text = [cookie.domain stringByAppendingFormat:@" — %@", cookie.path];
|
||||
} filterMatcher:^BOOL(NSString *filterText, NSHTTPCookie *cookie) {
|
||||
return [cookie.name localizedCaseInsensitiveContainsString:filterText] ||
|
||||
[cookie.value localizedCaseInsensitiveContainsString:filterText] ||
|
||||
[cookie.domain localizedCaseInsensitiveContainsString:filterText] ||
|
||||
[cookie.path localizedCaseInsensitiveContainsString:filterText];
|
||||
}
|
||||
];
|
||||
|
||||
return cell;
|
||||
}
|
||||
|
||||
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
|
||||
return self.headerTitle;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Table View Delegate
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
NSHTTPCookie *cookie = [self cookieForRowAtIndexPath:indexPath];
|
||||
UIViewController *cookieViewController = (UIViewController *)[FLEXObjectExplorerFactory explorerViewControllerForObject:cookie];
|
||||
self.cookies.selectionHandler = ^(UIViewController *host, NSHTTPCookie *cookie) {
|
||||
[host.navigationController pushViewController:[
|
||||
FLEXObjectExplorerFactory explorerViewControllerForObject:cookie
|
||||
] animated:YES];
|
||||
};
|
||||
|
||||
[self.navigationController pushViewController:cookieViewController animated:YES];
|
||||
return @[self.cookies];
|
||||
}
|
||||
|
||||
- (void)reloadData {
|
||||
self.headerTitle = [NSString stringWithFormat:
|
||||
@"%@ cookies", @(self.cookies.filteredList.count)
|
||||
];
|
||||
[super reloadData];
|
||||
}
|
||||
|
||||
#pragma mark - FLEXGlobalsEntry
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
//
|
||||
// FLEXInstancesViewController.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 5/28/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface FLEXInstancesViewController : UITableViewController
|
||||
|
||||
+ (instancetype)instancesTableViewControllerForClassName:(NSString *)className;
|
||||
+ (instancetype)instancesTableViewControllerForInstancesReferencingObject:(id)object;
|
||||
|
||||
@end
|
||||
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 5/28/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXTableViewController.h"
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 5/28/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXLiveObjectsTableViewController.h"
|
||||
#import "FLEXHeapEnumerator.h"
|
||||
#import "FLEXInstancesViewController.h"
|
||||
#import "FLEXObjectListViewController.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXScopeCarousel.h"
|
||||
#import "FLEXTableView.h"
|
||||
@@ -30,14 +30,9 @@ static const NSInteger kFLEXLiveObjectsSortBySizeIndex = 2;
|
||||
|
||||
@implementation FLEXLiveObjectsTableViewController
|
||||
|
||||
- (void)loadView {
|
||||
self.tableView = [FLEXTableView flexDefaultTableView];
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
// self.title = @"Live Objects";
|
||||
self.showsSearchBar = YES;
|
||||
self.searchBarDebounceInterval = kFLEXDebounceInstant;
|
||||
self.showsCarousel = YES;
|
||||
@@ -75,8 +70,8 @@ static const NSInteger kFLEXLiveObjectsSortBySizeIndex = 2;
|
||||
}];
|
||||
|
||||
// Convert our CF primitive dictionary into a nicer mapping of class name strings to counts that we will use as the table's model.
|
||||
NSMutableDictionary<NSString *, NSNumber *> *mutableCountsForClassNames = [NSMutableDictionary dictionary];
|
||||
NSMutableDictionary<NSString *, NSNumber *> *mutableSizesForClassNames = [NSMutableDictionary dictionary];
|
||||
NSMutableDictionary<NSString *, NSNumber *> *mutableCountsForClassNames = [NSMutableDictionary new];
|
||||
NSMutableDictionary<NSString *, NSNumber *> *mutableSizesForClassNames = [NSMutableDictionary new];
|
||||
for (unsigned int i = 0; i < classCount; i++) {
|
||||
Class class = classes[i];
|
||||
NSUInteger instanceCount = (NSUInteger)CFDictionaryGetValue(mutableCountsForClasses, (__bridge const void *)(class));
|
||||
@@ -229,7 +224,7 @@ static const NSInteger kFLEXLiveObjectsSortBySizeIndex = 2;
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
NSString *className = self.filteredClassNames[indexPath.row];
|
||||
FLEXInstancesViewController *instancesViewController = [FLEXInstancesViewController instancesTableViewControllerForClassName:className];
|
||||
FLEXObjectListViewController *instancesViewController = [FLEXObjectListViewController instancesOfClassWithName:className];
|
||||
[self.navigationController pushViewController:instancesViewController animated:YES];
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// FLEXObjectListViewController.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 5/28/14.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXFilteringTableViewController.h"
|
||||
|
||||
@interface FLEXObjectListViewController : FLEXFilteringTableViewController
|
||||
|
||||
+ (instancetype)instancesOfClassWithName:(NSString *)className;
|
||||
+ (instancetype)subclassesOfClassWithName:(NSString *)className;
|
||||
+ (instancetype)objectsWithReferencesToObject:(id)object;
|
||||
|
||||
@end
|
||||
+164
-154
@@ -1,124 +1,39 @@
|
||||
//
|
||||
// FLEXInstancesViewController.m
|
||||
// FLEXObjectListViewController.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 5/28/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXInstancesViewController.h"
|
||||
#import "FLEXObjectListViewController.h"
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
#import "FLEXObjectExplorerViewController.h"
|
||||
#import "FLEXMutableListSection.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXHeapEnumerator.h"
|
||||
#import "FLEXObjectRef.h"
|
||||
#import "NSString+FLEX.h"
|
||||
#import "NSObject+Reflection.h"
|
||||
#import "FLEXTableViewCell.h"
|
||||
#import <malloc/malloc.h>
|
||||
|
||||
|
||||
@interface FLEXInstancesViewController ()
|
||||
@interface FLEXObjectListViewController ()
|
||||
@property (nonatomic, copy) NSArray<FLEXMutableListSection *> *sections;
|
||||
@property (nonatomic, copy) NSArray<FLEXMutableListSection *> *allSections;
|
||||
|
||||
/// Array of [[section], [section], ...]
|
||||
/// where [section] is [["row title", instance], ["row title", instance], ...]
|
||||
@property (nonatomic) NSArray<FLEXObjectRef *> *instances;
|
||||
@property (nonatomic) NSArray<NSArray<FLEXObjectRef*>*> *sections;
|
||||
@property (nonatomic) NSArray<NSString *> *sectionTitles;
|
||||
@property (nonatomic) NSArray<NSPredicate *> *predicates;
|
||||
@property (nonatomic, readonly) NSInteger maxSections;
|
||||
@property (nonatomic, readonly) NSArray<FLEXObjectRef *> *references;
|
||||
@property (nonatomic, readonly) NSArray<NSPredicate *> *predicates;
|
||||
@property (nonatomic, readonly) NSArray<NSString *> *sectionTitles;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXInstancesViewController
|
||||
@implementation FLEXObjectListViewController
|
||||
@dynamic sections, allSections;
|
||||
|
||||
- (id)initWithReferences:(NSArray<FLEXObjectRef *> *)references {
|
||||
return [self initWithReferences:references predicates:nil sectionTitles:nil];
|
||||
}
|
||||
|
||||
- (id)initWithReferences:(NSArray<FLEXObjectRef *> *)references
|
||||
predicates:(NSArray<NSPredicate *> *)predicates
|
||||
sectionTitles:(NSArray<NSString *> *)sectionTitles {
|
||||
NSParameterAssert(predicates.count == sectionTitles.count);
|
||||
|
||||
self = [super init];
|
||||
if (self) {
|
||||
self.instances = references;
|
||||
self.predicates = predicates;
|
||||
self.sectionTitles = sectionTitles;
|
||||
|
||||
if (predicates.count) {
|
||||
[self buildSections];
|
||||
} else {
|
||||
self.sections = @[references];
|
||||
}
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
+ (instancetype)instancesTableViewControllerForClassName:(NSString *)className {
|
||||
const char *classNameCString = className.UTF8String;
|
||||
NSMutableArray *instances = [NSMutableArray array];
|
||||
[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];
|
||||
FLEXInstancesViewController *viewController = [[self alloc] initWithReferences:references];
|
||||
viewController.title = [NSString stringWithFormat:@"%@ (%lu)", className, (unsigned long)instances.count];
|
||||
return viewController;
|
||||
}
|
||||
|
||||
+ (instancetype)instancesTableViewControllerForInstancesReferencingObject:(id)object {
|
||||
NSMutableArray<FLEXObjectRef *> *instances = [NSMutableArray array];
|
||||
[FLEXHeapEnumerator enumerateLiveObjectsUsingBlock:^(__unsafe_unretained id tryObject, __unsafe_unretained Class actualClass) {
|
||||
// Skip Swift objects
|
||||
if ([actualClass isKindOfClass:NSClassFromString(@"SwiftObject")]) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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 = [self defaultSectionTitles];
|
||||
FLEXInstancesViewController *viewController = [[self alloc] initWithReferences:instances
|
||||
predicates:predicates
|
||||
sectionTitles:sectionTitles];
|
||||
viewController.title = [NSString stringWithFormat:@"Referencing %@ %p", NSStringFromClass(object_getClass(object)), object];
|
||||
return viewController;
|
||||
}
|
||||
#pragma mark - Reference Grouping
|
||||
|
||||
+ (NSPredicate *)defaultPredicateForSection:(NSInteger)section {
|
||||
// These are the types of references that we typically don't care about.
|
||||
@@ -174,70 +89,165 @@
|
||||
return @[@"", @"AutoLayout", @"Trivial"];
|
||||
}
|
||||
|
||||
- (void)buildSections {
|
||||
NSInteger maxSections = self.maxSections;
|
||||
NSMutableArray *sections = [NSMutableArray array];
|
||||
for (NSInteger i = 0; i < maxSections; i++) {
|
||||
NSPredicate *predicate = self.predicates[i];
|
||||
[sections addObject:[self.instances filteredArrayUsingPredicate:predicate]];
|
||||
|
||||
#pragma mark - Initialization
|
||||
|
||||
- (id)initWithReferences:(NSArray<FLEXObjectRef *> *)references {
|
||||
return [self initWithReferences:references predicates:nil sectionTitles:nil];
|
||||
}
|
||||
|
||||
- (id)initWithReferences:(NSArray<FLEXObjectRef *> *)references
|
||||
predicates:(NSArray<NSPredicate *> *)predicates
|
||||
sectionTitles:(NSArray<NSString *> *)sectionTitles {
|
||||
NSParameterAssert(predicates.count == sectionTitles.count);
|
||||
|
||||
self = [super initWithStyle:UITableViewStylePlain];
|
||||
if (self) {
|
||||
_references = references;
|
||||
_predicates = predicates;
|
||||
_sectionTitles = sectionTitles;
|
||||
}
|
||||
|
||||
self.sections = sections;
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSInteger)maxSections {
|
||||
return self.predicates.count ?: 1;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Table View Data Source
|
||||
|
||||
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
|
||||
return self.maxSections;
|
||||
}
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
|
||||
return self.sections[section].count;
|
||||
}
|
||||
|
||||
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
static NSString *CellIdentifier = @"Cell";
|
||||
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
|
||||
if (!cell) {
|
||||
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];
|
||||
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
|
||||
UIFont *cellFont = UIFont.flex_defaultTableCellFont;
|
||||
cell.textLabel.font = cellFont;
|
||||
cell.detailTextLabel.font = cellFont;
|
||||
cell.detailTextLabel.textColor = UIColor.grayColor;
|
||||
}
|
||||
|
||||
FLEXObjectRef *row = self.sections[indexPath.section][indexPath.row];
|
||||
cell.textLabel.text = row.reference;
|
||||
cell.detailTextLabel.text = [FLEXRuntimeUtility summaryForObject:row.object];
|
||||
|
||||
return cell;
|
||||
}
|
||||
|
||||
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
|
||||
if (self.sectionTitles.count) {
|
||||
// Return nil instead of empty strings
|
||||
NSString *title = self.sectionTitles[section];
|
||||
if (title.length) {
|
||||
return title;
|
||||
+ (instancetype)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];
|
||||
FLEXObjectListViewController *controller = [[self alloc] initWithReferences:references];
|
||||
controller.title = [NSString stringWithFormat:@"%@ (%lu)", className, (unsigned long)instances.count];
|
||||
return controller;
|
||||
}
|
||||
|
||||
return nil;
|
||||
+ (instancetype)subclassesOfClassWithName:(NSString *)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 %@ (%lu)",
|
||||
className, (unsigned long)classes.count
|
||||
];
|
||||
|
||||
return controller;
|
||||
}
|
||||
|
||||
+ (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 = [self defaultSectionTitles];
|
||||
FLEXObjectListViewController *viewController = [[self alloc]
|
||||
initWithReferences:instances
|
||||
predicates:predicates
|
||||
sectionTitles:sectionTitles
|
||||
];
|
||||
viewController.title = [NSString stringWithFormat:@"Referencing %@ %p",
|
||||
NSStringFromClass(object_getClass(object)), object
|
||||
];
|
||||
return viewController;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Table View Delegate
|
||||
#pragma mark - Overrides
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
id instance = self.instances[indexPath.row].object;
|
||||
FLEXObjectExplorerViewController *drillInViewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:instance];
|
||||
[self.navigationController pushViewController:drillInViewController animated:YES];
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
self.showsSearchBar = YES;
|
||||
}
|
||||
|
||||
- (NSArray<FLEXMutableListSection *> *)makeSections {
|
||||
if (self.predicates.count) {
|
||||
return [self buildSections:self.sectionTitles predicates:self.predicates];
|
||||
} else {
|
||||
return @[[self makeSection:self.references title:nil]];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
- (NSArray *)buildSections:(NSArray<NSString *> *)titles predicates:(NSArray<NSPredicate *> *)predicates {
|
||||
NSParameterAssert(titles.count == predicates.count);
|
||||
NSParameterAssert(titles); NSParameterAssert(predicates);
|
||||
|
||||
return [NSArray flex_forEachUpTo:titles.count map:^id(NSUInteger i) {
|
||||
NSArray *rows = [self.references filteredArrayUsingPredicate:predicates[i]];
|
||||
return [self makeSection:rows title:titles[i]];
|
||||
}];
|
||||
}
|
||||
|
||||
- (FLEXMutableListSection *)makeSection:(NSArray *)rows title:(NSString *)title {
|
||||
FLEXMutableListSection *section = [FLEXMutableListSection list:rows
|
||||
cellConfiguration:^(FLEXTableViewCell *cell, FLEXObjectRef *ref, NSInteger row) {
|
||||
cell.textLabel.text = ref.reference;
|
||||
cell.detailTextLabel.text = ref.summary;
|
||||
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
|
||||
} filterMatcher:^BOOL(NSString *filterText, FLEXObjectRef *ref) {
|
||||
if (ref.summary && [ref.summary localizedCaseInsensitiveContainsString:filterText]) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
return [ref.reference localizedCaseInsensitiveContainsString:filterText];
|
||||
}
|
||||
];
|
||||
|
||||
section.selectionHandler = ^(__kindof UIViewController *host, FLEXObjectRef *ref) {
|
||||
[self.navigationController pushViewController:[
|
||||
FLEXObjectExplorerFactory explorerViewControllerForObject:ref.object
|
||||
] animated:YES];
|
||||
};
|
||||
|
||||
section.customTitle = title;
|
||||
return section;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -3,7 +3,7 @@
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 7/24/18.
|
||||
// Copyright (c) 2018 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
@@ -14,9 +14,14 @@
|
||||
+ (instancetype)referencing:(id)object ivar:(NSString *)ivarName;
|
||||
|
||||
+ (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;
|
||||
|
||||
/// For example, "NSString 0x1d4085d0" or "NSLayoutConstraint _object"
|
||||
@property (nonatomic, readonly) NSString *reference;
|
||||
/// For instances, this is the result of -[FLEXRuntimeUtility summaryForObject:]
|
||||
/// For classes, there is no summary.
|
||||
@property (nonatomic, readonly) NSString *summary;
|
||||
@property (nonatomic, readonly) id object;
|
||||
|
||||
@end
|
||||
|
||||
@@ -3,45 +3,74 @@
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 7/24/18.
|
||||
// Copyright (c) 2018 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXObjectRef.h"
|
||||
#import <objc/runtime.h>
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
#import "NSArray+Functional.h"
|
||||
|
||||
@interface FLEXObjectRef ()
|
||||
@property (nonatomic, readonly) BOOL wantsSummary;
|
||||
@end
|
||||
|
||||
@implementation FLEXObjectRef
|
||||
@synthesize summary = _summary;
|
||||
|
||||
+ (instancetype)referencing:(id)object {
|
||||
return [[self alloc] initWithObject:object ivarName:nil];
|
||||
return [self referencing:object showSummary:YES];
|
||||
}
|
||||
|
||||
+ (instancetype)referencing:(id)object showSummary:(BOOL)showSummary {
|
||||
return [[self alloc] initWithObject:object ivarName:nil showSummary:showSummary];
|
||||
}
|
||||
|
||||
+ (instancetype)referencing:(id)object ivar:(NSString *)ivarName {
|
||||
return [[self alloc] initWithObject:object ivarName:ivarName];
|
||||
return [[self alloc] initWithObject:object ivarName:ivarName showSummary:YES];
|
||||
}
|
||||
|
||||
+ (NSArray<FLEXObjectRef *> *)referencingAll:(NSArray *)objects {
|
||||
NSMutableArray<FLEXObjectRef *> *refs = [NSMutableArray array];
|
||||
for (id obj in objects) {
|
||||
[refs addObject:[self referencing:obj]];
|
||||
}
|
||||
|
||||
return refs;
|
||||
return [objects flex_mapped:^id(id obj, NSUInteger idx) {
|
||||
return [self referencing:obj showSummary:YES];
|
||||
}];
|
||||
}
|
||||
|
||||
- (id)initWithObject:(id)object ivarName:(NSString *)ivar {
|
||||
+ (NSArray<FLEXObjectRef *> *)referencingClasses:(NSArray<Class> *)classes {
|
||||
return [classes flex_mapped:^id(id obj, NSUInteger idx) {
|
||||
return [self referencing:obj showSummary:NO];
|
||||
}];
|
||||
}
|
||||
|
||||
- (id)initWithObject:(id)object ivarName:(NSString *)ivar showSummary:(BOOL)showSummary {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_object = object;
|
||||
_wantsSummary = showSummary;
|
||||
|
||||
NSString *class = NSStringFromClass(object_getClass(object));
|
||||
if (ivar) {
|
||||
_reference = [NSString stringWithFormat:@"%@ %@", class, ivar];
|
||||
} else {
|
||||
} else if (showSummary) {
|
||||
_reference = [NSString stringWithFormat:@"%@ %p", class, object];
|
||||
} else {
|
||||
_reference = class;
|
||||
}
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSString *)summary {
|
||||
if (self.wantsSummary) {
|
||||
if (!_summary) {
|
||||
_summary = [FLEXRuntimeUtility summaryForObject:self.object];
|
||||
}
|
||||
|
||||
return _summary;
|
||||
}
|
||||
else {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/10/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/10/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
// Copyright (c) 2020 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXWebViewController.h"
|
||||
|
||||
@@ -60,10 +60,10 @@
|
||||
|
||||
- (void)main {
|
||||
NSFileManager *fileManager = NSFileManager.defaultManager;
|
||||
NSMutableArray<NSString *> *searchPaths = [NSMutableArray array];
|
||||
NSMutableDictionary<NSString *, NSNumber *> *sizeMapping = [NSMutableDictionary dictionary];
|
||||
NSMutableArray<NSString *> *searchPaths = [NSMutableArray new];
|
||||
NSMutableDictionary<NSString *, NSNumber *> *sizeMapping = [NSMutableDictionary new];
|
||||
uint64_t totalSize = 0;
|
||||
NSMutableArray<NSString *> *stack = [NSMutableArray array];
|
||||
NSMutableArray<NSString *> *stack = [NSMutableArray new];
|
||||
[stack flex_push:self.path];
|
||||
|
||||
//recursive found all match searchString paths, and precomputing there size
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#import "FLEXTableListViewController.h"
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
#import "FLEXObjectExplorerViewController.h"
|
||||
#import <mach-o/loader.h>
|
||||
|
||||
@interface FLEXFileBrowserTableViewCell : UITableViewCell
|
||||
@end
|
||||
@@ -234,16 +235,38 @@
|
||||
} @catch (NSException *e) { }
|
||||
|
||||
// Try to decode other things instead
|
||||
object = object
|
||||
?: [NSPropertyListSerialization propertyListWithData:fileData
|
||||
options:0
|
||||
format:NULL
|
||||
error:NULL]
|
||||
?: [NSDictionary dictionaryWithContentsOfFile:fullPath]
|
||||
?: [NSArray arrayWithContentsOfFile:fullPath];
|
||||
object = object ?: [NSPropertyListSerialization
|
||||
propertyListWithData:fileData
|
||||
options:0
|
||||
format:NULL
|
||||
error:NULL
|
||||
] ?: [NSDictionary dictionaryWithContentsOfFile:fullPath]
|
||||
?: [NSArray arrayWithContentsOfFile:fullPath];
|
||||
|
||||
if (object) {
|
||||
drillInViewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:object];
|
||||
} else {
|
||||
// Is it possibly a mach-O file?
|
||||
if (fileData.length > sizeof(struct mach_header_64)) {
|
||||
struct mach_header_64 header;
|
||||
[fileData getBytes:&header length:sizeof(struct mach_header_64)];
|
||||
|
||||
// Does it have the mach header magic number?
|
||||
if (header.magic == MH_MAGIC_64) {
|
||||
// See if we can get some classes out of it...
|
||||
unsigned int count = 0;
|
||||
const char **classList = objc_copyClassNamesForImage(
|
||||
fullPath.UTF8String, &count
|
||||
);
|
||||
|
||||
if (count > 0) {
|
||||
NSArray<NSString *> *classNames = [NSArray flex_forEachUpTo:count map:^id(NSUInteger i) {
|
||||
return objc_getClass(classList[i]);
|
||||
}];
|
||||
drillInViewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:classNames];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -424,7 +447,7 @@
|
||||
}
|
||||
|
||||
- (void)reloadCurrentPath {
|
||||
NSMutableArray<NSString *> *childPaths = [NSMutableArray array];
|
||||
NSMutableArray<NSString *> *childPaths = [NSMutableArray new];
|
||||
NSArray<NSString *> *subpaths = [NSFileManager.defaultManager contentsOfDirectoryAtPath:self.path error:NULL];
|
||||
for (NSString *subpath in subpaths) {
|
||||
[childPaths addObject:[self.path stringByAppendingPathComponent:subpath]];
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user