mirror of
https://github.com/SoPat712/YTLitePlus.git
synced 2025-10-29 12:00:47 -04:00
added files via upload
This commit is contained in:
1
Tweaks/FLEX/.gitignore
vendored
Normal file
1
Tweaks/FLEX/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.DS_Store
|
||||
@@ -0,0 +1,89 @@
|
||||
//
|
||||
// FLEXFilteringTableViewController.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 3/9/20.
|
||||
// Copyright © 2020 FLEX Team. 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, copy) 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, copy) 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
|
||||
209
Tweaks/FLEX/Core/Controllers/FLEXFilteringTableViewController.m
Normal file
209
Tweaks/FLEX/Core/Controllers/FLEXFilteringTableViewController.m
Normal file
@@ -0,0 +1,209 @@
|
||||
//
|
||||
// FLEXFilteringTableViewController.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 3/9/20.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXFilteringTableViewController.h"
|
||||
#import "FLEXTableViewSection.h"
|
||||
#import "NSArray+FLEX.h"
|
||||
#import "FLEXMacros.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)(void) = ^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;
|
||||
// Only display nonempty sections
|
||||
self.sections = self.nonemptySections;
|
||||
}
|
||||
|
||||
- (void)setSections:(NSArray<FLEXTableViewSection *> *)sections {
|
||||
// Allow sections to reload a portion of the table view at will
|
||||
[sections enumerateObjectsUsingBlock:^(FLEXTableViewSection *s, NSUInteger idx, BOOL *stop) {
|
||||
[s setTable:self.tableView section:idx];
|
||||
}];
|
||||
_sections = sections.copy;
|
||||
}
|
||||
|
||||
|
||||
#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);
|
||||
}
|
||||
|
||||
- (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;
|
||||
}
|
||||
|
||||
@end
|
||||
19
Tweaks/FLEX/Core/Controllers/FLEXNavigationController.h
Normal file
19
Tweaks/FLEX/Core/Controllers/FLEXNavigationController.h
Normal file
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// FLEXNavigationController.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 1/30/20.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FLEXNavigationController : UINavigationController
|
||||
|
||||
+ (instancetype)withRootViewController:(UIViewController *)rootVC;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
196
Tweaks/FLEX/Core/Controllers/FLEXNavigationController.m
Normal file
196
Tweaks/FLEX/Core/Controllers/FLEXNavigationController.m
Normal file
@@ -0,0 +1,196 @@
|
||||
//
|
||||
// FLEXNavigationController.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 1/30/20.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXNavigationController.h"
|
||||
#import "FLEXExplorerViewController.h"
|
||||
#import "FLEXTabList.h"
|
||||
|
||||
@interface UINavigationController (Private) <UIGestureRecognizerDelegate>
|
||||
- (void)_gestureRecognizedInteractiveHide:(UIGestureRecognizer *)sender;
|
||||
@end
|
||||
@interface UIPanGestureRecognizer (Private)
|
||||
- (void)_setDelegate:(id)delegate;
|
||||
@end
|
||||
|
||||
@interface FLEXNavigationController ()
|
||||
@property (nonatomic, readonly) BOOL toolbarWasHidden;
|
||||
@property (nonatomic) BOOL waitingToAddTab;
|
||||
@property (nonatomic, readonly) BOOL canShowToolbar;
|
||||
@property (nonatomic) BOOL didSetupPendingDismissButtons;
|
||||
@property (nonatomic) UISwipeGestureRecognizer *navigationBarSwipeGesture;
|
||||
@end
|
||||
|
||||
@implementation FLEXNavigationController
|
||||
|
||||
+ (instancetype)withRootViewController:(UIViewController *)rootVC {
|
||||
return [[self alloc] initWithRootViewController:rootVC];
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
self.waitingToAddTab = YES;
|
||||
|
||||
// Add gesture to reveal toolbar if hidden
|
||||
UITapGestureRecognizer *navbarTapGesture = [[UITapGestureRecognizer alloc]
|
||||
initWithTarget:self action:@selector(handleNavigationBarTap:)
|
||||
];
|
||||
|
||||
// Don't cancel touches to work around bug on versions of iOS prior to 13
|
||||
navbarTapGesture.cancelsTouchesInView = NO;
|
||||
[self.navigationBar addGestureRecognizer:navbarTapGesture];
|
||||
|
||||
// Add gesture to dismiss if not presented with a sheet style
|
||||
if (@available(iOS 13, *)) {
|
||||
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
|
||||
if ([self.presentingViewController isKindOfClass:[FLEXExplorerViewController class]]) {
|
||||
[FLEXTabList.sharedList closeTab:self];
|
||||
}
|
||||
|
||||
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
|
||||
}
|
||||
|
||||
- (BOOL)canShowToolbar {
|
||||
return self.topViewController.toolbarItems.count > 0;
|
||||
}
|
||||
|
||||
- (void)addNavigationBarItemsToViewController:(UINavigationItem *)navigationItem {
|
||||
if (!self.presentingViewController) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// Don't reveal the toolbar if we were just tapping a button
|
||||
CGPoint location = [sender locationInView:self.navigationBar];
|
||||
UIView *hitView = [self.navigationBar hitTest:location withEvent:nil];
|
||||
if ([hitView isKindOfClass:[UIControl class]]) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sender.state == UIGestureRecognizerStateRecognized) {
|
||||
if (self.toolbarHidden && self.canShowToolbar) {
|
||||
[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 {
|
||||
if (sender.state == UIGestureRecognizerStateRecognized) {
|
||||
BOOL show = self.canShowToolbar;
|
||||
CGFloat yTranslation = [sender translationInView:self.view].y;
|
||||
CGFloat yVelocity = [sender velocityInView:self.view].y;
|
||||
if (yVelocity > 2000) {
|
||||
[self setToolbarHidden:YES animated:YES];
|
||||
} else if (show && yTranslation > 20 && yVelocity > 250) {
|
||||
[self setToolbarHidden:NO animated:YES];
|
||||
} else if (yTranslation < -20) {
|
||||
[self setToolbarHidden:YES animated:YES];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
153
Tweaks/FLEX/Core/Controllers/FLEXTableViewController.h
Normal file
153
Tweaks/FLEX/Core/Controllers/FLEXTableViewController.h
Normal file
@@ -0,0 +1,153 @@
|
||||
//
|
||||
// FLEXTableViewController.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 7/5/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "FLEXTableView.h"
|
||||
@class FLEXScopeCarousel, FLEXWindow, FLEXTableViewSection;
|
||||
|
||||
typedef CGFloat FLEXDebounceInterval;
|
||||
/// No delay, all events delivered
|
||||
extern CGFloat const kFLEXDebounceInstant;
|
||||
/// Small delay which makes UI seem smoother by avoiding rapid events
|
||||
extern CGFloat const kFLEXDebounceFast;
|
||||
/// Slower than Fast, faster than ExpensiveIO
|
||||
extern CGFloat const kFLEXDebounceForAsyncSearch;
|
||||
/// The least frequent, at just over once per second; for I/O or other expensive operations
|
||||
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
|
||||
>
|
||||
|
||||
/// A grouped table view. Inset on iOS 13.
|
||||
///
|
||||
/// 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.
|
||||
@property (nonatomic) BOOL showsCarousel;
|
||||
/// A horizontally scrolling list with functionality similar to
|
||||
/// that of a search bar's scope bar. You'd want to use this when
|
||||
/// you have potentially more than 4 scope options.
|
||||
@property (nonatomic) FLEXScopeCarousel *carousel;
|
||||
|
||||
/// Defaults to NO.
|
||||
///
|
||||
/// Setting this to YES will initialize searchController and the view.
|
||||
@property (nonatomic) BOOL showsSearchBar;
|
||||
/// Defaults to NO.
|
||||
///
|
||||
/// Setting this to YES will make the search bar appear whenever the view appears.
|
||||
/// Otherwise, iOS will only show the search bar when you scroll up.
|
||||
@property (nonatomic) BOOL showSearchBarInitially;
|
||||
/// Defaults to NO.
|
||||
///
|
||||
/// Setting this to YES will make the search bar activate whenever the view appears.
|
||||
@property (nonatomic) BOOL activatesSearchBarAutomatically;
|
||||
|
||||
/// nil unless showsSearchBar is set to YES.
|
||||
///
|
||||
/// self is used as the default search results updater and delegate.
|
||||
/// The search bar will not dim the background or hide the navigation bar by default.
|
||||
/// On iOS 11 and up, the search bar will appear in the navigation bar below the title.
|
||||
@property (nonatomic) UISearchController *searchController;
|
||||
/// Used to initialize the search controller. Defaults to nil.
|
||||
@property (nonatomic) UIViewController *searchResultsController;
|
||||
/// Defaults to "Fast"
|
||||
///
|
||||
/// Determines how often search bar results will be "debounced."
|
||||
/// Empty query events are always sent instantly. Query events will
|
||||
/// be sent when the user has not changed the query for this interval.
|
||||
@property (nonatomic) FLEXDebounceInterval searchBarDebounceInterval;
|
||||
/// Whether the search bar stays at the top of the view while scrolling.
|
||||
///
|
||||
/// Calls into self.navigationItem.hidesSearchBarWhenScrolling.
|
||||
/// Do not change self.navigationItem.hidesSearchBarWhenScrolling directly,
|
||||
/// or it will not be respsected. Use this instead.
|
||||
/// Defaults to NO.
|
||||
@property (nonatomic) BOOL pinSearchBar;
|
||||
/// By default, we will show the search bar's cancel button when
|
||||
/// search becomes active and hide it when search is dismissed.
|
||||
///
|
||||
/// Do not set the showsCancelButton property on the searchController's
|
||||
/// searchBar manually. Set this property after turning on showsSearchBar.
|
||||
///
|
||||
/// Does nothing pre-iOS 13, safe to call on any version.
|
||||
@property (nonatomic) BOOL automaticallyShowsSearchBarCancelButton;
|
||||
|
||||
/// If using the scope bar, self.searchController.searchBar.selectedScopeButtonIndex.
|
||||
/// Otherwise, this is the selected index of the carousel, or NSNotFound if using neither.
|
||||
@property (nonatomic) NSInteger selectedScope;
|
||||
/// self.searchController.searchBar.text
|
||||
@property (nonatomic, readonly, copy) NSString *searchText;
|
||||
|
||||
/// A totally optional delegate to forward search results updater calls to.
|
||||
/// If a delegate is set, updateSearchResults: is not called on this view controller.
|
||||
@property (nonatomic, weak) id<FLEXSearchResultsUpdating> searchResultsUpdater;
|
||||
|
||||
/// self.view.window as a \c FLEXWindow
|
||||
@property (nonatomic, readonly) FLEXWindow *window;
|
||||
|
||||
/// 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:(UIBarButtonItem *)sender;
|
||||
|
||||
/// 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
|
||||
618
Tweaks/FLEX/Core/Controllers/FLEXTableViewController.m
Normal file
618
Tweaks/FLEX/Core/Controllers/FLEXTableViewController.m
Normal file
@@ -0,0 +1,618 @@
|
||||
//
|
||||
// FLEXTableViewController.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 7/5/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#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
|
||||
- (void)invoke;
|
||||
@end
|
||||
|
||||
CGFloat const kFLEXDebounceInstant = 0.f;
|
||||
CGFloat const kFLEXDebounceFast = 0.05;
|
||||
CGFloat const kFLEXDebounceForAsyncSearch = 0.15;
|
||||
CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
|
||||
|
||||
@interface FLEXTableViewController ()
|
||||
@property (nonatomic) NSTimer *debounceTimer;
|
||||
@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 - Initialization
|
||||
|
||||
- (id)init {
|
||||
if (@available(iOS 13.0, *)) {
|
||||
self = [self initWithStyle:UITableViewStyleInsetGrouped];
|
||||
} else {
|
||||
self = [self initWithStyle:UITableViewStyleGrouped];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (id)initWithStyle:(UITableViewStyle)style {
|
||||
self = [super initWithStyle:style];
|
||||
|
||||
if (self) {
|
||||
_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;
|
||||
}
|
||||
|
||||
- (void)setShowsSearchBar:(BOOL)showsSearchBar {
|
||||
if (_showsSearchBar == showsSearchBar) return;
|
||||
_showsSearchBar = showsSearchBar;
|
||||
|
||||
if (showsSearchBar) {
|
||||
UIViewController *results = self.searchResultsController;
|
||||
self.searchController = [[UISearchController alloc] initWithSearchResultsController:results];
|
||||
self.searchController.searchBar.placeholder = @"Filter";
|
||||
self.searchController.searchResultsUpdater = (id)self;
|
||||
self.searchController.delegate = (id)self;
|
||||
self.searchController.dimsBackgroundDuringPresentation = NO;
|
||||
self.searchController.hidesNavigationBarDuringPresentation = NO;
|
||||
/// Not necessary in iOS 13; remove this when iOS 13 is the minimum deployment target
|
||||
self.searchController.searchBar.delegate = self;
|
||||
|
||||
self.automaticallyShowsSearchBarCancelButton = YES;
|
||||
|
||||
if (@available(iOS 13, *)) {
|
||||
self.searchController.automaticallyShowsScopeBar = NO;
|
||||
}
|
||||
|
||||
[self addSearchController:self.searchController];
|
||||
} else {
|
||||
// Search already shown and just set to NO, so remove it
|
||||
[self removeSearchController:self.searchController];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setShowsCarousel:(BOOL)showsCarousel {
|
||||
if (_showsCarousel == showsCarousel) return;
|
||||
_showsCarousel = showsCarousel;
|
||||
|
||||
if (showsCarousel) {
|
||||
_carousel = ({ weakify(self)
|
||||
|
||||
FLEXScopeCarousel *carousel = [FLEXScopeCarousel new];
|
||||
carousel.selectedIndexChangedAction = ^(NSInteger idx) { strongify(self);
|
||||
[self.searchDelegate updateSearchResults:self.searchText];
|
||||
};
|
||||
|
||||
// UITableView won't update the header size unless you reset the header view
|
||||
[carousel registerBlockForDynamicTypeChanges:^(FLEXScopeCarousel *_) { strongify(self);
|
||||
[self layoutTableHeaderIfNeeded];
|
||||
}];
|
||||
|
||||
carousel;
|
||||
});
|
||||
[self addCarousel:_carousel];
|
||||
} else {
|
||||
// Carousel already shown and just set to NO, so remove it
|
||||
[self removeCarousel:_carousel];
|
||||
}
|
||||
}
|
||||
|
||||
- (NSInteger)selectedScope {
|
||||
if (self.searchController.searchBar.showsScopeBar) {
|
||||
return self.searchController.searchBar.selectedScopeButtonIndex;
|
||||
} else if (self.showsCarousel) {
|
||||
return self.carousel.selectedIndex;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setSelectedScope:(NSInteger)selectedScope {
|
||||
if (self.searchController.searchBar.showsScopeBar) {
|
||||
self.searchController.searchBar.selectedScopeButtonIndex = selectedScope;
|
||||
} else if (self.showsCarousel) {
|
||||
self.carousel.selectedIndex = selectedScope;
|
||||
}
|
||||
|
||||
[self.searchDelegate updateSearchResults:self.searchText];
|
||||
}
|
||||
|
||||
- (NSString *)searchText {
|
||||
return self.searchController.searchBar.text;
|
||||
}
|
||||
|
||||
- (BOOL)automaticallyShowsSearchBarCancelButton {
|
||||
if (@available(iOS 13, *)) {
|
||||
return self.searchController.automaticallyShowsCancelButton;
|
||||
}
|
||||
|
||||
return _automaticallyShowsSearchBarCancelButton;
|
||||
}
|
||||
|
||||
- (void)setAutomaticallyShowsSearchBarCancelButton:(BOOL)value {
|
||||
if (@available(iOS 13, *)) {
|
||||
self.searchController.automaticallyShowsCancelButton = value;
|
||||
}
|
||||
|
||||
_automaticallyShowsSearchBarCancelButton = value;
|
||||
}
|
||||
|
||||
- (void)onBackgroundQueue:(NSArray *(^)(void))backgroundBlock thenOnMainQueue:(void(^)(NSArray *))mainBlock {
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
NSArray *items = backgroundBlock();
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
mainBlock(items);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
- (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
|
||||
flex_itemWithImage:FLEXResources.bookmarksIcon target:self action:@selector(showBookmarks)
|
||||
];
|
||||
_openTabsToolbarItem = [UIBarButtonItem
|
||||
flex_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 = self.toolbarItems.count > 0;
|
||||
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
|
||||
// toggle navigationItem.hidesSearchBarWhenScrolling on and off. The flash
|
||||
// will still happen on subsequent view controllers, but we can at least
|
||||
// avoid it for the root view controller
|
||||
if (@available(iOS 13, *)) {
|
||||
if (self.navigationController.viewControllers.firstObject == self) {
|
||||
_showSearchBarInitially = NO;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated {
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
if (@available(iOS 11.0, *)) {
|
||||
// When going back, make the search bar reappear instead of hiding
|
||||
if ((self.pinSearchBar || self.showSearchBarInitially) && !self.didInitiallyRevealSearchBar) {
|
||||
self.navigationItem.hidesSearchBarWhenScrolling = NO;
|
||||
}
|
||||
}
|
||||
|
||||
// Make the keyboard seem to appear faster
|
||||
if (self.activatesSearchBarAutomatically) {
|
||||
[self makeKeyboardAppearNow];
|
||||
}
|
||||
|
||||
[self setupToolbarItems];
|
||||
}
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated {
|
||||
[super viewDidAppear:animated];
|
||||
|
||||
// Allow scrolling to collapse the search bar, only if we don't want it pinned
|
||||
if (@available(iOS 11.0, *)) {
|
||||
if (self.showSearchBarInitially && !self.pinSearchBar && !self.didInitiallyRevealSearchBar) {
|
||||
// All this mumbo jumbo is necessary to work around a bug in iOS 13 up to 13.2
|
||||
// wherein quickly toggling navigationItem.hidesSearchBarWhenScrolling to make
|
||||
// the search bar appear initially results in a bugged search bar that
|
||||
// becomes transparent and floats over the screen as you scroll
|
||||
[UIView animateWithDuration:0.2 animations:^{
|
||||
self.navigationItem.hidesSearchBarWhenScrolling = YES;
|
||||
[self.navigationController.view setNeedsLayout];
|
||||
[self.navigationController.view layoutIfNeeded];
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
if (self.activatesSearchBarAutomatically) {
|
||||
// Keyboard has appeared, now we call this as we soon present our search bar
|
||||
[self removeDummyTextField];
|
||||
|
||||
// Activate the search bar
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
// This doesn't work unless it's wrapped in this dispatch_async call
|
||||
[self.searchController.searchBar becomeFirstResponder];
|
||||
});
|
||||
}
|
||||
|
||||
// We only want to reveal the search bar when the view controller first appears.
|
||||
self.didInitiallyRevealSearchBar = YES;
|
||||
}
|
||||
|
||||
- (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:(UIBarButtonItem *)sender {
|
||||
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
- (void)debounce:(void(^)(void))block {
|
||||
[self.debounceTimer invalidate];
|
||||
|
||||
self.debounceTimer = [NSTimer
|
||||
scheduledTimerWithTimeInterval:self.searchBarDebounceInterval
|
||||
target:block
|
||||
selector:@selector(invoke)
|
||||
userInfo:nil
|
||||
repeats:NO
|
||||
];
|
||||
}
|
||||
|
||||
- (void)layoutTableHeaderIfNeeded {
|
||||
if (self.showsCarousel) {
|
||||
self.carousel.frame = FLEXRectSetHeight(
|
||||
self.carousel.frame, self.carousel.intrinsicContentSize.height
|
||||
);
|
||||
}
|
||||
|
||||
self.tableView.tableHeaderView = self.tableView.tableHeaderView;
|
||||
}
|
||||
|
||||
- (void)addCarousel:(FLEXScopeCarousel *)carousel {
|
||||
if (@available(iOS 11.0, *)) {
|
||||
self.tableView.tableHeaderView = carousel;
|
||||
} else {
|
||||
carousel.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin;
|
||||
|
||||
CGRect frame = self.tableHeaderViewContainer.frame;
|
||||
CGRect subviewFrame = carousel.frame;
|
||||
subviewFrame.origin.y = 0;
|
||||
|
||||
// Put the carousel below the search bar if it's already there
|
||||
if (self.showsSearchBar) {
|
||||
carousel.frame = subviewFrame = FLEXRectSetY(
|
||||
subviewFrame, self.searchController.searchBar.frame.size.height
|
||||
);
|
||||
frame.size.height += carousel.intrinsicContentSize.height;
|
||||
} else {
|
||||
frame.size.height = carousel.intrinsicContentSize.height;
|
||||
}
|
||||
|
||||
self.tableHeaderViewContainer.frame = frame;
|
||||
[self.tableHeaderViewContainer addSubview:carousel];
|
||||
}
|
||||
|
||||
[self layoutTableHeaderIfNeeded];
|
||||
}
|
||||
|
||||
- (void)removeCarousel:(FLEXScopeCarousel *)carousel {
|
||||
[carousel removeFromSuperview];
|
||||
|
||||
if (@available(iOS 11.0, *)) {
|
||||
self.tableView.tableHeaderView = nil;
|
||||
} else {
|
||||
if (self.showsSearchBar) {
|
||||
[self removeSearchController:self.searchController];
|
||||
[self addSearchController:self.searchController];
|
||||
} else {
|
||||
self.tableView.tableHeaderView = nil;
|
||||
_tableHeaderViewContainer = nil;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)addSearchController:(UISearchController *)controller {
|
||||
if (@available(iOS 11.0, *)) {
|
||||
self.navigationItem.searchController = controller;
|
||||
} else {
|
||||
controller.searchBar.autoresizingMask |= UIViewAutoresizingFlexibleBottomMargin;
|
||||
[self.tableHeaderViewContainer addSubview:controller.searchBar];
|
||||
CGRect subviewFrame = controller.searchBar.frame;
|
||||
CGRect frame = self.tableHeaderViewContainer.frame;
|
||||
frame.size.width = MAX(frame.size.width, subviewFrame.size.width);
|
||||
frame.size.height = subviewFrame.size.height;
|
||||
|
||||
// Move the carousel down if it's already there
|
||||
if (self.showsCarousel) {
|
||||
self.carousel.frame = FLEXRectSetY(
|
||||
self.carousel.frame, subviewFrame.size.height
|
||||
);
|
||||
frame.size.height += self.carousel.frame.size.height;
|
||||
}
|
||||
|
||||
self.tableHeaderViewContainer.frame = frame;
|
||||
[self layoutTableHeaderIfNeeded];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)removeSearchController:(UISearchController *)controller {
|
||||
if (@available(iOS 11.0, *)) {
|
||||
self.navigationItem.searchController = nil;
|
||||
} else {
|
||||
[controller.searchBar removeFromSuperview];
|
||||
|
||||
if (self.showsCarousel) {
|
||||
// self.carousel.frame = FLEXRectRemake(CGPointZero, self.carousel.frame.size);
|
||||
[self removeCarousel:self.carousel];
|
||||
[self addCarousel:self.carousel];
|
||||
} else {
|
||||
self.tableView.tableHeaderView = nil;
|
||||
_tableHeaderViewContainer = nil;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (UIView *)tableHeaderViewContainer {
|
||||
if (!_tableHeaderViewContainer) {
|
||||
_tableHeaderViewContainer = [UIView new];
|
||||
self.tableView.tableHeaderView = self.tableHeaderViewContainer;
|
||||
}
|
||||
|
||||
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 Faster keyboard
|
||||
|
||||
static UITextField *kDummyTextField = nil;
|
||||
|
||||
/// Make the keyboard appear instantly. We use this to make the
|
||||
/// keyboard appear faster when the search bar is set to appear initially.
|
||||
/// You must call \c -removeDummyTextField before your search bar is to appear.
|
||||
- (void)makeKeyboardAppearNow {
|
||||
if (!kDummyTextField) {
|
||||
kDummyTextField = [UITextField new];
|
||||
kDummyTextField.autocorrectionType = UITextAutocorrectionTypeNo;
|
||||
}
|
||||
|
||||
kDummyTextField.inputAccessoryView = self.searchController.searchBar.inputAccessoryView;
|
||||
[UIApplication.sharedApplication.keyWindow addSubview:kDummyTextField];
|
||||
[kDummyTextField becomeFirstResponder];
|
||||
}
|
||||
|
||||
- (void)removeDummyTextField {
|
||||
if (kDummyTextField.superview) {
|
||||
[kDummyTextField removeFromSuperview];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark UISearchResultsUpdating
|
||||
|
||||
- (void)updateSearchResultsForSearchController:(UISearchController *)searchController {
|
||||
[self.debounceTimer invalidate];
|
||||
NSString *text = searchController.searchBar.text;
|
||||
|
||||
void (^updateSearchResults)(void) = ^{
|
||||
if (self.searchResultsUpdater) {
|
||||
[self.searchResultsUpdater updateSearchResults:text];
|
||||
} else {
|
||||
[self.searchDelegate updateSearchResults:text];
|
||||
}
|
||||
};
|
||||
|
||||
// Only debounce if we want to, and if we have a non-empty string
|
||||
// Empty string events are sent instantly
|
||||
if (text.length && self.searchBarDebounceInterval > kFLEXDebounceInstant) {
|
||||
[self debounce:updateSearchResults];
|
||||
} else {
|
||||
updateSearchResults();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark UISearchControllerDelegate
|
||||
|
||||
- (void)willPresentSearchController:(UISearchController *)searchController {
|
||||
// Manually show cancel button for < iOS 13
|
||||
if (!@available(iOS 13, *) && self.automaticallyShowsSearchBarCancelButton) {
|
||||
[searchController.searchBar setShowsCancelButton:YES animated:YES];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)willDismissSearchController:(UISearchController *)searchController {
|
||||
// Manually hide cancel button for < iOS 13
|
||||
if (!@available(iOS 13, *) && self.automaticallyShowsSearchBarCancelButton) {
|
||||
[searchController.searchBar setShowsCancelButton:NO animated:YES];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark UISearchBarDelegate
|
||||
|
||||
/// Not necessary in iOS 13; remove this when iOS 13 is the deployment target
|
||||
- (void)searchBar:(UISearchBar *)searchBar selectedScopeButtonIndexDidChange:(NSInteger)selectedScope {
|
||||
[self updateSearchResultsForSearchController:self.searchController];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Table View
|
||||
|
||||
/// Not having a title in the first section looks weird with a rounded-corner table view style
|
||||
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
|
||||
if (@available(iOS 13, *)) {
|
||||
if (self.style == UITableViewStyleInsetGrouped) {
|
||||
return @" ";
|
||||
}
|
||||
}
|
||||
|
||||
return nil; // For plain/gropued style
|
||||
}
|
||||
|
||||
@end
|
||||
28
Tweaks/FLEX/Core/FLEXSingleRowSection.h
Normal file
28
Tweaks/FLEX/Core/FLEXSingleRowSection.h
Normal file
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// FLEXSingleRowSection.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 9/25/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXTableViewSection.h"
|
||||
|
||||
/// A section providing a specific single row.
|
||||
///
|
||||
/// You may optionally provide a view controller to push when the row
|
||||
/// is selected, or an action to perform when it is selected.
|
||||
/// Which one is used first is up to the table view data source.
|
||||
@interface FLEXSingleRowSection : FLEXTableViewSection
|
||||
|
||||
/// @param reuseIdentifier if nil, kFLEXDefaultCell is used.
|
||||
+ (instancetype)title:(NSString *)sectionTitle
|
||||
reuse:(NSString *)reuseIdentifier
|
||||
cell:(void(^)(__kindof UITableViewCell *cell))cellConfiguration;
|
||||
|
||||
@property (nonatomic) UIViewController *pushOnSelection;
|
||||
@property (nonatomic) void (^selectionAction)(UIViewController *host);
|
||||
/// Called to determine whether the single row should display itself or not.
|
||||
@property (nonatomic) BOOL (^filterMatcher)(NSString *filterText);
|
||||
|
||||
@end
|
||||
87
Tweaks/FLEX/Core/FLEXSingleRowSection.m
Normal file
87
Tweaks/FLEX/Core/FLEXSingleRowSection.m
Normal file
@@ -0,0 +1,87 @@
|
||||
//
|
||||
// FLEXSingleRowSection.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 9/25/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXSingleRowSection.h"
|
||||
#import "FLEXTableView.h"
|
||||
|
||||
@interface FLEXSingleRowSection ()
|
||||
@property (nonatomic, readonly) NSString *reuseIdentifier;
|
||||
@property (nonatomic, readonly) void (^cellConfiguration)(__kindof UITableViewCell *cell);
|
||||
|
||||
@property (nonatomic) NSString *lastTitle;
|
||||
@property (nonatomic) NSString *lastSubitle;
|
||||
@end
|
||||
|
||||
@implementation FLEXSingleRowSection
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
+ (instancetype)title:(NSString *)title
|
||||
reuse:(NSString *)reuse
|
||||
cell:(void (^)(__kindof UITableViewCell *))config {
|
||||
return [[self alloc] initWithTitle:title reuse:reuse cell:config];
|
||||
}
|
||||
|
||||
- (id)initWithTitle:(NSString *)sectionTitle
|
||||
reuse:(NSString *)reuseIdentifier
|
||||
cell:(void (^)(__kindof UITableViewCell *))cellConfiguration {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_title = sectionTitle;
|
||||
_reuseIdentifier = reuseIdentifier ?: kFLEXDefaultCell;
|
||||
_cellConfiguration = cellConfiguration;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - Overrides
|
||||
|
||||
- (NSInteger)numberOfRows {
|
||||
if (self.filterMatcher && self.filterText.length) {
|
||||
return self.filterMatcher(self.filterText) ? 1 : 0;
|
||||
}
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
- (BOOL)canSelectRow:(NSInteger)row {
|
||||
return self.pushOnSelection || self.selectionAction;
|
||||
}
|
||||
|
||||
- (void (^)(__kindof UIViewController *))didSelectRowAction:(NSInteger)row {
|
||||
return self.selectionAction;
|
||||
}
|
||||
|
||||
- (UIViewController *)viewControllerToPushForRow:(NSInteger)row {
|
||||
return self.pushOnSelection;
|
||||
}
|
||||
|
||||
- (NSString *)reuseIdentifierForRow:(NSInteger)row {
|
||||
return self.reuseIdentifier;
|
||||
}
|
||||
|
||||
- (void)configureCell:(__kindof UITableViewCell *)cell forRow:(NSInteger)row {
|
||||
cell.textLabel.text = nil;
|
||||
cell.detailTextLabel.text = nil;
|
||||
cell.accessoryType = UITableViewCellAccessoryNone;
|
||||
|
||||
self.cellConfiguration(cell);
|
||||
self.lastTitle = cell.textLabel.text;
|
||||
self.lastSubitle = cell.detailTextLabel.text;
|
||||
}
|
||||
|
||||
- (NSString *)titleForRow:(NSInteger)row {
|
||||
return self.lastTitle;
|
||||
}
|
||||
|
||||
- (NSString *)subtitleForRow:(NSInteger)row {
|
||||
return self.lastSubitle;
|
||||
}
|
||||
|
||||
@end
|
||||
146
Tweaks/FLEX/Core/FLEXTableViewSection.h
Normal file
146
Tweaks/FLEX/Core/FLEXTableViewSection.h
Normal file
@@ -0,0 +1,146 @@
|
||||
//
|
||||
// FLEXTableViewSection.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 1/29/20.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "NSArray+FLEX.h"
|
||||
@class FLEXTableView;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
#pragma mark FLEXTableViewSection
|
||||
|
||||
/// An abstract base class for table view sections.
|
||||
///
|
||||
/// Many properties or methods here return nil or some logical equivalent by default.
|
||||
/// Even so, most of the methods with defaults are intended to be overriden by subclasses.
|
||||
/// Some methods are not implemented at all and MUST be implemented by a subclass.
|
||||
@interface FLEXTableViewSection : NSObject {
|
||||
@protected
|
||||
/// Unused by default, use if you want
|
||||
NSString *_title;
|
||||
|
||||
@private
|
||||
__weak UITableView *_tableView;
|
||||
NSInteger _sectionIndex;
|
||||
}
|
||||
|
||||
#pragma mark - Data
|
||||
|
||||
/// A title to be displayed for the custom section.
|
||||
/// Subclasses may override or use the \c _title ivar.
|
||||
@property (nonatomic, readonly, nullable, copy) NSString *title;
|
||||
/// The number of rows in this section. Subclasses must override.
|
||||
/// This should not change until \c filterText is changed or \c reloadData is called.
|
||||
@property (nonatomic, readonly) NSInteger numberOfRows;
|
||||
/// A map of reuse identifiers to \c UITableViewCell (sub)class objects.
|
||||
/// Subclasses \e may override this as necessary, but are not required to.
|
||||
/// See \c FLEXTableView.h for more information.
|
||||
/// @return nil by default.
|
||||
@property (nonatomic, readonly, nullable) NSDictionary<NSString *, Class> *cellRegistrationMapping;
|
||||
|
||||
/// The section should filter itself based on the contents of this property
|
||||
/// as it is set. If it is set to nil or an empty string, it should not filter.
|
||||
/// Subclasses should override or observe this property and react to changes.
|
||||
///
|
||||
/// It is common practice to use two arrays for the underlying model:
|
||||
/// One to hold all rows, and one to hold unfiltered rows. When \c setFilterText:
|
||||
/// is called, call \c super to store the new value, and re-filter your model accordingly.
|
||||
@property (nonatomic, nullable) NSString *filterText;
|
||||
|
||||
/// Provides an avenue for the section to refresh data or change the number of rows.
|
||||
///
|
||||
/// This is called before reloading the table view itself. If your section pulls data
|
||||
/// from an external data source, this is a good place to refresh that data entirely.
|
||||
/// If your section does not, then it might be simpler for you to just override
|
||||
/// \c setFilterText: to call \c super and call \c reloadData.
|
||||
- (void)reloadData;
|
||||
|
||||
/// Like \c reloadData, but optionally reloads the table view section
|
||||
/// associated with this section object, if any. Do not override.
|
||||
/// Do not call outside of the main thread.
|
||||
- (void)reloadData:(BOOL)updateTable;
|
||||
|
||||
/// Provide a table view and section index to allow the section to efficiently reload
|
||||
/// its own section of the table when something changes it. The table reference is
|
||||
/// held weakly, and subclasses cannot access it or the index. Call this method again
|
||||
/// if the section numbers have changed since you last called it.
|
||||
- (void)setTable:(UITableView *)tableView section:(NSInteger)index;
|
||||
|
||||
#pragma mark - Row Selection
|
||||
|
||||
/// Whether the given row should be selectable, such as if tapping the cell
|
||||
/// should take the user to a new screen or trigger an action.
|
||||
/// Subclasses \e may override this as necessary, but are not required to.
|
||||
/// @return \c NO by default
|
||||
- (BOOL)canSelectRow:(NSInteger)row;
|
||||
|
||||
/// An action "future" to be triggered when the row is selected, if the row
|
||||
/// supports being selected as indicated by \c canSelectRow:. Subclasses
|
||||
/// must implement this in accordance with how they implement \c canSelectRow:
|
||||
/// if they do not implement \c viewControllerToPushForRow:
|
||||
/// @return This returns \c nil if no view controller is provided by
|
||||
/// \c viewControllerToPushForRow: — otherwise it pushes that view controller
|
||||
/// onto \c host.navigationController
|
||||
- (nullable void(^)(__kindof UIViewController *host))didSelectRowAction:(NSInteger)row;
|
||||
|
||||
/// A view controller to display when the row is selected, if the row
|
||||
/// supports being selected as indicated by \c canSelectRow:. Subclasses
|
||||
/// must implement this in accordance with how they implement \c canSelectRow:
|
||||
/// if they do not implement \c didSelectRowAction:
|
||||
/// @return \c nil by default
|
||||
- (nullable UIViewController *)viewControllerToPushForRow:(NSInteger)row;
|
||||
|
||||
/// Called when the accessory view's detail button is pressed.
|
||||
/// @return \c nil by default.
|
||||
- (nullable void(^)(__kindof UIViewController *host))didPressInfoButtonAction:(NSInteger)row;
|
||||
|
||||
#pragma mark - Context Menus
|
||||
|
||||
/// By default, this is the title of the row.
|
||||
/// @return The title of the context menu, if any.
|
||||
- (nullable NSString *)menuTitleForRow:(NSInteger)row API_AVAILABLE(ios(13.0));
|
||||
/// Protected, not intended for public use. \c menuTitleForRow:
|
||||
/// already includes the value returned from this method.
|
||||
///
|
||||
/// By default, this returns \c @"". Subclasses may override to
|
||||
/// provide a detailed description of the target of the context menu.
|
||||
- (NSString *)menuSubtitleForRow:(NSInteger)row API_AVAILABLE(ios(13.0));
|
||||
/// The context menu items, if any. Subclasses may override.
|
||||
/// By default, only inludes items for \c copyMenuItemsForRow:.
|
||||
- (nullable NSArray<UIMenuElement *> *)menuItemsForRow:(NSInteger)row sender:(UIViewController *)sender API_AVAILABLE(ios(13.0));
|
||||
/// Subclasses may override to return a list of copiable items.
|
||||
///
|
||||
/// Every two elements in the list compose a key-value pair, where the key
|
||||
/// should be a description of what will be copied, and the values should be
|
||||
/// the strings to copy. Return an empty string as a value to show a disabled action.
|
||||
- (nullable NSArray<NSString *> *)copyMenuItemsForRow:(NSInteger)row API_AVAILABLE(ios(13.0));
|
||||
|
||||
#pragma mark - Cell Configuration
|
||||
|
||||
/// Provide a reuse identifier for the given row. Subclasses should override.
|
||||
///
|
||||
/// Custom reuse identifiers should be specified in \c cellRegistrationMapping.
|
||||
/// You may return any of the identifiers in \c FLEXTableView.h
|
||||
/// without including them in the \c cellRegistrationMapping.
|
||||
/// @return \c kFLEXDefaultCell by default.
|
||||
- (NSString *)reuseIdentifierForRow:(NSInteger)row;
|
||||
/// Configure a cell for the given row. Subclasses must override.
|
||||
- (void)configureCell:(__kindof UITableViewCell *)cell forRow:(NSInteger)row;
|
||||
|
||||
#pragma mark - External Convenience
|
||||
|
||||
/// For use by whatever view controller uses your section. Not required.
|
||||
/// @return An optional title.
|
||||
- (nullable NSString *)titleForRow:(NSInteger)row;
|
||||
/// For use by whatever view controller uses your section. Not required.
|
||||
/// @return An optional subtitle.
|
||||
- (nullable NSString *)subtitleForRow:(NSInteger)row;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
137
Tweaks/FLEX/Core/FLEXTableViewSection.m
Normal file
137
Tweaks/FLEX/Core/FLEXTableViewSection.m
Normal file
@@ -0,0 +1,137 @@
|
||||
//
|
||||
// FLEXTableViewSection.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 1/29/20.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXTableViewSection.h"
|
||||
#import "FLEXTableView.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "UIMenu+FLEX.h"
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wincomplete-implementation"
|
||||
|
||||
@implementation FLEXTableViewSection
|
||||
|
||||
- (NSInteger)numberOfRows {
|
||||
return 0;
|
||||
}
|
||||
|
||||
- (void)reloadData { }
|
||||
|
||||
- (void)reloadData:(BOOL)updateTable {
|
||||
[self reloadData];
|
||||
if (updateTable) {
|
||||
NSIndexSet *index = [NSIndexSet indexSetWithIndex:_sectionIndex];
|
||||
[_tableView reloadSections:index withRowAnimation:UITableViewRowAnimationNone];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setTable:(UITableView *)tableView section:(NSInteger)index {
|
||||
_tableView = tableView;
|
||||
_sectionIndex = index;
|
||||
}
|
||||
|
||||
- (NSDictionary<NSString *,Class> *)cellRegistrationMapping {
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (BOOL)canSelectRow:(NSInteger)row { return NO; }
|
||||
|
||||
- (void (^)(__kindof UIViewController *))didSelectRowAction:(NSInteger)row {
|
||||
UIViewController *toPush = [self viewControllerToPushForRow:row];
|
||||
if (toPush) {
|
||||
return ^(UIViewController *host) {
|
||||
[host.navigationController pushViewController:toPush animated:YES];
|
||||
};
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (UIViewController *)viewControllerToPushForRow:(NSInteger)row {
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (void (^)(__kindof UIViewController *))didPressInfoButtonAction:(NSInteger)row {
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (NSString *)reuseIdentifierForRow:(NSInteger)row {
|
||||
return kFLEXDefaultCell;
|
||||
}
|
||||
|
||||
- (NSString *)menuTitleForRow:(NSInteger)row {
|
||||
NSString *title = [self titleForRow:row];
|
||||
NSString *subtitle = [self menuSubtitleForRow:row];
|
||||
|
||||
if (subtitle.length) {
|
||||
return [NSString stringWithFormat:@"%@\n\n%@", title, subtitle];
|
||||
}
|
||||
|
||||
return title;
|
||||
}
|
||||
|
||||
- (NSString *)menuSubtitleForRow:(NSInteger)row {
|
||||
return @"";
|
||||
}
|
||||
|
||||
- (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");
|
||||
|
||||
if (copyItems.count) {
|
||||
NSInteger numberOfActions = copyItems.count / 2;
|
||||
BOOL collapseMenu = numberOfActions > 4;
|
||||
UIImage *copyIcon = [UIImage systemImageNamed:@"doc.on.doc"];
|
||||
|
||||
NSMutableArray *actions = [NSMutableArray new];
|
||||
|
||||
for (NSInteger i = 0; i < copyItems.count; i += 2) {
|
||||
NSString *key = copyItems[i], *value = copyItems[i+1];
|
||||
NSString *title = collapseMenu ? key : [@"Copy " stringByAppendingString:key];
|
||||
|
||||
UIAction *copy = [UIAction
|
||||
actionWithTitle:title
|
||||
image:copyIcon
|
||||
identifier:nil
|
||||
handler:^(__kindof UIAction *action) {
|
||||
UIPasteboard.generalPasteboard.string = value;
|
||||
}
|
||||
];
|
||||
if (!value.length) {
|
||||
copy.attributes = UIMenuElementAttributesDisabled;
|
||||
}
|
||||
|
||||
[actions addObject:copy];
|
||||
}
|
||||
|
||||
UIMenu *copyMenu = [UIMenu
|
||||
flex_inlineMenuWithTitle:@"Copy…"
|
||||
image:copyIcon
|
||||
children:actions
|
||||
];
|
||||
|
||||
if (collapseMenu) {
|
||||
return @[[copyMenu flex_collapsed]];
|
||||
} else {
|
||||
return @[copyMenu];
|
||||
}
|
||||
}
|
||||
|
||||
return @[];
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)copyMenuItemsForRow:(NSInteger)row {
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (NSString *)titleForRow:(NSInteger)row { return nil; }
|
||||
- (NSString *)subtitleForRow:(NSInteger)row { return nil; }
|
||||
|
||||
@end
|
||||
|
||||
#pragma clang diagnostic pop
|
||||
15
Tweaks/FLEX/Core/Views/Carousel/FLEXCarouselCell.h
Normal file
15
Tweaks/FLEX/Core/Views/Carousel/FLEXCarouselCell.h
Normal file
@@ -0,0 +1,15 @@
|
||||
//
|
||||
// FLEXCarouselCell.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 7/17/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface FLEXCarouselCell : UICollectionViewCell
|
||||
|
||||
@property (nonatomic, copy) NSString *title;
|
||||
|
||||
@end
|
||||
93
Tweaks/FLEX/Core/Views/Carousel/FLEXCarouselCell.m
Normal file
93
Tweaks/FLEX/Core/Views/Carousel/FLEXCarouselCell.m
Normal file
@@ -0,0 +1,93 @@
|
||||
//
|
||||
// FLEXCarouselCell.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 7/17/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXCarouselCell.h"
|
||||
#import "FLEXColor.h"
|
||||
#import "UIView+FLEX_Layout.h"
|
||||
|
||||
@interface FLEXCarouselCell ()
|
||||
@property (nonatomic, readonly) UILabel *titleLabel;
|
||||
@property (nonatomic, readonly) UIView *selectionIndicatorStripe;
|
||||
@property (nonatomic) BOOL constraintsInstalled;
|
||||
@end
|
||||
|
||||
@implementation FLEXCarouselCell
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
_titleLabel = [UILabel new];
|
||||
_selectionIndicatorStripe = [UIView new];
|
||||
|
||||
self.titleLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];
|
||||
self.selectionIndicatorStripe.backgroundColor = self.tintColor;
|
||||
if (@available(iOS 10, *)) {
|
||||
self.titleLabel.adjustsFontForContentSizeCategory = YES;
|
||||
}
|
||||
|
||||
[self.contentView addSubview:self.titleLabel];
|
||||
[self.contentView addSubview:self.selectionIndicatorStripe];
|
||||
|
||||
[self installConstraints];
|
||||
|
||||
[self updateAppearance];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)updateAppearance {
|
||||
self.selectionIndicatorStripe.hidden = !self.selected;
|
||||
|
||||
if (self.selected) {
|
||||
self.titleLabel.textColor = self.tintColor;
|
||||
} else {
|
||||
self.titleLabel.textColor = FLEXColor.deemphasizedTextColor;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark Public
|
||||
|
||||
- (NSString *)title {
|
||||
return self.titleLabel.text;
|
||||
}
|
||||
|
||||
- (void)setTitle:(NSString *)title {
|
||||
self.titleLabel.text = title;
|
||||
[self.titleLabel sizeToFit];
|
||||
[self setNeedsLayout];
|
||||
}
|
||||
|
||||
#pragma mark Overrides
|
||||
|
||||
- (void)prepareForReuse {
|
||||
[super prepareForReuse];
|
||||
[self updateAppearance];
|
||||
}
|
||||
|
||||
- (void)installConstraints {
|
||||
CGFloat stripeHeight = 2;
|
||||
|
||||
self.titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
self.selectionIndicatorStripe.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
|
||||
UIView *superview = self.contentView;
|
||||
[self.titleLabel flex_pinEdgesToSuperviewWithInsets:UIEdgeInsetsMake(10, 15, 8 + stripeHeight, 15)];
|
||||
|
||||
[self.selectionIndicatorStripe.leadingAnchor constraintEqualToAnchor:superview.leadingAnchor].active = YES;
|
||||
[self.selectionIndicatorStripe.bottomAnchor constraintEqualToAnchor:superview.bottomAnchor].active = YES;
|
||||
[self.selectionIndicatorStripe.trailingAnchor constraintEqualToAnchor:superview.trailingAnchor].active = YES;
|
||||
[self.selectionIndicatorStripe.heightAnchor constraintEqualToConstant:stripeHeight].active = YES;
|
||||
}
|
||||
|
||||
- (void)setSelected:(BOOL)selected {
|
||||
super.selected = selected;
|
||||
[self updateAppearance];
|
||||
}
|
||||
|
||||
@end
|
||||
20
Tweaks/FLEX/Core/Views/Carousel/FLEXScopeCarousel.h
Normal file
20
Tweaks/FLEX/Core/Views/Carousel/FLEXScopeCarousel.h
Normal file
@@ -0,0 +1,20 @@
|
||||
//
|
||||
// FLEXScopeCarousel.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 7/17/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
/// Only use on iOS 10 and up. Requires iOS 10 APIs for calculating row sizes.
|
||||
@interface FLEXScopeCarousel : UIControl
|
||||
|
||||
@property (nonatomic, copy) NSArray<NSString *> *items;
|
||||
@property (nonatomic) NSInteger selectedIndex;
|
||||
@property (nonatomic) void(^selectedIndexChangedAction)(NSInteger idx);
|
||||
|
||||
- (void)registerBlockForDynamicTypeChanges:(void(^)(FLEXScopeCarousel *))handler;
|
||||
|
||||
@end
|
||||
204
Tweaks/FLEX/Core/Views/Carousel/FLEXScopeCarousel.m
Normal file
204
Tweaks/FLEX/Core/Views/Carousel/FLEXScopeCarousel.m
Normal file
@@ -0,0 +1,204 @@
|
||||
//
|
||||
// FLEXScopeCarousel.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 7/17/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXScopeCarousel.h"
|
||||
#import "FLEXCarouselCell.h"
|
||||
#import "FLEXColor.h"
|
||||
#import "FLEXMacros.h"
|
||||
#import "UIView+FLEX_Layout.h"
|
||||
|
||||
const CGFloat kCarouselItemSpacing = 0;
|
||||
NSString * const kCarouselCellReuseIdentifier = @"kCarouselCellReuseIdentifier";
|
||||
|
||||
@interface FLEXScopeCarousel () <UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout>
|
||||
@property (nonatomic, readonly) UICollectionView *collectionView;
|
||||
@property (nonatomic, readonly) FLEXCarouselCell *sizingCell;
|
||||
|
||||
@property (nonatomic, readonly) id dynamicTypeObserver;
|
||||
@property (nonatomic, readonly) NSMutableArray *dynamicTypeHandlers;
|
||||
|
||||
@property (nonatomic) BOOL constraintsInstalled;
|
||||
@end
|
||||
|
||||
@implementation FLEXScopeCarousel
|
||||
|
||||
- (id)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
self.backgroundColor = FLEXColor.primaryBackgroundColor;
|
||||
self.autoresizingMask = UIViewAutoresizingFlexibleWidth;
|
||||
self.translatesAutoresizingMaskIntoConstraints = YES;
|
||||
_dynamicTypeHandlers = [NSMutableArray new];
|
||||
|
||||
CGSize itemSize = CGSizeZero;
|
||||
if (@available(iOS 10.0, *)) {
|
||||
itemSize = UICollectionViewFlowLayoutAutomaticSize;
|
||||
}
|
||||
|
||||
// Collection view layout
|
||||
UICollectionViewFlowLayout *layout = ({
|
||||
UICollectionViewFlowLayout *layout = [UICollectionViewFlowLayout new];
|
||||
layout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
|
||||
layout.sectionInset = UIEdgeInsetsZero;
|
||||
layout.minimumLineSpacing = kCarouselItemSpacing;
|
||||
layout.itemSize = itemSize;
|
||||
layout.estimatedItemSize = itemSize;
|
||||
layout;
|
||||
});
|
||||
|
||||
// Collection view
|
||||
_collectionView = ({
|
||||
UICollectionView *cv = [[UICollectionView alloc]
|
||||
initWithFrame:CGRectZero
|
||||
collectionViewLayout:layout
|
||||
];
|
||||
cv.showsHorizontalScrollIndicator = NO;
|
||||
cv.backgroundColor = UIColor.clearColor;
|
||||
cv.delegate = self;
|
||||
cv.dataSource = self;
|
||||
[cv registerClass:[FLEXCarouselCell class] forCellWithReuseIdentifier:kCarouselCellReuseIdentifier];
|
||||
|
||||
[self addSubview:cv];
|
||||
cv;
|
||||
});
|
||||
|
||||
|
||||
// Sizing cell
|
||||
_sizingCell = [FLEXCarouselCell new];
|
||||
self.sizingCell.title = @"NSObject";
|
||||
|
||||
// Dynamic type
|
||||
weakify(self);
|
||||
_dynamicTypeObserver = [NSNotificationCenter.defaultCenter
|
||||
addObserverForName:UIContentSizeCategoryDidChangeNotification
|
||||
object:nil queue:nil usingBlock:^(NSNotification *note) { strongify(self)
|
||||
[self.collectionView setNeedsLayout];
|
||||
[self setNeedsUpdateConstraints];
|
||||
|
||||
// Notify observers
|
||||
for (void (^block)(FLEXScopeCarousel *) in self.dynamicTypeHandlers) {
|
||||
block(self);
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[NSNotificationCenter.defaultCenter removeObserver:self.dynamicTypeObserver];
|
||||
}
|
||||
|
||||
#pragma mark - Overrides
|
||||
|
||||
- (void)drawRect:(CGRect)rect {
|
||||
[super drawRect:rect];
|
||||
|
||||
CGFloat width = 1.f / UIScreen.mainScreen.scale;
|
||||
|
||||
// Draw hairline
|
||||
CGContextRef context = UIGraphicsGetCurrentContext();
|
||||
CGContextSetStrokeColorWithColor(context, FLEXColor.hairlineColor.CGColor);
|
||||
CGContextSetLineWidth(context, width);
|
||||
CGContextMoveToPoint(context, 0, rect.size.height - width);
|
||||
CGContextAddLineToPoint(context, rect.size.width, rect.size.height - width);
|
||||
CGContextStrokePath(context);
|
||||
}
|
||||
|
||||
+ (BOOL)requiresConstraintBasedLayout {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (void)updateConstraints {
|
||||
if (!self.constraintsInstalled) {
|
||||
self.collectionView.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
[self.collectionView flex_pinEdgesToSuperview];
|
||||
|
||||
self.constraintsInstalled = YES;
|
||||
}
|
||||
|
||||
[super updateConstraints];
|
||||
}
|
||||
|
||||
- (CGSize)intrinsicContentSize {
|
||||
return CGSizeMake(
|
||||
UIViewNoIntrinsicMetric,
|
||||
[self.sizingCell systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height
|
||||
);
|
||||
}
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
- (void)setItems:(NSArray<NSString *> *)items {
|
||||
NSParameterAssert(items.count);
|
||||
|
||||
_items = items.copy;
|
||||
|
||||
// Refresh list, select first item initially
|
||||
[self.collectionView reloadData];
|
||||
self.selectedIndex = 0;
|
||||
}
|
||||
|
||||
- (void)setSelectedIndex:(NSInteger)idx {
|
||||
NSParameterAssert(idx < self.items.count);
|
||||
|
||||
_selectedIndex = idx;
|
||||
NSIndexPath *path = [NSIndexPath indexPathForItem:idx inSection:0];
|
||||
[self.collectionView selectItemAtIndexPath:path
|
||||
animated:YES
|
||||
scrollPosition:UICollectionViewScrollPositionCenteredHorizontally];
|
||||
[self collectionView:self.collectionView didSelectItemAtIndexPath:path];
|
||||
}
|
||||
|
||||
- (void)registerBlockForDynamicTypeChanges:(void (^)(FLEXScopeCarousel *))handler {
|
||||
[self.dynamicTypeHandlers addObject:handler];
|
||||
}
|
||||
|
||||
#pragma mark - UICollectionView
|
||||
|
||||
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
|
||||
// if (@available(iOS 10.0, *)) {
|
||||
// return UICollectionViewFlowLayoutAutomaticSize;
|
||||
// }
|
||||
|
||||
self.sizingCell.title = self.items[indexPath.item];
|
||||
return [self.sizingCell systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
|
||||
}
|
||||
|
||||
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
|
||||
return self.items.count;
|
||||
}
|
||||
|
||||
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView
|
||||
cellForItemAtIndexPath:(NSIndexPath *)indexPath {
|
||||
FLEXCarouselCell *cell = (id)[collectionView dequeueReusableCellWithReuseIdentifier:kCarouselCellReuseIdentifier
|
||||
forIndexPath:indexPath];
|
||||
cell.title = self.items[indexPath.row];
|
||||
return cell;
|
||||
}
|
||||
|
||||
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
|
||||
_selectedIndex = indexPath.item; // In case self.selectedIndex didn't trigger this call
|
||||
|
||||
if (self.selectedIndexChangedAction) {
|
||||
self.selectedIndexChangedAction(indexPath.row);
|
||||
}
|
||||
|
||||
// TODO: dynamically choose a scroll position. Very wide items should
|
||||
// get "Left" while smaller items should not scroll at all, unless
|
||||
// they are only partially on the screen, in which case they
|
||||
// should get "HorizontallyCentered" to bring them onto the screen.
|
||||
// For now, everything goes to the left, as this has a similar effect.
|
||||
[collectionView scrollToItemAtIndexPath:indexPath
|
||||
atScrollPosition:UICollectionViewScrollPositionLeft
|
||||
animated:YES];
|
||||
[self sendActionsForControlEvents:UIControlEventValueChanged];
|
||||
}
|
||||
|
||||
@end
|
||||
17
Tweaks/FLEX/Core/Views/Cells/FLEXCodeFontCell.h
Normal file
17
Tweaks/FLEX/Core/Views/Cells/FLEXCodeFontCell.h
Normal file
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// FLEXCodeFontCell.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 12/27/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXMultilineTableViewCell.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FLEXCodeFontCell : FLEXMultilineDetailTableViewCell
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
34
Tweaks/FLEX/Core/Views/Cells/FLEXCodeFontCell.m
Normal file
34
Tweaks/FLEX/Core/Views/Cells/FLEXCodeFontCell.m
Normal file
@@ -0,0 +1,34 @@
|
||||
//
|
||||
// FLEXCodeFontCell.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 12/27/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXCodeFontCell.h"
|
||||
#import "UIFont+FLEX.h"
|
||||
|
||||
@implementation FLEXCodeFontCell
|
||||
|
||||
- (void)postInit {
|
||||
[super postInit];
|
||||
|
||||
self.titleLabel.font = UIFont.flex_codeFont;
|
||||
self.subtitleLabel.font = UIFont.flex_codeFont;
|
||||
|
||||
self.titleLabel.adjustsFontSizeToFitWidth = YES;
|
||||
self.titleLabel.minimumScaleFactor = 0.9;
|
||||
self.subtitleLabel.adjustsFontSizeToFitWidth = YES;
|
||||
self.subtitleLabel.minimumScaleFactor = 0.75;
|
||||
|
||||
// Disable mutli-line pre iOS 11
|
||||
if (@available(iOS 11, *)) {
|
||||
self.subtitleLabel.numberOfLines = 5;
|
||||
} else {
|
||||
self.titleLabel.numberOfLines = 1;
|
||||
self.subtitleLabel.numberOfLines = 1;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
13
Tweaks/FLEX/Core/Views/Cells/FLEXKeyValueTableViewCell.h
Normal file
13
Tweaks/FLEX/Core/Views/Cells/FLEXKeyValueTableViewCell.h
Normal file
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// FLEXKeyValueTableViewCell.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 1/23/20.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXTableViewCell.h"
|
||||
|
||||
@interface FLEXKeyValueTableViewCell : FLEXTableViewCell
|
||||
|
||||
@end
|
||||
17
Tweaks/FLEX/Core/Views/Cells/FLEXKeyValueTableViewCell.m
Normal file
17
Tweaks/FLEX/Core/Views/Cells/FLEXKeyValueTableViewCell.m
Normal file
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// FLEXKeyValueTableViewCell.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 1/23/20.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXKeyValueTableViewCell.h"
|
||||
|
||||
@implementation FLEXKeyValueTableViewCell
|
||||
|
||||
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
|
||||
return [super initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:reuseIdentifier];
|
||||
}
|
||||
|
||||
@end
|
||||
24
Tweaks/FLEX/Core/Views/Cells/FLEXMultilineTableViewCell.h
Normal file
24
Tweaks/FLEX/Core/Views/Cells/FLEXMultilineTableViewCell.h
Normal file
@@ -0,0 +1,24 @@
|
||||
//
|
||||
// FLEXMultilineTableViewCell.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Ryan Olson on 2/13/15.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXTableViewCell.h"
|
||||
|
||||
/// A cell with both labels set to be multi-line capable.
|
||||
@interface FLEXMultilineTableViewCell : FLEXTableViewCell
|
||||
|
||||
+ (CGFloat)preferredHeightWithAttributedText:(NSAttributedString *)attributedText
|
||||
maxWidth:(CGFloat)contentViewWidth
|
||||
style:(UITableViewStyle)style
|
||||
showsAccessory:(BOOL)showsAccessory;
|
||||
|
||||
@end
|
||||
|
||||
/// A \c FLEXMultilineTableViewCell initialized with \c UITableViewCellStyleSubtitle
|
||||
@interface FLEXMultilineDetailTableViewCell : FLEXMultilineTableViewCell
|
||||
|
||||
@end
|
||||
67
Tweaks/FLEX/Core/Views/Cells/FLEXMultilineTableViewCell.m
Normal file
67
Tweaks/FLEX/Core/Views/Cells/FLEXMultilineTableViewCell.m
Normal file
@@ -0,0 +1,67 @@
|
||||
//
|
||||
// FLEXMultilineTableViewCell.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Ryan Olson on 2/13/15.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXMultilineTableViewCell.h"
|
||||
#import "UIView+FLEX_Layout.h"
|
||||
#import "FLEXUtility.h"
|
||||
|
||||
@interface FLEXMultilineTableViewCell ()
|
||||
@property (nonatomic, readonly) UILabel *_titleLabel;
|
||||
@property (nonatomic, readonly) UILabel *_subtitleLabel;
|
||||
@property (nonatomic) BOOL constraintsUpdated;
|
||||
@end
|
||||
|
||||
@implementation FLEXMultilineTableViewCell
|
||||
|
||||
- (void)postInit {
|
||||
[super postInit];
|
||||
|
||||
self.titleLabel.numberOfLines = 0;
|
||||
self.subtitleLabel.numberOfLines = 0;
|
||||
}
|
||||
|
||||
+ (UIEdgeInsets)labelInsets {
|
||||
return UIEdgeInsetsMake(10.0, 16.0, 10.0, 8.0);
|
||||
}
|
||||
|
||||
+ (CGFloat)preferredHeightWithAttributedText:(NSAttributedString *)attributedText
|
||||
maxWidth:(CGFloat)contentViewWidth
|
||||
style:(UITableViewStyle)style
|
||||
showsAccessory:(BOOL)showsAccessory {
|
||||
CGFloat labelWidth = contentViewWidth;
|
||||
|
||||
// Content view inset due to accessory view observed on iOS 8.1 iPhone 6.
|
||||
if (showsAccessory) {
|
||||
labelWidth -= 34.0;
|
||||
}
|
||||
|
||||
UIEdgeInsets labelInsets = [self labelInsets];
|
||||
labelWidth -= (labelInsets.left + labelInsets.right);
|
||||
|
||||
CGSize constrainSize = CGSizeMake(labelWidth, CGFLOAT_MAX);
|
||||
CGRect boundingBox = [attributedText
|
||||
boundingRectWithSize:constrainSize
|
||||
options:NSStringDrawingUsesLineFragmentOrigin
|
||||
context:nil
|
||||
];
|
||||
CGFloat preferredLabelHeight = FLEXFloor(boundingBox.size.height);
|
||||
CGFloat preferredCellHeight = preferredLabelHeight + labelInsets.top + labelInsets.bottom + 1.0;
|
||||
|
||||
return preferredCellHeight;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation FLEXMultilineDetailTableViewCell
|
||||
|
||||
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
|
||||
return [super initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:reuseIdentifier];
|
||||
}
|
||||
|
||||
@end
|
||||
14
Tweaks/FLEX/Core/Views/Cells/FLEXSubtitleTableViewCell.h
Normal file
14
Tweaks/FLEX/Core/Views/Cells/FLEXSubtitleTableViewCell.h
Normal file
@@ -0,0 +1,14 @@
|
||||
//
|
||||
// FLEXSubtitleTableViewCell.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 4/17/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXTableViewCell.h"
|
||||
|
||||
/// A cell initialized with \c UITableViewCellStyleSubtitle
|
||||
@interface FLEXSubtitleTableViewCell : FLEXTableViewCell
|
||||
|
||||
@end
|
||||
17
Tweaks/FLEX/Core/Views/Cells/FLEXSubtitleTableViewCell.m
Normal file
17
Tweaks/FLEX/Core/Views/Cells/FLEXSubtitleTableViewCell.m
Normal file
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// FLEXSubtitleTableViewCell.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 4/17/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXSubtitleTableViewCell.h"
|
||||
|
||||
@implementation FLEXSubtitleTableViewCell
|
||||
|
||||
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
|
||||
return [super initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:reuseIdentifier];
|
||||
}
|
||||
|
||||
@end
|
||||
23
Tweaks/FLEX/Core/Views/Cells/FLEXTableViewCell.h
Normal file
23
Tweaks/FLEX/Core/Views/Cells/FLEXTableViewCell.h
Normal file
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// FLEXTableViewCell.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 4/17/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface FLEXTableViewCell : UITableViewCell
|
||||
|
||||
/// Use this instead of .textLabel
|
||||
@property (nonatomic, readonly) UILabel *titleLabel;
|
||||
/// Use this instead of .detailTextLabel
|
||||
@property (nonatomic, readonly) UILabel *subtitleLabel;
|
||||
|
||||
/// Subclasses can override this instead of initializers to
|
||||
/// perform additional initialization without lots of boilerplate.
|
||||
/// Remember to call super!
|
||||
- (void)postInit;
|
||||
|
||||
@end
|
||||
57
Tweaks/FLEX/Core/Views/Cells/FLEXTableViewCell.m
Normal file
57
Tweaks/FLEX/Core/Views/Cells/FLEXTableViewCell.m
Normal file
@@ -0,0 +1,57 @@
|
||||
//
|
||||
// FLEXTableViewCell.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 4/17/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXTableViewCell.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXColor.h"
|
||||
#import "FLEXTableView.h"
|
||||
|
||||
@interface UITableView (Internal)
|
||||
// Exists at least since iOS 5
|
||||
- (BOOL)_canPerformAction:(SEL)action forCell:(UITableViewCell *)cell sender:(id)sender;
|
||||
- (void)_performAction:(SEL)action forCell:(UITableViewCell *)cell sender:(id)sender;
|
||||
@end
|
||||
|
||||
@interface UITableViewCell (Internal)
|
||||
// Exists at least since iOS 5
|
||||
@property (nonatomic, readonly) FLEXTableView *_tableView;
|
||||
@end
|
||||
|
||||
@implementation FLEXTableViewCell
|
||||
|
||||
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
|
||||
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
|
||||
if (self) {
|
||||
[self postInit];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)postInit {
|
||||
UIFont *cellFont = UIFont.flex_defaultTableCellFont;
|
||||
self.titleLabel.font = cellFont;
|
||||
self.subtitleLabel.font = cellFont;
|
||||
self.subtitleLabel.textColor = FLEXColor.deemphasizedTextColor;
|
||||
|
||||
self.titleLabel.lineBreakMode = NSLineBreakByTruncatingMiddle;
|
||||
self.subtitleLabel.lineBreakMode = NSLineBreakByTruncatingMiddle;
|
||||
|
||||
self.titleLabel.numberOfLines = 1;
|
||||
self.subtitleLabel.numberOfLines = 1;
|
||||
}
|
||||
|
||||
- (UILabel *)titleLabel {
|
||||
return self.textLabel;
|
||||
}
|
||||
|
||||
- (UILabel *)subtitleLabel {
|
||||
return self.detailTextLabel;
|
||||
}
|
||||
|
||||
@end
|
||||
48
Tweaks/FLEX/Core/Views/FLEXTableView.h
Normal file
48
Tweaks/FLEX/Core/Views/FLEXTableView.h
Normal file
@@ -0,0 +1,48 @@
|
||||
//
|
||||
// FLEXTableView.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 4/17/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
#pragma mark Reuse identifiers
|
||||
|
||||
typedef NSString * FLEXTableViewCellReuseIdentifier;
|
||||
|
||||
/// A regular \c FLEXTableViewCell initialized with \c UITableViewCellStyleDefault
|
||||
extern FLEXTableViewCellReuseIdentifier const kFLEXDefaultCell;
|
||||
/// A \c FLEXSubtitleTableViewCell initialized with \c UITableViewCellStyleSubtitle
|
||||
extern FLEXTableViewCellReuseIdentifier const kFLEXDetailCell;
|
||||
/// A \c FLEXMultilineTableViewCell initialized with \c UITableViewCellStyleDefault
|
||||
extern FLEXTableViewCellReuseIdentifier const kFLEXMultilineCell;
|
||||
/// A \c FLEXMultilineTableViewCell initialized with \c UITableViewCellStyleSubtitle
|
||||
extern FLEXTableViewCellReuseIdentifier const kFLEXMultilineDetailCell;
|
||||
/// A \c FLEXTableViewCell initialized with \c UITableViewCellStyleValue1
|
||||
extern FLEXTableViewCellReuseIdentifier const kFLEXKeyValueCell;
|
||||
/// A \c FLEXSubtitleTableViewCell which uses monospaced fonts for both labels
|
||||
extern FLEXTableViewCellReuseIdentifier const kFLEXCodeFontCell;
|
||||
|
||||
#pragma mark - FLEXTableView
|
||||
@interface FLEXTableView : UITableView
|
||||
|
||||
+ (instancetype)flexDefaultTableView;
|
||||
+ (instancetype)groupedTableView;
|
||||
+ (instancetype)plainTableView;
|
||||
+ (instancetype)style:(UITableViewStyle)style;
|
||||
|
||||
/// You do not need to register classes for any of the default reuse identifiers above
|
||||
/// (annotated as \c FLEXTableViewCellReuseIdentifier types) unless you wish to provide
|
||||
/// a custom cell for any of those reuse identifiers. By default, \c FLEXTableViewCell,
|
||||
/// \c FLEXSubtitleTableViewCell, and \c FLEXMultilineTableViewCell are used, respectively.
|
||||
///
|
||||
/// @param registrationMapping A map of reuse identifiers to \c UITableViewCell (sub)class objects.
|
||||
- (void)registerCells:(NSDictionary<NSString *, Class> *)registrationMapping;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
83
Tweaks/FLEX/Core/Views/FLEXTableView.m
Normal file
83
Tweaks/FLEX/Core/Views/FLEXTableView.m
Normal file
@@ -0,0 +1,83 @@
|
||||
//
|
||||
// FLEXTableView.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 4/17/19.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXTableView.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXSubtitleTableViewCell.h"
|
||||
#import "FLEXMultilineTableViewCell.h"
|
||||
#import "FLEXKeyValueTableViewCell.h"
|
||||
#import "FLEXCodeFontCell.h"
|
||||
|
||||
FLEXTableViewCellReuseIdentifier const kFLEXDefaultCell = @"kFLEXDefaultCell";
|
||||
FLEXTableViewCellReuseIdentifier const kFLEXDetailCell = @"kFLEXDetailCell";
|
||||
FLEXTableViewCellReuseIdentifier const kFLEXMultilineCell = @"kFLEXMultilineCell";
|
||||
FLEXTableViewCellReuseIdentifier const kFLEXMultilineDetailCell = @"kFLEXMultilineDetailCell";
|
||||
FLEXTableViewCellReuseIdentifier const kFLEXKeyValueCell = @"kFLEXKeyValueCell";
|
||||
FLEXTableViewCellReuseIdentifier const kFLEXCodeFontCell = @"kFLEXCodeFontCell";
|
||||
|
||||
#pragma mark Private
|
||||
|
||||
@interface UITableView (Private)
|
||||
- (CGFloat)_heightForHeaderInSection:(NSInteger)section;
|
||||
- (NSString *)_titleForHeaderInSection:(NSInteger)section;
|
||||
@end
|
||||
|
||||
@implementation FLEXTableView
|
||||
|
||||
+ (instancetype)flexDefaultTableView {
|
||||
if (@available(iOS 13.0, *)) {
|
||||
return [[self alloc] initWithFrame:CGRectZero style:UITableViewStyleInsetGrouped];
|
||||
} else {
|
||||
return [[self alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Initialization
|
||||
|
||||
+ (id)groupedTableView {
|
||||
if (@available(iOS 13.0, *)) {
|
||||
return [[self alloc] initWithFrame:CGRectZero style:UITableViewStyleInsetGrouped];
|
||||
} else {
|
||||
return [[self alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];
|
||||
}
|
||||
}
|
||||
|
||||
+ (id)plainTableView {
|
||||
return [[self alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
|
||||
}
|
||||
|
||||
+ (id)style:(UITableViewStyle)style {
|
||||
return [[self alloc] initWithFrame:CGRectZero style:style];
|
||||
}
|
||||
|
||||
- (id)initWithFrame:(CGRect)frame style:(UITableViewStyle)style {
|
||||
self = [super initWithFrame:frame style:style];
|
||||
if (self) {
|
||||
[self registerCells:@{
|
||||
kFLEXDefaultCell : [FLEXTableViewCell class],
|
||||
kFLEXDetailCell : [FLEXSubtitleTableViewCell class],
|
||||
kFLEXMultilineCell : [FLEXMultilineTableViewCell class],
|
||||
kFLEXMultilineDetailCell : [FLEXMultilineDetailTableViewCell class],
|
||||
kFLEXKeyValueCell : [FLEXKeyValueTableViewCell class],
|
||||
kFLEXCodeFontCell : [FLEXCodeFontCell class],
|
||||
}];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
- (void)registerCells:(NSDictionary<NSString*, Class> *)registrationMapping {
|
||||
[registrationMapping enumerateKeysAndObjectsUsingBlock:^(NSString *identifier, Class cellClass, BOOL *stop) {
|
||||
[self registerClass:cellClass forCellReuseIdentifier:identifier];
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// FLEXArgumentInputColorView.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/30/14.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputView.h"
|
||||
|
||||
@interface FLEXArgumentInputColorView : FLEXArgumentInputView
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,311 @@
|
||||
//
|
||||
// FLEXArgumentInputColorView.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/30/14.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputColorView.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
|
||||
@protocol FLEXColorComponentInputViewDelegate;
|
||||
|
||||
@interface FLEXColorComponentInputView : UIView
|
||||
|
||||
@property (nonatomic) UISlider *slider;
|
||||
@property (nonatomic) UILabel *valueLabel;
|
||||
|
||||
@property (nonatomic, weak) id <FLEXColorComponentInputViewDelegate> delegate;
|
||||
|
||||
@end
|
||||
|
||||
@protocol FLEXColorComponentInputViewDelegate <NSObject>
|
||||
|
||||
- (void)colorComponentInputViewValueDidChange:(FLEXColorComponentInputView *)colorComponentInputView;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation FLEXColorComponentInputView
|
||||
|
||||
- (id)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
self.slider = [UISlider new];
|
||||
[self.slider addTarget:self action:@selector(sliderChanged:) forControlEvents:UIControlEventValueChanged];
|
||||
[self addSubview:self.slider];
|
||||
|
||||
self.valueLabel = [UILabel new];
|
||||
self.valueLabel.backgroundColor = self.backgroundColor;
|
||||
self.valueLabel.font = [UIFont systemFontOfSize:14.0];
|
||||
self.valueLabel.textAlignment = NSTextAlignmentRight;
|
||||
[self addSubview:self.valueLabel];
|
||||
|
||||
[self updateValueLabel];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setBackgroundColor:(UIColor *)backgroundColor {
|
||||
[super setBackgroundColor:backgroundColor];
|
||||
self.slider.backgroundColor = backgroundColor;
|
||||
self.valueLabel.backgroundColor = backgroundColor;
|
||||
}
|
||||
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
|
||||
const CGFloat kValueLabelWidth = 50.0;
|
||||
|
||||
[self.slider sizeToFit];
|
||||
CGFloat sliderWidth = self.bounds.size.width - kValueLabelWidth;
|
||||
self.slider.frame = CGRectMake(0, 0, sliderWidth, self.slider.frame.size.height);
|
||||
|
||||
[self.valueLabel sizeToFit];
|
||||
CGFloat valueLabelOriginX = CGRectGetMaxX(self.slider.frame);
|
||||
CGFloat valueLabelOriginY = FLEXFloor((self.slider.frame.size.height - self.valueLabel.frame.size.height) / 2.0);
|
||||
self.valueLabel.frame = CGRectMake(valueLabelOriginX, valueLabelOriginY, kValueLabelWidth, self.valueLabel.frame.size.height);
|
||||
}
|
||||
|
||||
- (void)sliderChanged:(id)sender {
|
||||
[self.delegate colorComponentInputViewValueDidChange:self];
|
||||
[self updateValueLabel];
|
||||
}
|
||||
|
||||
- (void)updateValueLabel {
|
||||
self.valueLabel.text = [NSString stringWithFormat:@"%.3f", self.slider.value];
|
||||
}
|
||||
|
||||
- (CGSize)sizeThatFits:(CGSize)size {
|
||||
CGFloat height = [self.slider sizeThatFits:size].height;
|
||||
return CGSizeMake(size.width, height);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface FLEXColorPreviewBox : UIView
|
||||
|
||||
@property (nonatomic) UIColor *color;
|
||||
|
||||
@property (nonatomic) UIView *colorOverlayView;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXColorPreviewBox
|
||||
|
||||
- (id)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
self.layer.borderWidth = 1.0;
|
||||
self.layer.borderColor = UIColor.blackColor.CGColor;
|
||||
self.backgroundColor = [UIColor colorWithPatternImage:[[self class] backgroundPatternImage]];
|
||||
|
||||
self.colorOverlayView = [[UIView alloc] initWithFrame:self.bounds];
|
||||
self.colorOverlayView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
||||
self.colorOverlayView.backgroundColor = UIColor.clearColor;
|
||||
[self addSubview:self.colorOverlayView];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setColor:(UIColor *)color {
|
||||
self.colorOverlayView.backgroundColor = color;
|
||||
}
|
||||
|
||||
- (UIColor *)color {
|
||||
return self.colorOverlayView.backgroundColor;
|
||||
}
|
||||
|
||||
+ (UIImage *)backgroundPatternImage {
|
||||
const CGFloat kSquareDimension = 5.0;
|
||||
CGSize squareSize = CGSizeMake(kSquareDimension, kSquareDimension);
|
||||
CGSize imageSize = CGSizeMake(2.0 * kSquareDimension, 2.0 * kSquareDimension);
|
||||
|
||||
UIGraphicsBeginImageContextWithOptions(imageSize, YES, UIScreen.mainScreen.scale);
|
||||
|
||||
[UIColor.whiteColor setFill];
|
||||
UIRectFill(CGRectMake(0, 0, imageSize.width, imageSize.height));
|
||||
|
||||
[UIColor.grayColor setFill];
|
||||
UIRectFill(CGRectMake(squareSize.width, 0, squareSize.width, squareSize.height));
|
||||
UIRectFill(CGRectMake(0, squareSize.height, squareSize.width, squareSize.height));
|
||||
|
||||
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
|
||||
UIGraphicsEndImageContext();
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface FLEXArgumentInputColorView () <FLEXColorComponentInputViewDelegate>
|
||||
|
||||
@property (nonatomic) FLEXColorPreviewBox *colorPreviewBox;
|
||||
@property (nonatomic) UILabel *hexLabel;
|
||||
@property (nonatomic) FLEXColorComponentInputView *alphaInput;
|
||||
@property (nonatomic) FLEXColorComponentInputView *redInput;
|
||||
@property (nonatomic) FLEXColorComponentInputView *greenInput;
|
||||
@property (nonatomic) FLEXColorComponentInputView *blueInput;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXArgumentInputColorView
|
||||
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
|
||||
self = [super initWithArgumentTypeEncoding:typeEncoding];
|
||||
if (self) {
|
||||
self.colorPreviewBox = [FLEXColorPreviewBox new];
|
||||
[self addSubview:self.colorPreviewBox];
|
||||
|
||||
self.hexLabel = [UILabel new];
|
||||
self.hexLabel.backgroundColor = [UIColor colorWithWhite:1.0 alpha:0.9];
|
||||
self.hexLabel.textAlignment = NSTextAlignmentCenter;
|
||||
self.hexLabel.font = [UIFont systemFontOfSize:12.0];
|
||||
[self addSubview:self.hexLabel];
|
||||
|
||||
self.alphaInput = [FLEXColorComponentInputView new];
|
||||
self.alphaInput.slider.minimumTrackTintColor = UIColor.blackColor;
|
||||
self.alphaInput.delegate = self;
|
||||
[self addSubview:self.alphaInput];
|
||||
|
||||
self.redInput = [FLEXColorComponentInputView new];
|
||||
self.redInput.slider.minimumTrackTintColor = UIColor.redColor;
|
||||
self.redInput.delegate = self;
|
||||
[self addSubview:self.redInput];
|
||||
|
||||
self.greenInput = [FLEXColorComponentInputView new];
|
||||
self.greenInput.slider.minimumTrackTintColor = UIColor.greenColor;
|
||||
self.greenInput.delegate = self;
|
||||
[self addSubview:self.greenInput];
|
||||
|
||||
self.blueInput = [FLEXColorComponentInputView new];
|
||||
self.blueInput.slider.minimumTrackTintColor = UIColor.blueColor;
|
||||
self.blueInput.delegate = self;
|
||||
[self addSubview:self.blueInput];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setBackgroundColor:(UIColor *)backgroundColor {
|
||||
[super setBackgroundColor:backgroundColor];
|
||||
self.alphaInput.backgroundColor = backgroundColor;
|
||||
self.redInput.backgroundColor = backgroundColor;
|
||||
self.greenInput.backgroundColor = backgroundColor;
|
||||
self.blueInput.backgroundColor = backgroundColor;
|
||||
}
|
||||
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
|
||||
CGFloat runningOriginY = 0;
|
||||
CGSize constrainSize = CGSizeMake(self.bounds.size.width, CGFLOAT_MAX);
|
||||
|
||||
self.colorPreviewBox.frame = CGRectMake(0, runningOriginY, self.bounds.size.width, [[self class] colorPreviewBoxHeight]);
|
||||
runningOriginY = CGRectGetMaxY(self.colorPreviewBox.frame) + [[self class] inputViewVerticalPadding];
|
||||
|
||||
[self.hexLabel sizeToFit];
|
||||
const CGFloat kLabelVerticalOutsetAmount = 0.0;
|
||||
const CGFloat kLabelHorizontalOutsetAmount = 2.0;
|
||||
UIEdgeInsets labelOutset = UIEdgeInsetsMake(-kLabelVerticalOutsetAmount, -kLabelHorizontalOutsetAmount, -kLabelVerticalOutsetAmount, -kLabelHorizontalOutsetAmount);
|
||||
self.hexLabel.frame = UIEdgeInsetsInsetRect(self.hexLabel.frame, labelOutset);
|
||||
CGFloat hexLabelOriginX = self.colorPreviewBox.layer.borderWidth;
|
||||
CGFloat hexLabelOriginY = CGRectGetMaxY(self.colorPreviewBox.frame) - self.colorPreviewBox.layer.borderWidth - self.hexLabel.frame.size.height;
|
||||
self.hexLabel.frame = CGRectMake(hexLabelOriginX, hexLabelOriginY, self.hexLabel.frame.size.width, self.hexLabel.frame.size.height);
|
||||
|
||||
NSArray<FLEXColorComponentInputView *> *colorComponentInputViews = @[self.alphaInput, self.redInput, self.greenInput, self.blueInput];
|
||||
for (FLEXColorComponentInputView *inputView in colorComponentInputViews) {
|
||||
CGSize fitSize = [inputView sizeThatFits:constrainSize];
|
||||
inputView.frame = CGRectMake(0, runningOriginY, fitSize.width, fitSize.height);
|
||||
runningOriginY = CGRectGetMaxY(inputView.frame) + [[self class] inputViewVerticalPadding];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setInputValue:(id)inputValue {
|
||||
if ([inputValue isKindOfClass:[UIColor class]]) {
|
||||
[self updateWithColor:inputValue];
|
||||
} else if ([inputValue isKindOfClass:[NSValue class]]) {
|
||||
const char *type = [inputValue objCType];
|
||||
if (strcmp(type, @encode(CGColorRef)) == 0) {
|
||||
CGColorRef colorRef;
|
||||
[inputValue getValue:&colorRef];
|
||||
UIColor *color = [[UIColor alloc] initWithCGColor:colorRef];
|
||||
[self updateWithColor:color];
|
||||
}
|
||||
} else {
|
||||
[self updateWithColor:UIColor.clearColor];
|
||||
}
|
||||
}
|
||||
|
||||
- (id)inputValue {
|
||||
return [UIColor colorWithRed:self.redInput.slider.value green:self.greenInput.slider.value blue:self.blueInput.slider.value alpha:self.alphaInput.slider.value];
|
||||
}
|
||||
|
||||
- (void)colorComponentInputViewValueDidChange:(FLEXColorComponentInputView *)colorComponentInputView {
|
||||
[self updateColorPreview];
|
||||
}
|
||||
|
||||
- (void)updateWithColor:(UIColor *)color {
|
||||
CGFloat red, green, blue, white, alpha;
|
||||
if ([color getRed:&red green:&green blue:&blue alpha:&alpha]) {
|
||||
self.alphaInput.slider.value = alpha;
|
||||
[self.alphaInput updateValueLabel];
|
||||
self.redInput.slider.value = red;
|
||||
[self.redInput updateValueLabel];
|
||||
self.greenInput.slider.value = green;
|
||||
[self.greenInput updateValueLabel];
|
||||
self.blueInput.slider.value = blue;
|
||||
[self.blueInput updateValueLabel];
|
||||
} else if ([color getWhite:&white alpha:&alpha]) {
|
||||
self.alphaInput.slider.value = alpha;
|
||||
[self.alphaInput updateValueLabel];
|
||||
self.redInput.slider.value = white;
|
||||
[self.redInput updateValueLabel];
|
||||
self.greenInput.slider.value = white;
|
||||
[self.greenInput updateValueLabel];
|
||||
self.blueInput.slider.value = white;
|
||||
[self.blueInput updateValueLabel];
|
||||
}
|
||||
[self updateColorPreview];
|
||||
}
|
||||
|
||||
- (void)updateColorPreview {
|
||||
self.colorPreviewBox.color = self.inputValue;
|
||||
unsigned char redByte = self.redInput.slider.value * 255;
|
||||
unsigned char greenByte = self.greenInput.slider.value * 255;
|
||||
unsigned char blueByte = self.blueInput.slider.value * 255;
|
||||
self.hexLabel.text = [NSString stringWithFormat:@"#%02X%02X%02X", redByte, greenByte, blueByte];
|
||||
[self setNeedsLayout];
|
||||
}
|
||||
|
||||
- (CGSize)sizeThatFits:(CGSize)size {
|
||||
CGFloat height = 0;
|
||||
height += [[self class] colorPreviewBoxHeight];
|
||||
height += [[self class] inputViewVerticalPadding];
|
||||
height += [self.alphaInput sizeThatFits:size].height;
|
||||
height += [[self class] inputViewVerticalPadding];
|
||||
height += [self.redInput sizeThatFits:size].height;
|
||||
height += [[self class] inputViewVerticalPadding];
|
||||
height += [self.greenInput sizeThatFits:size].height;
|
||||
height += [[self class] inputViewVerticalPadding];
|
||||
height += [self.blueInput sizeThatFits:size].height;
|
||||
return CGSizeMake(size.width, height);
|
||||
}
|
||||
|
||||
+ (CGFloat)inputViewVerticalPadding {
|
||||
return 10.0;
|
||||
}
|
||||
|
||||
+ (CGFloat)colorPreviewBoxHeight {
|
||||
return 40.0;
|
||||
}
|
||||
|
||||
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
|
||||
NSParameterAssert(type);
|
||||
|
||||
// We don't care if currentValue is a color or not; we will default to +clearColor
|
||||
return (strcmp(type, @encode(CGColorRef)) == 0) || (strcmp(type, FLEXEncodeClass(UIColor)) == 0);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// FLEXArgumentInputDataView.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Daniel Rodriguez Troitino on 2/14/15.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputView.h"
|
||||
|
||||
@interface FLEXArgumentInputDateView : FLEXArgumentInputView
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,58 @@
|
||||
//
|
||||
// FLEXArgumentInputDataView.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Daniel Rodriguez Troitino on 2/14/15.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputDateView.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
|
||||
@interface FLEXArgumentInputDateView ()
|
||||
|
||||
@property (nonatomic) UIDatePicker *datePicker;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXArgumentInputDateView
|
||||
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
|
||||
self = [super initWithArgumentTypeEncoding:typeEncoding];
|
||||
if (self) {
|
||||
self.datePicker = [UIDatePicker new];
|
||||
self.datePicker.datePickerMode = UIDatePickerModeDateAndTime;
|
||||
// Using UTC, because that's what the NSDate description prints
|
||||
self.datePicker.calendar = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierGregorian];
|
||||
self.datePicker.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"];
|
||||
[self addSubview:self.datePicker];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setInputValue:(id)inputValue {
|
||||
if ([inputValue isKindOfClass:[NSDate class]]) {
|
||||
self.datePicker.date = inputValue;
|
||||
}
|
||||
}
|
||||
|
||||
- (id)inputValue {
|
||||
return self.datePicker.date;
|
||||
}
|
||||
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
self.datePicker.frame = self.bounds;
|
||||
}
|
||||
|
||||
- (CGSize)sizeThatFits:(CGSize)size {
|
||||
CGFloat height = [self.datePicker sizeThatFits:size].height;
|
||||
return CGSizeMake(size.width, height);
|
||||
}
|
||||
|
||||
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
|
||||
NSParameterAssert(type);
|
||||
return strcmp(type, FLEXEncodeClass(NSDate)) == 0;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// FLEXArgumentInputFontView.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/28/14.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputView.h"
|
||||
|
||||
@interface FLEXArgumentInputFontView : FLEXArgumentInputView
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,109 @@
|
||||
//
|
||||
// FLEXArgumentInputFontView.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/28/14.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputFontView.h"
|
||||
#import "FLEXArgumentInputViewFactory.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
#import "FLEXArgumentInputFontsPickerView.h"
|
||||
|
||||
@interface FLEXArgumentInputFontView ()
|
||||
|
||||
@property (nonatomic) FLEXArgumentInputView *fontNameInput;
|
||||
@property (nonatomic) FLEXArgumentInputView *pointSizeInput;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXArgumentInputFontView
|
||||
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
|
||||
self = [super initWithArgumentTypeEncoding:typeEncoding];
|
||||
if (self) {
|
||||
self.fontNameInput = [[FLEXArgumentInputFontsPickerView alloc] initWithArgumentTypeEncoding:FLEXEncodeClass(NSString)];
|
||||
self.fontNameInput.targetSize = FLEXArgumentInputViewSizeSmall;
|
||||
self.fontNameInput.title = @"Font Name:";
|
||||
[self addSubview:self.fontNameInput];
|
||||
|
||||
self.pointSizeInput = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:@encode(CGFloat)];
|
||||
self.pointSizeInput.targetSize = FLEXArgumentInputViewSizeSmall;
|
||||
self.pointSizeInput.title = @"Point Size:";
|
||||
[self addSubview:self.pointSizeInput];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setBackgroundColor:(UIColor *)backgroundColor {
|
||||
[super setBackgroundColor:backgroundColor];
|
||||
self.fontNameInput.backgroundColor = backgroundColor;
|
||||
self.pointSizeInput.backgroundColor = backgroundColor;
|
||||
}
|
||||
|
||||
- (void)setInputValue:(id)inputValue {
|
||||
if ([inputValue isKindOfClass:[UIFont class]]) {
|
||||
UIFont *font = (UIFont *)inputValue;
|
||||
self.fontNameInput.inputValue = font.fontName;
|
||||
self.pointSizeInput.inputValue = @(font.pointSize);
|
||||
}
|
||||
}
|
||||
|
||||
- (id)inputValue {
|
||||
CGFloat pointSize = 0;
|
||||
if ([self.pointSizeInput.inputValue isKindOfClass:[NSValue class]]) {
|
||||
NSValue *pointSizeValue = (NSValue *)self.pointSizeInput.inputValue;
|
||||
if (strcmp([pointSizeValue objCType], @encode(CGFloat)) == 0) {
|
||||
[pointSizeValue getValue:&pointSize];
|
||||
}
|
||||
}
|
||||
return [UIFont fontWithName:self.fontNameInput.inputValue size:pointSize];
|
||||
}
|
||||
|
||||
- (BOOL)inputViewIsFirstResponder {
|
||||
return [self.fontNameInput inputViewIsFirstResponder] || [self.pointSizeInput inputViewIsFirstResponder];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Layout and Sizing
|
||||
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
|
||||
CGFloat runningOriginY = self.topInputFieldVerticalLayoutGuide;
|
||||
|
||||
CGSize fontNameFitSize = [self.fontNameInput sizeThatFits:self.bounds.size];
|
||||
self.fontNameInput.frame = CGRectMake(0, runningOriginY, fontNameFitSize.width, fontNameFitSize.height);
|
||||
runningOriginY = CGRectGetMaxY(self.fontNameInput.frame) + [[self class] verticalPaddingBetweenFields];
|
||||
|
||||
CGSize pointSizeFitSize = [self.pointSizeInput sizeThatFits:self.bounds.size];
|
||||
self.pointSizeInput.frame = CGRectMake(0, runningOriginY, pointSizeFitSize.width, pointSizeFitSize.height);
|
||||
}
|
||||
|
||||
+ (CGFloat)verticalPaddingBetweenFields {
|
||||
return 10.0;
|
||||
}
|
||||
|
||||
- (CGSize)sizeThatFits:(CGSize)size {
|
||||
CGSize fitSize = [super sizeThatFits:size];
|
||||
|
||||
CGSize constrainSize = CGSizeMake(size.width, CGFLOAT_MAX);
|
||||
|
||||
CGFloat height = fitSize.height;
|
||||
height += [self.fontNameInput sizeThatFits:constrainSize].height;
|
||||
height += [[self class] verticalPaddingBetweenFields];
|
||||
height += [self.pointSizeInput sizeThatFits:constrainSize].height;
|
||||
|
||||
return CGSizeMake(fitSize.width, height);
|
||||
}
|
||||
|
||||
|
||||
#pragma mark -
|
||||
|
||||
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
|
||||
NSParameterAssert(type);
|
||||
return strcmp(type, FLEXEncodeClass(UIFont)) == 0;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,12 @@
|
||||
//
|
||||
// FLEXArgumentInputFontsPickerView.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by 啟倫 陳 on 2014/7/27.
|
||||
// Copyright (c) 2014年 f. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputTextView.h"
|
||||
|
||||
@interface FLEXArgumentInputFontsPickerView : FLEXArgumentInputTextView <UIPickerViewDataSource, UIPickerViewDelegate>
|
||||
@end
|
||||
@@ -0,0 +1,96 @@
|
||||
//
|
||||
// FLEXArgumentInputFontsPickerView.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by 啟倫 陳 on 2014/7/27.
|
||||
// Copyright (c) 2014年 f. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputFontsPickerView.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
|
||||
@interface FLEXArgumentInputFontsPickerView ()
|
||||
|
||||
@property (nonatomic) NSMutableArray<NSString *> *availableFonts;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation FLEXArgumentInputFontsPickerView
|
||||
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
|
||||
self = [super initWithArgumentTypeEncoding:typeEncoding];
|
||||
if (self) {
|
||||
self.targetSize = FLEXArgumentInputViewSizeSmall;
|
||||
[self createAvailableFonts];
|
||||
self.inputTextView.inputView = [self createFontsPicker];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setInputValue:(id)inputValue {
|
||||
self.inputTextView.text = inputValue;
|
||||
if ([self.availableFonts indexOfObject:inputValue] == NSNotFound) {
|
||||
[self.availableFonts insertObject:inputValue atIndex:0];
|
||||
}
|
||||
[(UIPickerView *)self.inputTextView.inputView selectRow:[self.availableFonts indexOfObject:inputValue] inComponent:0 animated:NO];
|
||||
}
|
||||
|
||||
- (id)inputValue {
|
||||
return self.inputTextView.text.length > 0 ? [self.inputTextView.text copy] : nil;
|
||||
}
|
||||
|
||||
#pragma mark - private
|
||||
|
||||
- (UIPickerView*)createFontsPicker {
|
||||
UIPickerView *fontsPicker = [UIPickerView new];
|
||||
fontsPicker.dataSource = self;
|
||||
fontsPicker.delegate = self;
|
||||
fontsPicker.showsSelectionIndicator = YES;
|
||||
return fontsPicker;
|
||||
}
|
||||
|
||||
- (void)createAvailableFonts {
|
||||
NSMutableArray<NSString *> *unsortedFontsArray = [NSMutableArray new];
|
||||
for (NSString *eachFontFamily in UIFont.familyNames) {
|
||||
for (NSString *eachFontName in [UIFont fontNamesForFamilyName:eachFontFamily]) {
|
||||
[unsortedFontsArray addObject:eachFontName];
|
||||
}
|
||||
}
|
||||
self.availableFonts = [NSMutableArray arrayWithArray:[unsortedFontsArray sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)]];
|
||||
}
|
||||
|
||||
#pragma mark - UIPickerViewDataSource
|
||||
|
||||
- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView {
|
||||
return 1;
|
||||
}
|
||||
|
||||
- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component {
|
||||
return self.availableFonts.count;
|
||||
}
|
||||
|
||||
#pragma mark - UIPickerViewDelegate
|
||||
|
||||
- (UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(UIView *)view {
|
||||
UILabel *fontLabel;
|
||||
if (!view) {
|
||||
fontLabel = [UILabel new];
|
||||
fontLabel.backgroundColor = UIColor.clearColor;
|
||||
fontLabel.textAlignment = NSTextAlignmentCenter;
|
||||
} else {
|
||||
fontLabel = (UILabel*)view;
|
||||
}
|
||||
UIFont *font = [UIFont fontWithName:self.availableFonts[row] size:15.0];
|
||||
NSDictionary<NSString *, id> *attributesDictionary = [NSDictionary<NSString *, id> dictionaryWithObject:font forKey:NSFontAttributeName];
|
||||
NSAttributedString *attributesString = [[NSAttributedString alloc] initWithString:self.availableFonts[row] attributes:attributesDictionary];
|
||||
fontLabel.attributedText = attributesString;
|
||||
[fontLabel sizeToFit];
|
||||
return fontLabel;
|
||||
}
|
||||
|
||||
- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component {
|
||||
self.inputTextView.text = self.availableFonts[row];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// FLEXArgumentInputNotSupportedView.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/18/14.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputTextView.h"
|
||||
|
||||
@interface FLEXArgumentInputNotSupportedView : FLEXArgumentInputTextView
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,25 @@
|
||||
//
|
||||
// FLEXArgumentInputNotSupportedView.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/18/14.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputNotSupportedView.h"
|
||||
#import "FLEXColor.h"
|
||||
|
||||
@implementation FLEXArgumentInputNotSupportedView
|
||||
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
|
||||
self = [super initWithArgumentTypeEncoding:typeEncoding];
|
||||
if (self) {
|
||||
self.inputTextView.userInteractionEnabled = NO;
|
||||
self.inputTextView.backgroundColor = [FLEXColor secondaryGroupedBackgroundColorWithAlpha:0.5];
|
||||
self.inputPlaceholderText = @"nil (type not supported)";
|
||||
self.targetSize = FLEXArgumentInputViewSizeSmall;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// FLEXArgumentInputNumberView.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/15/14.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputTextView.h"
|
||||
|
||||
@interface FLEXArgumentInputNumberView : FLEXArgumentInputTextView
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,62 @@
|
||||
//
|
||||
// FLEXArgumentInputNumberView.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/15/14.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputNumberView.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
|
||||
@implementation FLEXArgumentInputNumberView
|
||||
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
|
||||
self = [super initWithArgumentTypeEncoding:typeEncoding];
|
||||
if (self) {
|
||||
self.inputTextView.keyboardType = UIKeyboardTypeNumbersAndPunctuation;
|
||||
self.targetSize = FLEXArgumentInputViewSizeSmall;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setInputValue:(id)inputValue {
|
||||
if ([inputValue respondsToSelector:@selector(stringValue)]) {
|
||||
self.inputTextView.text = [inputValue stringValue];
|
||||
}
|
||||
}
|
||||
|
||||
- (id)inputValue {
|
||||
return [FLEXRuntimeUtility valueForNumberWithObjCType:self.typeEncoding.UTF8String fromInputString:self.inputTextView.text];
|
||||
}
|
||||
|
||||
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
|
||||
NSParameterAssert(type);
|
||||
|
||||
static NSArray<NSString *> *supportedTypes = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
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 && [supportedTypes containsObject:@(type)];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// FLEXArgumentInputObjectView.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/15/14.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputTextView.h"
|
||||
|
||||
@interface FLEXArgumentInputObjectView : FLEXArgumentInputTextView
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,232 @@
|
||||
//
|
||||
// FLEXArgumentInputJSONObjectView.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/15/14.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputObjectView.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
|
||||
static const CGFloat kSegmentInputMargin = 10;
|
||||
|
||||
typedef NS_ENUM(NSUInteger, FLEXArgInputObjectType) {
|
||||
FLEXArgInputObjectTypeJSON,
|
||||
FLEXArgInputObjectTypeAddress
|
||||
};
|
||||
|
||||
@interface FLEXArgumentInputObjectView ()
|
||||
|
||||
@property (nonatomic) UISegmentedControl *objectTypeSegmentControl;
|
||||
@property (nonatomic) FLEXArgInputObjectType inputType;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXArgumentInputObjectView
|
||||
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
|
||||
self = [super initWithArgumentTypeEncoding:typeEncoding];
|
||||
if (self) {
|
||||
// Start with the numbers and punctuation keyboard since quotes, curly braces, or
|
||||
// square brackets are likely to be the first characters type for the JSON.
|
||||
self.inputTextView.keyboardType = UIKeyboardTypeNumbersAndPunctuation;
|
||||
self.targetSize = FLEXArgumentInputViewSizeLarge;
|
||||
|
||||
self.objectTypeSegmentControl = [[UISegmentedControl alloc] initWithItems:@[@"Value", @"Address"]];
|
||||
[self.objectTypeSegmentControl addTarget:self action:@selector(didChangeType) forControlEvents:UIControlEventValueChanged];
|
||||
self.objectTypeSegmentControl.selectedSegmentIndex = 0;
|
||||
[self addSubview:self.objectTypeSegmentControl];
|
||||
|
||||
self.inputType = [[self class] preferredDefaultTypeForObjCType:typeEncoding withCurrentValue:nil];
|
||||
self.objectTypeSegmentControl.selectedSegmentIndex = self.inputType;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)didChangeType {
|
||||
self.inputType = self.objectTypeSegmentControl.selectedSegmentIndex;
|
||||
|
||||
if (super.inputValue) {
|
||||
// Trigger an update to the text field to show
|
||||
// the address of the stored object we were given,
|
||||
// or to show a JSON representation of the object
|
||||
[self populateTextAreaFromValue:super.inputValue];
|
||||
} else {
|
||||
// Clear the text field
|
||||
[self populateTextAreaFromValue:nil];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setInputType:(FLEXArgInputObjectType)inputType {
|
||||
if (_inputType == inputType) return;
|
||||
|
||||
_inputType = inputType;
|
||||
|
||||
// Resize input view
|
||||
switch (inputType) {
|
||||
case FLEXArgInputObjectTypeJSON:
|
||||
self.targetSize = FLEXArgumentInputViewSizeLarge;
|
||||
break;
|
||||
case FLEXArgInputObjectTypeAddress:
|
||||
self.targetSize = FLEXArgumentInputViewSizeSmall;
|
||||
break;
|
||||
}
|
||||
|
||||
// Change placeholder
|
||||
switch (inputType) {
|
||||
case FLEXArgInputObjectTypeJSON:
|
||||
self.inputPlaceholderText =
|
||||
@"You can put any valid JSON here, such as a string, number, array, or dictionary:"
|
||||
"\n\"This is a string\""
|
||||
"\n1234"
|
||||
"\n{ \"name\": \"Bob\", \"age\": 47 }"
|
||||
"\n["
|
||||
"\n 1, 2, 3"
|
||||
"\n]";
|
||||
break;
|
||||
case FLEXArgInputObjectTypeAddress:
|
||||
self.inputPlaceholderText = @"0x0000deadb33f";
|
||||
break;
|
||||
}
|
||||
|
||||
[self setNeedsLayout];
|
||||
[self.superview setNeedsLayout];
|
||||
}
|
||||
|
||||
- (void)setInputValue:(id)inputValue {
|
||||
super.inputValue = inputValue;
|
||||
[self populateTextAreaFromValue:inputValue];
|
||||
}
|
||||
|
||||
- (id)inputValue {
|
||||
switch (self.inputType) {
|
||||
case FLEXArgInputObjectTypeJSON:
|
||||
return [FLEXRuntimeUtility objectValueFromEditableJSONString:self.inputTextView.text];
|
||||
case FLEXArgInputObjectTypeAddress: {
|
||||
NSScanner *scanner = [NSScanner scannerWithString:self.inputTextView.text];
|
||||
|
||||
unsigned long long objectPointerValue;
|
||||
if ([scanner scanHexLongLong:&objectPointerValue]) {
|
||||
return (__bridge id)(void *)objectPointerValue;
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)populateTextAreaFromValue:(id)value {
|
||||
if (!value) {
|
||||
self.inputTextView.text = nil;
|
||||
} else {
|
||||
if (self.inputType == FLEXArgInputObjectTypeJSON) {
|
||||
self.inputTextView.text = [FLEXRuntimeUtility editableJSONStringForObject:value];
|
||||
} else if (self.inputType == FLEXArgInputObjectTypeAddress) {
|
||||
self.inputTextView.text = [NSString stringWithFormat:@"%p", value];
|
||||
}
|
||||
}
|
||||
|
||||
// Delegate methods are not called for programmatic changes
|
||||
[self textViewDidChange:self.inputTextView];
|
||||
}
|
||||
|
||||
- (CGSize)sizeThatFits:(CGSize)size {
|
||||
CGSize fitSize = [super sizeThatFits:size];
|
||||
fitSize.height += [self.objectTypeSegmentControl sizeThatFits:size].height + kSegmentInputMargin;
|
||||
|
||||
return fitSize;
|
||||
}
|
||||
|
||||
- (void)layoutSubviews {
|
||||
CGFloat segmentHeight = [self.objectTypeSegmentControl sizeThatFits:self.frame.size].height;
|
||||
self.objectTypeSegmentControl.frame = CGRectMake(
|
||||
0.0,
|
||||
// Our segmented control is taking the position
|
||||
// of the text view, as far as super is concerned,
|
||||
// and we override this property to be different
|
||||
super.topInputFieldVerticalLayoutGuide,
|
||||
self.frame.size.width,
|
||||
segmentHeight
|
||||
);
|
||||
|
||||
[super layoutSubviews];
|
||||
}
|
||||
|
||||
- (CGFloat)topInputFieldVerticalLayoutGuide {
|
||||
// Our text view is offset from the segmented control
|
||||
CGFloat segmentHeight = [self.objectTypeSegmentControl sizeThatFits:self.frame.size].height;
|
||||
return segmentHeight + super.topInputFieldVerticalLayoutGuide + kSegmentInputMargin;
|
||||
}
|
||||
|
||||
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
|
||||
NSParameterAssert(type);
|
||||
// Must be object type
|
||||
return type[0] == FLEXTypeEncodingObjcObject || type[0] == FLEXTypeEncodingObjcClass;
|
||||
}
|
||||
|
||||
+ (FLEXArgInputObjectType)preferredDefaultTypeForObjCType:(const char *)type withCurrentValue:(id)value {
|
||||
NSParameterAssert(type[0] == FLEXTypeEncodingObjcObject || type[0] == FLEXTypeEncodingObjcClass);
|
||||
|
||||
if (value) {
|
||||
// If there's a current value, it must be serializable to JSON
|
||||
// to display the JSON editor. Otherwise display the address field.
|
||||
if ([FLEXRuntimeUtility editableJSONStringForObject:value]) {
|
||||
return FLEXArgInputObjectTypeJSON;
|
||||
} else {
|
||||
return FLEXArgInputObjectTypeAddress;
|
||||
}
|
||||
} else {
|
||||
// Otherwise, see if we have more type information than just 'id'.
|
||||
// If we do, make sure the encoding is something serializable to JSON.
|
||||
// Properties and ivars keep more detailed type encoding information than method arguments.
|
||||
if (strcmp(type, @encode(id)) != 0) {
|
||||
BOOL isJSONSerializableType = NO;
|
||||
|
||||
// Parse class name out of the string,
|
||||
// which is in the form `@"ClassName"`
|
||||
Class cls = NSClassFromString(({
|
||||
NSString *className = nil;
|
||||
NSScanner *scan = [NSScanner scannerWithString:@(type)];
|
||||
NSCharacterSet *allowed = [NSCharacterSet
|
||||
characterSetWithCharactersInString:@"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_$"
|
||||
];
|
||||
|
||||
// Skip over the @" then scan the name
|
||||
if ([scan scanString:@"@\"" intoString:nil]) {
|
||||
[scan scanCharactersFromSet:allowed intoString:&className];
|
||||
}
|
||||
|
||||
className;
|
||||
}));
|
||||
|
||||
// Note: we can't use @encode(NSString) here because that drops
|
||||
// the class information and just goes to @encode(id).
|
||||
NSArray<Class> *jsonTypes = @[
|
||||
[NSString class],
|
||||
[NSNumber class],
|
||||
[NSArray class],
|
||||
[NSDictionary class],
|
||||
];
|
||||
|
||||
// Look for matching types
|
||||
for (Class jsonClass in jsonTypes) {
|
||||
if ([cls isSubclassOfClass:jsonClass]) {
|
||||
isJSONSerializableType = YES;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isJSONSerializableType) {
|
||||
return FLEXArgInputObjectTypeJSON;
|
||||
} else {
|
||||
return FLEXArgInputObjectTypeAddress;
|
||||
}
|
||||
} else {
|
||||
return FLEXArgInputObjectTypeAddress;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// FLEXArgumentInputStringView.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/28/14.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputTextView.h"
|
||||
|
||||
@interface FLEXArgumentInputStringView : FLEXArgumentInputTextView
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,129 @@
|
||||
//
|
||||
// FLEXArgumentInputStringView.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/28/14.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputStringView.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
|
||||
@implementation FLEXArgumentInputStringView
|
||||
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
|
||||
self = [super initWithArgumentTypeEncoding:typeEncoding];
|
||||
if (self) {
|
||||
FLEXTypeEncoding type = typeEncoding[0];
|
||||
if (type == FLEXTypeEncodingConst) {
|
||||
// A crash here would mean an invalid type encoding string
|
||||
type = typeEncoding[1];
|
||||
}
|
||||
|
||||
// Selectors don't need a multi-line text box
|
||||
if (type == FLEXTypeEncodingSelector) {
|
||||
self.targetSize = FLEXArgumentInputViewSizeSmall;
|
||||
} else {
|
||||
self.targetSize = FLEXArgumentInputViewSizeLarge;
|
||||
}
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setInputValue:(id)inputValue {
|
||||
if ([inputValue isKindOfClass:[NSString class]]) {
|
||||
self.inputTextView.text = inputValue;
|
||||
} else if ([inputValue isKindOfClass:[NSValue class]]) {
|
||||
NSValue *value = (id)inputValue;
|
||||
NSParameterAssert(strlen(value.objCType) == 1);
|
||||
|
||||
// C-String or SEL from NSValue
|
||||
FLEXTypeEncoding type = value.objCType[0];
|
||||
if (type == FLEXTypeEncodingConst) {
|
||||
// A crash here would mean an invalid type encoding string
|
||||
type = value.objCType[1];
|
||||
}
|
||||
|
||||
if (type == FLEXTypeEncodingCString) {
|
||||
self.inputTextView.text = @((const char *)value.pointerValue);
|
||||
} else if (type == FLEXTypeEncodingSelector) {
|
||||
self.inputTextView.text = NSStringFromSelector((SEL)value.pointerValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (id)inputValue {
|
||||
NSString *text = self.inputTextView.text;
|
||||
// Interpret empty string as nil. We loose the ability to set empty string as a string value,
|
||||
// but we accept that tradeoff in exchange for not having to type quotes for every string.
|
||||
if (!text.length) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
// Case: C-strings and SELs
|
||||
if (self.typeEncoding.length <= 2) {
|
||||
FLEXTypeEncoding type = [self.typeEncoding characterAtIndex:0];
|
||||
if (type == FLEXTypeEncodingConst) {
|
||||
// A crash here would mean an invalid type encoding string
|
||||
type = [self.typeEncoding characterAtIndex:1];
|
||||
}
|
||||
|
||||
if (type == FLEXTypeEncodingCString || type == FLEXTypeEncodingSelector) {
|
||||
const char *encoding = self.typeEncoding.UTF8String;
|
||||
SEL selector = NSSelectorFromString(text);
|
||||
return [NSValue valueWithBytes:&selector objCType:encoding];
|
||||
}
|
||||
}
|
||||
|
||||
// Case: NSStrings
|
||||
return self.inputTextView.text.copy;
|
||||
}
|
||||
|
||||
// TODO: Support using object address for strings, as in the object arg view.
|
||||
|
||||
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
|
||||
NSParameterAssert(type);
|
||||
unsigned long len = strlen(type);
|
||||
|
||||
BOOL isConst = type[0] == FLEXTypeEncodingConst;
|
||||
NSInteger i = isConst ? 1 : 0;
|
||||
|
||||
BOOL typeIsString = strcmp(type, FLEXEncodeClass(NSString)) == 0;
|
||||
BOOL typeIsCString = len <= 2 && type[i] == FLEXTypeEncodingCString;
|
||||
BOOL typeIsSEL = len <= 2 && type[i] == FLEXTypeEncodingSelector;
|
||||
BOOL valueIsString = [value isKindOfClass:[NSString class]];
|
||||
|
||||
BOOL typeIsPrimitiveString = typeIsSEL || typeIsCString;
|
||||
BOOL typeIsSupported = typeIsString || typeIsCString || typeIsSEL;
|
||||
|
||||
BOOL valueIsNSValueWithCorrectType = NO;
|
||||
if ([value isKindOfClass:[NSValue class]]) {
|
||||
NSValue *v = (id)value;
|
||||
len = strlen(v.objCType);
|
||||
if (len == 1) {
|
||||
FLEXTypeEncoding type = v.objCType[i];
|
||||
if (type == FLEXTypeEncodingCString && typeIsCString) {
|
||||
valueIsNSValueWithCorrectType = YES;
|
||||
} else if (type == FLEXTypeEncodingSelector && typeIsSEL) {
|
||||
valueIsNSValueWithCorrectType = YES;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!value && typeIsSupported) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
if (typeIsString && valueIsString) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
// Primitive strings can be input as NSStrings or NSValues
|
||||
if (typeIsPrimitiveString && (valueIsString || valueIsNSValueWithCorrectType)) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// FLEXArgumentInputStructView.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/16/14.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputView.h"
|
||||
|
||||
@interface FLEXArgumentInputStructView : FLEXArgumentInputView
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,220 @@
|
||||
//
|
||||
// FLEXArgumentInputStructView.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/16/14.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputStructView.h"
|
||||
#import "FLEXArgumentInputViewFactory.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
#import "FLEXTypeEncodingParser.h"
|
||||
|
||||
@interface FLEXArgumentInputStructView ()
|
||||
|
||||
@property (nonatomic) NSArray<FLEXArgumentInputView *> *argumentInputViews;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXArgumentInputStructView
|
||||
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
|
||||
self = [super initWithArgumentTypeEncoding:typeEncoding];
|
||||
if (self) {
|
||||
NSMutableArray<FLEXArgumentInputView *> *inputViews = [NSMutableArray new];
|
||||
NSArray<NSString *> *customTitles = [[self class] customFieldTitlesForTypeEncoding:typeEncoding];
|
||||
[FLEXRuntimeUtility enumerateTypesInStructEncoding:typeEncoding usingBlock:^(NSString *structName,
|
||||
const char *fieldTypeEncoding,
|
||||
NSString *prettyTypeEncoding,
|
||||
NSUInteger fieldIndex,
|
||||
NSUInteger fieldOffset) {
|
||||
|
||||
FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:fieldTypeEncoding];
|
||||
inputView.targetSize = FLEXArgumentInputViewSizeSmall;
|
||||
|
||||
if (fieldIndex < customTitles.count) {
|
||||
inputView.title = customTitles[fieldIndex];
|
||||
} else {
|
||||
inputView.title = [NSString stringWithFormat:@"%@ field %lu (%@)",
|
||||
structName, (unsigned long)fieldIndex, prettyTypeEncoding
|
||||
];
|
||||
}
|
||||
|
||||
[inputViews addObject:inputView];
|
||||
[self addSubview:inputView];
|
||||
}];
|
||||
self.argumentInputViews = inputViews;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Superclass Overrides
|
||||
|
||||
- (void)setBackgroundColor:(UIColor *)backgroundColor {
|
||||
[super setBackgroundColor:backgroundColor];
|
||||
for (FLEXArgumentInputView *inputView in self.argumentInputViews) {
|
||||
inputView.backgroundColor = backgroundColor;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setInputValue:(id)inputValue {
|
||||
if ([inputValue isKindOfClass:[NSValue class]]) {
|
||||
const char *structTypeEncoding = [inputValue objCType];
|
||||
if (strcmp(self.typeEncoding.UTF8String, structTypeEncoding) == 0) {
|
||||
NSUInteger valueSize = 0;
|
||||
|
||||
if (FLEXGetSizeAndAlignment(structTypeEncoding, &valueSize, NULL)) {
|
||||
void *unboxedValue = malloc(valueSize);
|
||||
[inputValue getValue:unboxedValue];
|
||||
[FLEXRuntimeUtility enumerateTypesInStructEncoding:structTypeEncoding usingBlock:^(NSString *structName,
|
||||
const char *fieldTypeEncoding,
|
||||
NSString *prettyTypeEncoding,
|
||||
NSUInteger fieldIndex,
|
||||
NSUInteger fieldOffset) {
|
||||
|
||||
void *fieldPointer = unboxedValue + fieldOffset;
|
||||
FLEXArgumentInputView *inputView = self.argumentInputViews[fieldIndex];
|
||||
|
||||
if (fieldTypeEncoding[0] == FLEXTypeEncodingObjcObject || fieldTypeEncoding[0] == FLEXTypeEncodingObjcClass) {
|
||||
inputView.inputValue = (__bridge id)fieldPointer;
|
||||
} else {
|
||||
NSValue *boxedField = [FLEXRuntimeUtility valueForPrimitivePointer:fieldPointer objCType:fieldTypeEncoding];
|
||||
inputView.inputValue = boxedField;
|
||||
}
|
||||
}];
|
||||
free(unboxedValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (id)inputValue {
|
||||
NSValue *boxedStruct = nil;
|
||||
const char *structTypeEncoding = self.typeEncoding.UTF8String;
|
||||
NSUInteger structSize = 0;
|
||||
|
||||
if (FLEXGetSizeAndAlignment(structTypeEncoding, &structSize, NULL)) {
|
||||
void *unboxedStruct = malloc(structSize);
|
||||
[FLEXRuntimeUtility enumerateTypesInStructEncoding:structTypeEncoding usingBlock:^(NSString *structName,
|
||||
const char *fieldTypeEncoding,
|
||||
NSString *prettyTypeEncoding,
|
||||
NSUInteger fieldIndex,
|
||||
NSUInteger fieldOffset) {
|
||||
|
||||
void *fieldPointer = unboxedStruct + fieldOffset;
|
||||
FLEXArgumentInputView *inputView = self.argumentInputViews[fieldIndex];
|
||||
|
||||
if (fieldTypeEncoding[0] == FLEXTypeEncodingObjcObject || fieldTypeEncoding[0] == FLEXTypeEncodingObjcClass) {
|
||||
// Object fields
|
||||
memcpy(fieldPointer, (__bridge void *)inputView.inputValue, sizeof(id));
|
||||
} else {
|
||||
// Boxed primitive/struct fields
|
||||
id inputValue = inputView.inputValue;
|
||||
if ([inputValue isKindOfClass:[NSValue class]] && strcmp([inputValue objCType], fieldTypeEncoding) == 0) {
|
||||
[inputValue getValue:fieldPointer];
|
||||
}
|
||||
}
|
||||
}];
|
||||
|
||||
boxedStruct = [NSValue value:unboxedStruct withObjCType:structTypeEncoding];
|
||||
free(unboxedStruct);
|
||||
}
|
||||
|
||||
return boxedStruct;
|
||||
}
|
||||
|
||||
- (BOOL)inputViewIsFirstResponder {
|
||||
BOOL isFirstResponder = NO;
|
||||
for (FLEXArgumentInputView *inputView in self.argumentInputViews) {
|
||||
if ([inputView inputViewIsFirstResponder]) {
|
||||
isFirstResponder = YES;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return isFirstResponder;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Layout and Sizing
|
||||
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
|
||||
CGFloat runningOriginY = self.topInputFieldVerticalLayoutGuide;
|
||||
|
||||
for (FLEXArgumentInputView *inputView in self.argumentInputViews) {
|
||||
CGSize inputFitSize = [inputView sizeThatFits:self.bounds.size];
|
||||
inputView.frame = CGRectMake(0, runningOriginY, inputFitSize.width, inputFitSize.height);
|
||||
runningOriginY = CGRectGetMaxY(inputView.frame) + [[self class] verticalPaddingBetweenFields];
|
||||
}
|
||||
}
|
||||
|
||||
+ (CGFloat)verticalPaddingBetweenFields {
|
||||
return 10.0;
|
||||
}
|
||||
|
||||
- (CGSize)sizeThatFits:(CGSize)size {
|
||||
CGSize fitSize = [super sizeThatFits:size];
|
||||
|
||||
CGSize constrainSize = CGSizeMake(size.width, CGFLOAT_MAX);
|
||||
CGFloat height = fitSize.height;
|
||||
|
||||
for (FLEXArgumentInputView *inputView in self.argumentInputViews) {
|
||||
height += [inputView sizeThatFits:constrainSize].height;
|
||||
height += [[self class] verticalPaddingBetweenFields];
|
||||
}
|
||||
|
||||
return CGSizeMake(fitSize.width, height);
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Class Helpers
|
||||
|
||||
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
|
||||
NSParameterAssert(type);
|
||||
if (type[0] == FLEXTypeEncodingStructBegin) {
|
||||
return FLEXGetSizeAndAlignment(type, nil, nil);
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
+ (NSArray<NSString *> *)customFieldTitlesForTypeEncoding:(const char *)typeEncoding {
|
||||
NSArray<NSString *> *customTitles = nil;
|
||||
if (strcmp(typeEncoding, @encode(CGRect)) == 0) {
|
||||
customTitles = @[@"CGPoint origin", @"CGSize size"];
|
||||
} else if (strcmp(typeEncoding, @encode(CGPoint)) == 0) {
|
||||
customTitles = @[@"CGFloat x", @"CGFloat y"];
|
||||
} else if (strcmp(typeEncoding, @encode(CGSize)) == 0) {
|
||||
customTitles = @[@"CGFloat width", @"CGFloat height"];
|
||||
} else if (strcmp(typeEncoding, @encode(CGVector)) == 0) {
|
||||
customTitles = @[@"CGFloat dx", @"CGFloat dy"];
|
||||
} else if (strcmp(typeEncoding, @encode(UIEdgeInsets)) == 0) {
|
||||
customTitles = @[@"CGFloat top", @"CGFloat left", @"CGFloat bottom", @"CGFloat right"];
|
||||
} else if (strcmp(typeEncoding, @encode(UIOffset)) == 0) {
|
||||
customTitles = @[@"CGFloat horizontal", @"CGFloat vertical"];
|
||||
} else if (strcmp(typeEncoding, @encode(NSRange)) == 0) {
|
||||
customTitles = @[@"NSUInteger location", @"NSUInteger length"];
|
||||
} else if (strcmp(typeEncoding, @encode(CATransform3D)) == 0) {
|
||||
customTitles = @[@"CGFloat m11", @"CGFloat m12", @"CGFloat m13", @"CGFloat m14",
|
||||
@"CGFloat m21", @"CGFloat m22", @"CGFloat m23", @"CGFloat m24",
|
||||
@"CGFloat m31", @"CGFloat m32", @"CGFloat m33", @"CGFloat m34",
|
||||
@"CGFloat m41", @"CGFloat m42", @"CGFloat m43", @"CGFloat m44"];
|
||||
} else if (strcmp(typeEncoding, @encode(CGAffineTransform)) == 0) {
|
||||
customTitles = @[@"CGFloat a", @"CGFloat b",
|
||||
@"CGFloat c", @"CGFloat d",
|
||||
@"CGFloat tx", @"CGFloat ty"];
|
||||
} else {
|
||||
if (@available(iOS 11.0, *)) {
|
||||
if (strcmp(typeEncoding, @encode(NSDirectionalEdgeInsets)) == 0) {
|
||||
customTitles = @[@"CGFloat top", @"CGFloat leading",
|
||||
@"CGFloat bottom", @"CGFloat trailing"];
|
||||
}
|
||||
}
|
||||
}
|
||||
return customTitles;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// FLEXArgumentInputSwitchView.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/16/14.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputView.h"
|
||||
|
||||
@interface FLEXArgumentInputSwitchView : FLEXArgumentInputView
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,81 @@
|
||||
//
|
||||
// FLEXArgumentInputSwitchView.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 6/16/14.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputSwitchView.h"
|
||||
|
||||
@interface FLEXArgumentInputSwitchView ()
|
||||
|
||||
@property (nonatomic) UISwitch *inputSwitch;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXArgumentInputSwitchView
|
||||
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
|
||||
self = [super initWithArgumentTypeEncoding:typeEncoding];
|
||||
if (self) {
|
||||
self.inputSwitch = [UISwitch new];
|
||||
[self.inputSwitch addTarget:self action:@selector(switchValueDidChange:) forControlEvents:UIControlEventValueChanged];
|
||||
[self.inputSwitch sizeToFit];
|
||||
[self addSubview:self.inputSwitch];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark Input/Output
|
||||
|
||||
- (void)setInputValue:(id)inputValue {
|
||||
BOOL on = NO;
|
||||
if ([inputValue isKindOfClass:[NSNumber class]]) {
|
||||
NSNumber *number = (NSNumber *)inputValue;
|
||||
on = [number boolValue];
|
||||
} else if ([inputValue isKindOfClass:[NSValue class]]) {
|
||||
NSValue *value = (NSValue *)inputValue;
|
||||
if (strcmp([value objCType], @encode(BOOL)) == 0) {
|
||||
[value getValue:&on];
|
||||
}
|
||||
}
|
||||
self.inputSwitch.on = on;
|
||||
}
|
||||
|
||||
- (id)inputValue {
|
||||
BOOL isOn = [self.inputSwitch isOn];
|
||||
NSValue *boxedBool = [NSValue value:&isOn withObjCType:@encode(BOOL)];
|
||||
return boxedBool;
|
||||
}
|
||||
|
||||
- (void)switchValueDidChange:(id)sender {
|
||||
[self.delegate argumentInputViewValueDidChange:self];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Layout and Sizing
|
||||
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
|
||||
self.inputSwitch.frame = CGRectMake(0, self.topInputFieldVerticalLayoutGuide, self.inputSwitch.frame.size.width, self.inputSwitch.frame.size.height);
|
||||
}
|
||||
|
||||
- (CGSize)sizeThatFits:(CGSize)size {
|
||||
CGSize fitSize = [super sizeThatFits:size];
|
||||
fitSize.height += self.inputSwitch.frame.size.height;
|
||||
return fitSize;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Class Helpers
|
||||
|
||||
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
|
||||
NSParameterAssert(type);
|
||||
// Only BOOLs. Current value is irrelevant.
|
||||
return strcmp(type, @encode(BOOL)) == 0;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// FLEXArgumentInputTextView.h
|
||||
// FLEXInjected
|
||||
//
|
||||
// Created by Ryan Olson on 6/15/14.
|
||||
//
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputView.h"
|
||||
|
||||
@interface FLEXArgumentInputTextView : FLEXArgumentInputView <UITextViewDelegate>
|
||||
|
||||
// For subclass eyes only
|
||||
|
||||
@property (nonatomic, readonly) UITextView *inputTextView;
|
||||
@property (nonatomic) NSString *inputPlaceholderText;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,155 @@
|
||||
//
|
||||
// FLEXArgumentInputTextView.m
|
||||
// FLEXInjected
|
||||
//
|
||||
// Created by Ryan Olson on 6/15/14.
|
||||
//
|
||||
//
|
||||
|
||||
#import "FLEXColor.h"
|
||||
#import "FLEXArgumentInputTextView.h"
|
||||
#import "FLEXUtility.h"
|
||||
|
||||
@interface FLEXArgumentInputTextView ()
|
||||
|
||||
@property (nonatomic) UITextView *inputTextView;
|
||||
@property (nonatomic) UILabel *placeholderLabel;
|
||||
@property (nonatomic, readonly) NSUInteger numberOfInputLines;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXArgumentInputTextView
|
||||
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
|
||||
self = [super initWithArgumentTypeEncoding:typeEncoding];
|
||||
if (self) {
|
||||
self.inputTextView = [UITextView new];
|
||||
self.inputTextView.font = [[self class] inputFont];
|
||||
self.inputTextView.backgroundColor = FLEXColor.secondaryGroupedBackgroundColor;
|
||||
self.inputTextView.layer.cornerRadius = 10.f;
|
||||
self.inputTextView.contentInset = UIEdgeInsetsMake(0, 5, 0, 0);
|
||||
self.inputTextView.autocapitalizationType = UITextAutocapitalizationTypeNone;
|
||||
self.inputTextView.autocorrectionType = UITextAutocorrectionTypeNo;
|
||||
self.inputTextView.delegate = self;
|
||||
self.inputTextView.inputAccessoryView = [self createToolBar];
|
||||
if (@available(iOS 11, *)) {
|
||||
self.inputTextView.smartQuotesType = UITextSmartQuotesTypeNo;
|
||||
[self.inputTextView.layer setValue:@YES forKey:@"continuousCorners"];
|
||||
} else {
|
||||
self.inputTextView.layer.borderWidth = 1.f;
|
||||
self.inputTextView.layer.borderColor = FLEXColor.borderColor.CGColor;
|
||||
}
|
||||
|
||||
self.placeholderLabel = [UILabel new];
|
||||
self.placeholderLabel.font = self.inputTextView.font;
|
||||
self.placeholderLabel.textColor = FLEXColor.deemphasizedTextColor;
|
||||
self.placeholderLabel.numberOfLines = 0;
|
||||
|
||||
[self addSubview:self.inputTextView];
|
||||
[self.inputTextView addSubview:self.placeholderLabel];
|
||||
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
- (UIToolbar *)createToolBar {
|
||||
UIToolbar *toolBar = [UIToolbar new];
|
||||
[toolBar sizeToFit];
|
||||
UIBarButtonItem *spaceItem = [[UIBarButtonItem alloc]
|
||||
initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace
|
||||
target:nil action:nil
|
||||
];
|
||||
UIBarButtonItem *pasteItem = [[UIBarButtonItem alloc]
|
||||
initWithTitle:@"Paste" style:UIBarButtonItemStyleDone
|
||||
target:self.inputTextView action:@selector(paste:)
|
||||
];
|
||||
UIBarButtonItem *doneItem = [[UIBarButtonItem alloc]
|
||||
initWithBarButtonSystemItem:UIBarButtonSystemItemDone
|
||||
target:self.inputTextView action:@selector(resignFirstResponder)
|
||||
];
|
||||
toolBar.items = @[spaceItem, pasteItem, doneItem];
|
||||
return toolBar;
|
||||
}
|
||||
|
||||
- (void)setInputPlaceholderText:(NSString *)placeholder {
|
||||
self.placeholderLabel.text = placeholder;
|
||||
if (placeholder.length) {
|
||||
if (!self.inputTextView.text.length) {
|
||||
self.placeholderLabel.hidden = NO;
|
||||
} else {
|
||||
self.placeholderLabel.hidden = YES;
|
||||
}
|
||||
} else {
|
||||
self.placeholderLabel.hidden = YES;
|
||||
}
|
||||
|
||||
[self setNeedsLayout];
|
||||
}
|
||||
|
||||
- (NSString *)inputPlaceholderText {
|
||||
return self.placeholderLabel.text;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Superclass Overrides
|
||||
|
||||
- (BOOL)inputViewIsFirstResponder {
|
||||
return self.inputTextView.isFirstResponder;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Layout and Sizing
|
||||
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
|
||||
self.inputTextView.frame = CGRectMake(0, self.topInputFieldVerticalLayoutGuide, self.bounds.size.width, [self inputTextViewHeight]);
|
||||
// Placeholder label is positioned by insetting then origin
|
||||
// by the content inset then the text container inset
|
||||
CGSize s = self.inputTextView.frame.size;
|
||||
self.placeholderLabel.frame = CGRectMake(0, 0, s.width, s.height);
|
||||
self.placeholderLabel.frame = UIEdgeInsetsInsetRect(
|
||||
UIEdgeInsetsInsetRect(self.placeholderLabel.frame, self.inputTextView.contentInset),
|
||||
self.inputTextView.textContainerInset
|
||||
);
|
||||
}
|
||||
|
||||
- (NSUInteger)numberOfInputLines {
|
||||
switch (self.targetSize) {
|
||||
case FLEXArgumentInputViewSizeDefault:
|
||||
return 2;
|
||||
case FLEXArgumentInputViewSizeSmall:
|
||||
return 1;
|
||||
case FLEXArgumentInputViewSizeLarge:
|
||||
return 8;
|
||||
}
|
||||
}
|
||||
|
||||
- (CGFloat)inputTextViewHeight {
|
||||
return ceil([[self class] inputFont].lineHeight * self.numberOfInputLines) + 16.0;
|
||||
}
|
||||
|
||||
- (CGSize)sizeThatFits:(CGSize)size {
|
||||
CGSize fitSize = [super sizeThatFits:size];
|
||||
fitSize.height += [self inputTextViewHeight];
|
||||
return fitSize;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Class Helpers
|
||||
|
||||
+ (UIFont *)inputFont {
|
||||
return [UIFont systemFontOfSize:14.0];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - UITextViewDelegate
|
||||
|
||||
- (void)textViewDidChange:(UITextView *)textView {
|
||||
[self.delegate argumentInputViewValueDidChange:self];
|
||||
self.placeholderLabel.hidden = !(self.inputPlaceholderText.length && !textView.text.length);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,64 @@
|
||||
//
|
||||
// FLEXArgumentInputView.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 5/30/14.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
typedef NS_ENUM(NSUInteger, FLEXArgumentInputViewSize) {
|
||||
/// 2 lines, medium-sized
|
||||
FLEXArgumentInputViewSizeDefault = 0,
|
||||
/// One line
|
||||
FLEXArgumentInputViewSizeSmall,
|
||||
/// Several lines
|
||||
FLEXArgumentInputViewSizeLarge
|
||||
};
|
||||
|
||||
@protocol FLEXArgumentInputViewDelegate;
|
||||
|
||||
@interface FLEXArgumentInputView : UIView
|
||||
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding;
|
||||
|
||||
/// The name of the field. Optional (can be nil).
|
||||
@property (nonatomic, copy) NSString *title;
|
||||
|
||||
/// To populate the filed with an initial value, set this property.
|
||||
/// To reteive the value input by the user, access the property.
|
||||
/// Primitive types and structs should/will be boxed in NSValue containers.
|
||||
/// Concrete subclasses should override both the setter and getter for this property.
|
||||
/// Subclasses can call super.inputValue to access a backing store for the value.
|
||||
@property (nonatomic) id inputValue;
|
||||
|
||||
/// Setting this value to large will make some argument input views increase the size of their input field(s).
|
||||
/// Useful to increase the use of space if there is only one input view on screen (i.e. for property and ivar editing).
|
||||
@property (nonatomic) FLEXArgumentInputViewSize targetSize;
|
||||
|
||||
/// Users of the input view can get delegate callbacks for incremental changes in user input.
|
||||
@property (nonatomic, weak) id <FLEXArgumentInputViewDelegate> delegate;
|
||||
|
||||
// Subclasses can override
|
||||
|
||||
/// If the input view has one or more text views, returns YES when one of them is focused.
|
||||
@property (nonatomic, readonly) BOOL inputViewIsFirstResponder;
|
||||
|
||||
/// For subclasses to indicate that they can handle editing a field the give type and value.
|
||||
/// Used by FLEXArgumentInputViewFactory to create appropriate input views.
|
||||
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value;
|
||||
|
||||
// For subclass eyes only
|
||||
|
||||
@property (nonatomic, readonly) UILabel *titleLabel;
|
||||
@property (nonatomic, readonly) NSString *typeEncoding;
|
||||
@property (nonatomic, readonly) CGFloat topInputFieldVerticalLayoutGuide;
|
||||
|
||||
@end
|
||||
|
||||
@protocol FLEXArgumentInputViewDelegate <NSObject>
|
||||
|
||||
- (void)argumentInputViewValueDidChange:(FLEXArgumentInputView *)argumentInputView;
|
||||
|
||||
@end
|
||||
114
Tweaks/FLEX/Editing/ArgumentInputViews/FLEXArgumentInputView.m
Normal file
114
Tweaks/FLEX/Editing/ArgumentInputViews/FLEXArgumentInputView.m
Normal file
@@ -0,0 +1,114 @@
|
||||
//
|
||||
// FLEXArgumentInputView.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 5/30/14.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputView.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXColor.h"
|
||||
|
||||
@interface FLEXArgumentInputView ()
|
||||
|
||||
@property (nonatomic) UILabel *titleLabel;
|
||||
@property (nonatomic) NSString *typeEncoding;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXArgumentInputView
|
||||
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
|
||||
self = [super initWithFrame:CGRectZero];
|
||||
if (self) {
|
||||
self.typeEncoding = typeEncoding != NULL ? @(typeEncoding) : nil;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
|
||||
if (self.showsTitle) {
|
||||
CGSize constrainSize = CGSizeMake(self.bounds.size.width, CGFLOAT_MAX);
|
||||
CGSize labelSize = [self.titleLabel sizeThatFits:constrainSize];
|
||||
self.titleLabel.frame = CGRectMake(0, 0, labelSize.width, labelSize.height);
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setBackgroundColor:(UIColor *)backgroundColor {
|
||||
[super setBackgroundColor:backgroundColor];
|
||||
self.titleLabel.backgroundColor = backgroundColor;
|
||||
}
|
||||
|
||||
- (void)setTitle:(NSString *)title {
|
||||
if (![_title isEqual:title]) {
|
||||
_title = title;
|
||||
self.titleLabel.text = title;
|
||||
[self setNeedsLayout];
|
||||
}
|
||||
}
|
||||
|
||||
- (UILabel *)titleLabel {
|
||||
if (!_titleLabel) {
|
||||
_titleLabel = [UILabel new];
|
||||
_titleLabel.font = [[self class] titleFont];
|
||||
_titleLabel.textColor = FLEXColor.primaryTextColor;
|
||||
_titleLabel.numberOfLines = 0;
|
||||
[self addSubview:_titleLabel];
|
||||
}
|
||||
return _titleLabel;
|
||||
}
|
||||
|
||||
- (BOOL)showsTitle {
|
||||
return self.title.length > 0;
|
||||
}
|
||||
|
||||
- (CGFloat)topInputFieldVerticalLayoutGuide {
|
||||
CGFloat verticalLayoutGuide = 0;
|
||||
if (self.showsTitle) {
|
||||
CGFloat titleHeight = [self.titleLabel sizeThatFits:self.bounds.size].height;
|
||||
verticalLayoutGuide = titleHeight + [[self class] titleBottomPadding];
|
||||
}
|
||||
return verticalLayoutGuide;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Subclasses Can Override
|
||||
|
||||
- (BOOL)inputViewIsFirstResponder {
|
||||
return NO;
|
||||
}
|
||||
|
||||
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
|
||||
return NO;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Class Helpers
|
||||
|
||||
+ (UIFont *)titleFont {
|
||||
return [UIFont systemFontOfSize:12.0];
|
||||
}
|
||||
|
||||
+ (CGFloat)titleBottomPadding {
|
||||
return 4.0;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Sizing
|
||||
|
||||
- (CGSize)sizeThatFits:(CGSize)size {
|
||||
CGFloat height = 0;
|
||||
|
||||
if (self.title.length > 0) {
|
||||
CGSize constrainSize = CGSizeMake(size.width, CGFLOAT_MAX);
|
||||
height += ceil([self.titleLabel sizeThatFits:constrainSize].height);
|
||||
height += [[self class] titleBottomPadding];
|
||||
}
|
||||
|
||||
return CGSizeMake(size.width, height);
|
||||
}
|
||||
|
||||
@end
|
||||
24
Tweaks/FLEX/Editing/FLEXArgumentInputViewFactory.h
Normal file
24
Tweaks/FLEX/Editing/FLEXArgumentInputViewFactory.h
Normal file
@@ -0,0 +1,24 @@
|
||||
//
|
||||
// FLEXArgumentInputViewFactory.h
|
||||
// FLEXInjected
|
||||
//
|
||||
// Created by Ryan Olson on 6/15/14.
|
||||
//
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "FLEXArgumentInputSwitchView.h"
|
||||
|
||||
@interface FLEXArgumentInputViewFactory : NSObject
|
||||
|
||||
/// Forwards to argumentInputViewForTypeEncoding:currentValue: with a nil currentValue.
|
||||
+ (FLEXArgumentInputView *)argumentInputViewForTypeEncoding:(const char *)typeEncoding;
|
||||
|
||||
/// The main factory method for making argument input view subclasses that are the best fit for the type.
|
||||
+ (FLEXArgumentInputView *)argumentInputViewForTypeEncoding:(const char *)typeEncoding currentValue:(id)currentValue;
|
||||
|
||||
/// A way to check if we should try editing a filed given its type encoding and value.
|
||||
/// Useful when deciding whether to edit or explore a property, ivar, or NSUserDefaults value.
|
||||
+ (BOOL)canEditFieldWithTypeEncoding:(const char *)typeEncoding currentValue:(id)currentValue;
|
||||
|
||||
@end
|
||||
70
Tweaks/FLEX/Editing/FLEXArgumentInputViewFactory.m
Normal file
70
Tweaks/FLEX/Editing/FLEXArgumentInputViewFactory.m
Normal file
@@ -0,0 +1,70 @@
|
||||
//
|
||||
// FLEXArgumentInputViewFactory.m
|
||||
// FLEXInjected
|
||||
//
|
||||
// Created by Ryan Olson on 6/15/14.
|
||||
//
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputViewFactory.h"
|
||||
#import "FLEXArgumentInputView.h"
|
||||
#import "FLEXArgumentInputObjectView.h"
|
||||
#import "FLEXArgumentInputNumberView.h"
|
||||
#import "FLEXArgumentInputSwitchView.h"
|
||||
#import "FLEXArgumentInputStructView.h"
|
||||
#import "FLEXArgumentInputNotSupportedView.h"
|
||||
#import "FLEXArgumentInputStringView.h"
|
||||
#import "FLEXArgumentInputFontView.h"
|
||||
#import "FLEXArgumentInputColorView.h"
|
||||
#import "FLEXArgumentInputDateView.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
|
||||
@implementation FLEXArgumentInputViewFactory
|
||||
|
||||
+ (FLEXArgumentInputView *)argumentInputViewForTypeEncoding:(const char *)typeEncoding {
|
||||
return [self argumentInputViewForTypeEncoding:typeEncoding currentValue:nil];
|
||||
}
|
||||
|
||||
+ (FLEXArgumentInputView *)argumentInputViewForTypeEncoding:(const char *)typeEncoding currentValue:(id)currentValue {
|
||||
Class subclass = [self argumentInputViewSubclassForTypeEncoding:typeEncoding currentValue:currentValue];
|
||||
if (!subclass) {
|
||||
// Fall back to a FLEXArgumentInputNotSupportedView if we can't find a subclass that fits the type encoding.
|
||||
// The unsupported view shows "nil" and does not allow user input.
|
||||
subclass = [FLEXArgumentInputNotSupportedView class];
|
||||
}
|
||||
// Remove the field name if there is any (e.g. \"width\"d -> d)
|
||||
const NSUInteger fieldNameOffset = [FLEXRuntimeUtility fieldNameOffsetForTypeEncoding:typeEncoding];
|
||||
return [[subclass alloc] initWithArgumentTypeEncoding:typeEncoding + fieldNameOffset];
|
||||
}
|
||||
|
||||
+ (Class)argumentInputViewSubclassForTypeEncoding:(const char *)typeEncoding currentValue:(id)currentValue {
|
||||
// Remove the field name if there is any (e.g. \"width\"d -> d)
|
||||
const NSUInteger fieldNameOffset = [FLEXRuntimeUtility fieldNameOffsetForTypeEncoding:typeEncoding];
|
||||
Class argumentInputViewSubclass = nil;
|
||||
NSArray<Class> *inputViewClasses = @[[FLEXArgumentInputColorView class],
|
||||
[FLEXArgumentInputFontView class],
|
||||
[FLEXArgumentInputStringView class],
|
||||
[FLEXArgumentInputStructView class],
|
||||
[FLEXArgumentInputSwitchView class],
|
||||
[FLEXArgumentInputDateView class],
|
||||
[FLEXArgumentInputNumberView class],
|
||||
[FLEXArgumentInputObjectView class]];
|
||||
|
||||
// Note that order is important here since multiple subclasses may support the same type.
|
||||
// An example is the number subclass and the bool subclass for the type @encode(BOOL).
|
||||
// Both work, but we'd prefer to use the bool subclass.
|
||||
for (Class inputViewClass in inputViewClasses) {
|
||||
if ([inputViewClass supportsObjCType:typeEncoding + fieldNameOffset withCurrentValue:currentValue]) {
|
||||
argumentInputViewSubclass = inputViewClass;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return argumentInputViewSubclass;
|
||||
}
|
||||
|
||||
+ (BOOL)canEditFieldWithTypeEncoding:(const char *)typeEncoding currentValue:(id)currentValue {
|
||||
return [self argumentInputViewSubclassForTypeEncoding:typeEncoding currentValue:currentValue] != nil;
|
||||
}
|
||||
|
||||
@end
|
||||
21
Tweaks/FLEX/Editing/FLEXDefaultEditorViewController.h
Normal file
21
Tweaks/FLEX/Editing/FLEXDefaultEditorViewController.h
Normal file
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// FLEXDefaultEditorViewController.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 5/23/14.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXFieldEditorViewController.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FLEXDefaultEditorViewController : FLEXVariableEditorViewController
|
||||
|
||||
+ (instancetype)target:(NSUserDefaults *)defaults key:(NSString *)key commitHandler:(void(^_Nullable)(void))onCommit;
|
||||
|
||||
+ (BOOL)canEditDefaultWithValue:(nullable id)currentValue;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
80
Tweaks/FLEX/Editing/FLEXDefaultEditorViewController.m
Normal file
80
Tweaks/FLEX/Editing/FLEXDefaultEditorViewController.m
Normal file
@@ -0,0 +1,80 @@
|
||||
//
|
||||
// FLEXDefaultEditorViewController.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 5/23/14.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXDefaultEditorViewController.h"
|
||||
#import "FLEXFieldEditorView.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
#import "FLEXArgumentInputView.h"
|
||||
#import "FLEXArgumentInputViewFactory.h"
|
||||
|
||||
@interface FLEXDefaultEditorViewController ()
|
||||
|
||||
@property (nonatomic, readonly) NSUserDefaults *defaults;
|
||||
@property (nonatomic, readonly) NSString *key;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXDefaultEditorViewController
|
||||
|
||||
+ (instancetype)target:(NSUserDefaults *)defaults key:(NSString *)key commitHandler:(void(^_Nullable)(void))onCommit {
|
||||
FLEXDefaultEditorViewController *editor = [self target:defaults data:key commitHandler:onCommit];
|
||||
editor.title = @"Edit Default";
|
||||
return editor;
|
||||
}
|
||||
|
||||
- (NSUserDefaults *)defaults {
|
||||
return [_target isKindOfClass:[NSUserDefaults class]] ? _target : nil;
|
||||
}
|
||||
|
||||
- (NSString *)key {
|
||||
return _data;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
self.fieldEditorView.fieldDescription = self.key;
|
||||
|
||||
id currentValue = [self.defaults objectForKey:self.key];
|
||||
FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory
|
||||
argumentInputViewForTypeEncoding:FLEXEncodeObject(currentValue)
|
||||
currentValue:currentValue
|
||||
];
|
||||
inputView.backgroundColor = self.view.backgroundColor;
|
||||
inputView.inputValue = currentValue;
|
||||
self.fieldEditorView.argumentInputViews = @[inputView];
|
||||
}
|
||||
|
||||
- (void)actionButtonPressed:(id)sender {
|
||||
id value = self.firstInputView.inputValue;
|
||||
if (value) {
|
||||
[self.defaults setObject:value forKey:self.key];
|
||||
} else {
|
||||
[self.defaults removeObjectForKey:self.key];
|
||||
}
|
||||
[self.defaults synchronize];
|
||||
|
||||
// Dismiss keyboard and handle committed changes
|
||||
[super actionButtonPressed:sender];
|
||||
|
||||
// Go back after setting, but not for switches.
|
||||
if (sender) {
|
||||
[self.navigationController popViewControllerAnimated:YES];
|
||||
} else {
|
||||
self.firstInputView.inputValue = [self.defaults objectForKey:self.key];
|
||||
}
|
||||
}
|
||||
|
||||
+ (BOOL)canEditDefaultWithValue:(id)currentValue {
|
||||
return [FLEXArgumentInputViewFactory
|
||||
canEditFieldWithTypeEncoding:FLEXEncodeObject(currentValue)
|
||||
currentValue:currentValue
|
||||
];
|
||||
}
|
||||
|
||||
@end
|
||||
20
Tweaks/FLEX/Editing/FLEXFieldEditorView.h
Normal file
20
Tweaks/FLEX/Editing/FLEXFieldEditorView.h
Normal file
@@ -0,0 +1,20 @@
|
||||
//
|
||||
// FLEXFieldEditorView.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 5/16/14.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@class FLEXArgumentInputView;
|
||||
|
||||
@interface FLEXFieldEditorView : UIView
|
||||
|
||||
@property (nonatomic, copy) NSString *targetDescription;
|
||||
@property (nonatomic, copy) NSString *fieldDescription;
|
||||
|
||||
@property (nonatomic, copy) NSArray<FLEXArgumentInputView *> *argumentInputViews;
|
||||
|
||||
@end
|
||||
172
Tweaks/FLEX/Editing/FLEXFieldEditorView.m
Normal file
172
Tweaks/FLEX/Editing/FLEXFieldEditorView.m
Normal file
@@ -0,0 +1,172 @@
|
||||
//
|
||||
// FLEXFieldEditorView.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 5/16/14.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXFieldEditorView.h"
|
||||
#import "FLEXArgumentInputView.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXColor.h"
|
||||
|
||||
@interface FLEXFieldEditorView ()
|
||||
|
||||
@property (nonatomic) UILabel *targetDescriptionLabel;
|
||||
@property (nonatomic) UIView *targetDescriptionDivider;
|
||||
@property (nonatomic) UILabel *fieldDescriptionLabel;
|
||||
@property (nonatomic) UIView *fieldDescriptionDivider;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXFieldEditorView
|
||||
|
||||
- (id)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
self.targetDescriptionLabel = [UILabel new];
|
||||
self.targetDescriptionLabel.numberOfLines = 0;
|
||||
self.targetDescriptionLabel.font = [[self class] labelFont];
|
||||
[self addSubview:self.targetDescriptionLabel];
|
||||
|
||||
self.targetDescriptionDivider = [[self class] dividerView];
|
||||
[self addSubview:self.targetDescriptionDivider];
|
||||
|
||||
self.fieldDescriptionLabel = [UILabel new];
|
||||
self.fieldDescriptionLabel.numberOfLines = 0;
|
||||
self.fieldDescriptionLabel.font = [[self class] labelFont];
|
||||
[self addSubview:self.fieldDescriptionLabel];
|
||||
|
||||
self.fieldDescriptionDivider = [[self class] dividerView];
|
||||
[self addSubview:self.fieldDescriptionDivider];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
|
||||
CGFloat horizontalPadding = [[self class] horizontalPadding];
|
||||
CGFloat verticalPadding = [[self class] verticalPadding];
|
||||
CGFloat dividerLineHeight = [[self class] dividerLineHeight];
|
||||
|
||||
CGFloat originY = verticalPadding;
|
||||
CGFloat originX = horizontalPadding;
|
||||
CGFloat contentWidth = self.bounds.size.width - 2.0 * horizontalPadding;
|
||||
CGSize constrainSize = CGSizeMake(contentWidth, CGFLOAT_MAX);
|
||||
|
||||
CGSize instanceDescriptionSize = [self.targetDescriptionLabel sizeThatFits:constrainSize];
|
||||
self.targetDescriptionLabel.frame = CGRectMake(originX, originY, instanceDescriptionSize.width, instanceDescriptionSize.height);
|
||||
originY = CGRectGetMaxY(self.targetDescriptionLabel.frame) + verticalPadding;
|
||||
|
||||
self.targetDescriptionDivider.frame = CGRectMake(originX, originY, contentWidth, dividerLineHeight);
|
||||
originY = CGRectGetMaxY(self.targetDescriptionDivider.frame) + verticalPadding;
|
||||
|
||||
CGSize fieldDescriptionSize = [self.fieldDescriptionLabel sizeThatFits:constrainSize];
|
||||
self.fieldDescriptionLabel.frame = CGRectMake(originX, originY, fieldDescriptionSize.width, fieldDescriptionSize.height);
|
||||
originY = CGRectGetMaxY(self.fieldDescriptionLabel.frame) + verticalPadding;
|
||||
|
||||
self.fieldDescriptionDivider.frame = CGRectMake(originX, originY, contentWidth, dividerLineHeight);
|
||||
originY = CGRectGetMaxY(self.fieldDescriptionDivider.frame) + verticalPadding;
|
||||
|
||||
for (UIView *argumentInputView in self.argumentInputViews) {
|
||||
CGSize inputViewSize = [argumentInputView sizeThatFits:constrainSize];
|
||||
argumentInputView.frame = CGRectMake(originX, originY, inputViewSize.width, inputViewSize.height);
|
||||
originY = CGRectGetMaxY(argumentInputView.frame) + verticalPadding;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setBackgroundColor:(UIColor *)backgroundColor {
|
||||
[super setBackgroundColor:backgroundColor];
|
||||
self.targetDescriptionLabel.backgroundColor = backgroundColor;
|
||||
self.fieldDescriptionLabel.backgroundColor = backgroundColor;
|
||||
}
|
||||
|
||||
- (void)setTargetDescription:(NSString *)targetDescription {
|
||||
if (![_targetDescription isEqual:targetDescription]) {
|
||||
_targetDescription = targetDescription;
|
||||
self.targetDescriptionLabel.text = targetDescription;
|
||||
[self setNeedsLayout];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setFieldDescription:(NSString *)fieldDescription {
|
||||
if (![_fieldDescription isEqual:fieldDescription]) {
|
||||
_fieldDescription = fieldDescription;
|
||||
self.fieldDescriptionLabel.text = fieldDescription;
|
||||
[self setNeedsLayout];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setArgumentInputViews:(NSArray<FLEXArgumentInputView *> *)argumentInputViews {
|
||||
if (![_argumentInputViews isEqual:argumentInputViews]) {
|
||||
|
||||
for (FLEXArgumentInputView *inputView in _argumentInputViews) {
|
||||
[inputView removeFromSuperview];
|
||||
}
|
||||
|
||||
_argumentInputViews = argumentInputViews;
|
||||
|
||||
for (FLEXArgumentInputView *newInputView in argumentInputViews) {
|
||||
[self addSubview:newInputView];
|
||||
}
|
||||
|
||||
[self setNeedsLayout];
|
||||
}
|
||||
}
|
||||
|
||||
+ (UIView *)dividerView {
|
||||
UIView *dividerView = [UIView new];
|
||||
dividerView.backgroundColor = [self dividerColor];
|
||||
return dividerView;
|
||||
}
|
||||
|
||||
+ (UIColor *)dividerColor {
|
||||
return FLEXColor.tertiaryBackgroundColor;
|
||||
}
|
||||
|
||||
+ (CGFloat)horizontalPadding {
|
||||
return 10.0;
|
||||
}
|
||||
|
||||
+ (CGFloat)verticalPadding {
|
||||
return 20.0;
|
||||
}
|
||||
|
||||
+ (UIFont *)labelFont {
|
||||
return [UIFont systemFontOfSize:14.0];
|
||||
}
|
||||
|
||||
+ (CGFloat)dividerLineHeight {
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
- (CGSize)sizeThatFits:(CGSize)size {
|
||||
CGFloat horizontalPadding = [[self class] horizontalPadding];
|
||||
CGFloat verticalPadding = [[self class] verticalPadding];
|
||||
CGFloat dividerLineHeight = [[self class] dividerLineHeight];
|
||||
|
||||
CGFloat height = 0;
|
||||
CGFloat availableWidth = size.width - 2.0 * horizontalPadding;
|
||||
CGSize constrainSize = CGSizeMake(availableWidth, CGFLOAT_MAX);
|
||||
|
||||
height += verticalPadding;
|
||||
height += ceil([self.targetDescriptionLabel sizeThatFits:constrainSize].height);
|
||||
height += verticalPadding;
|
||||
height += dividerLineHeight;
|
||||
height += verticalPadding;
|
||||
height += ceil([self.fieldDescriptionLabel sizeThatFits:constrainSize].height);
|
||||
height += verticalPadding;
|
||||
height += dividerLineHeight;
|
||||
height += verticalPadding;
|
||||
|
||||
for (FLEXArgumentInputView *inputView in self.argumentInputViews) {
|
||||
height += [inputView sizeThatFits:constrainSize].height;
|
||||
height += verticalPadding;
|
||||
}
|
||||
|
||||
return CGSizeMake(size.width, height);
|
||||
}
|
||||
|
||||
@end
|
||||
29
Tweaks/FLEX/Editing/FLEXFieldEditorViewController.h
Normal file
29
Tweaks/FLEX/Editing/FLEXFieldEditorViewController.h
Normal file
@@ -0,0 +1,29 @@
|
||||
//
|
||||
// FLEXFieldEditorViewController.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 11/22/18.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXVariableEditorViewController.h"
|
||||
#import "FLEXProperty.h"
|
||||
#import "FLEXIvar.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FLEXFieldEditorViewController : FLEXVariableEditorViewController
|
||||
|
||||
/// @return nil if the property is readonly or if the type is unsupported
|
||||
+ (nullable instancetype)target:(id)target property:(FLEXProperty *)property commitHandler:(void(^_Nullable)(void))onCommit;
|
||||
/// @return nil if the ivar type is unsupported
|
||||
+ (nullable instancetype)target:(id)target ivar:(FLEXIvar *)ivar commitHandler:(void(^_Nullable)(void))onCommit;
|
||||
|
||||
/// Subclasses can change the button title via the \c title property
|
||||
@property (nonatomic, readonly) UIBarButtonItem *getterButton;
|
||||
|
||||
- (void)getterButtonPressed:(id)sender;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
149
Tweaks/FLEX/Editing/FLEXFieldEditorViewController.m
Normal file
149
Tweaks/FLEX/Editing/FLEXFieldEditorViewController.m
Normal file
@@ -0,0 +1,149 @@
|
||||
//
|
||||
// FLEXFieldEditorViewController.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 11/22/18.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXFieldEditorViewController.h"
|
||||
#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>
|
||||
|
||||
@property (nonatomic) FLEXProperty *property;
|
||||
@property (nonatomic) FLEXIvar *ivar;
|
||||
|
||||
@property (nonatomic, readonly) id currentValue;
|
||||
@property (nonatomic, readonly) const FLEXTypeEncoding *typeEncoding;
|
||||
@property (nonatomic, readonly) NSString *fieldDescription;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXFieldEditorViewController
|
||||
|
||||
#pragma mark - Initialization
|
||||
|
||||
+ (instancetype)target:(id)target property:(nonnull FLEXProperty *)property commitHandler:(void(^_Nullable)(void))onCommit {
|
||||
FLEXFieldEditorViewController *editor = [self target:target data:property commitHandler:onCommit];
|
||||
editor.title = [@"Property: " stringByAppendingString:property.name];
|
||||
editor.property = property;
|
||||
return editor;
|
||||
}
|
||||
|
||||
+ (instancetype)target:(id)target ivar:(nonnull FLEXIvar *)ivar commitHandler:(void(^_Nullable)(void))onCommit {
|
||||
FLEXFieldEditorViewController *editor = [self target:target data:ivar commitHandler:onCommit];
|
||||
editor.title = [@"Ivar: " stringByAppendingString:ivar.name];
|
||||
editor.ivar = ivar;
|
||||
return editor;
|
||||
}
|
||||
|
||||
#pragma mark - Overrides
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
self.view.backgroundColor = FLEXColor.groupedBackgroundColor;
|
||||
|
||||
// Create getter button
|
||||
_getterButton = [[UIBarButtonItem alloc]
|
||||
initWithTitle:@"Get"
|
||||
style:UIBarButtonItemStyleDone
|
||||
target:self
|
||||
action:@selector(getterButtonPressed:)
|
||||
];
|
||||
self.toolbarItems = @[
|
||||
UIBarButtonItem.flex_flexibleSpace, self.getterButton, self.actionButton
|
||||
];
|
||||
|
||||
// Configure input view
|
||||
self.fieldEditorView.fieldDescription = self.fieldDescription;
|
||||
FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:self.typeEncoding];
|
||||
inputView.inputValue = self.currentValue;
|
||||
inputView.delegate = self;
|
||||
self.fieldEditorView.argumentInputViews = @[inputView];
|
||||
|
||||
// Don't show a "set" button for switches; we mutate when the switch is flipped
|
||||
if ([inputView isKindOfClass:[FLEXArgumentInputSwitchView class]]) {
|
||||
self.actionButton.enabled = NO;
|
||||
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
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)actionButtonPressed:(id)sender {
|
||||
if (self.property) {
|
||||
id userInputObject = self.firstInputView.inputValue;
|
||||
NSArray *arguments = userInputObject ? @[userInputObject] : nil;
|
||||
SEL setterSelector = self.property.likelySetter;
|
||||
NSError *error = nil;
|
||||
[FLEXRuntimeUtility performSelector:setterSelector onObject:self.target withArguments:arguments error:&error];
|
||||
if (error) {
|
||||
[FLEXAlert showAlert:@"Property Setter Failed" message:error.localizedDescription from:self];
|
||||
sender = nil; // Don't pop back
|
||||
}
|
||||
} else {
|
||||
// TODO: check mutability and use mutableCopy if necessary;
|
||||
// this currently could and would assign NSArray to NSMutableArray
|
||||
[self.ivar setValue:self.firstInputView.inputValue onObject:self.target];
|
||||
}
|
||||
|
||||
// Dismiss keyboard and handle committed changes
|
||||
[super actionButtonPressed:sender];
|
||||
|
||||
// Go back after setting, but not for switches.
|
||||
if (sender) {
|
||||
[self.navigationController popViewControllerAnimated:YES];
|
||||
} else {
|
||||
self.firstInputView.inputValue = self.currentValue;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)getterButtonPressed:(id)sender {
|
||||
[self.fieldEditorView endEditing:YES];
|
||||
|
||||
[self exploreObjectOrPopViewController:self.currentValue];
|
||||
}
|
||||
|
||||
- (void)argumentInputViewValueDidChange:(FLEXArgumentInputView *)argumentInputView {
|
||||
if ([argumentInputView isKindOfClass:[FLEXArgumentInputSwitchView class]]) {
|
||||
[self actionButtonPressed:nil];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
- (id)currentValue {
|
||||
if (self.property) {
|
||||
return [self.property getValue:self.target];
|
||||
} else {
|
||||
return [self.ivar getValue:self.target];
|
||||
}
|
||||
}
|
||||
|
||||
- (const FLEXTypeEncoding *)typeEncoding {
|
||||
if (self.property) {
|
||||
return self.property.attributes.typeEncoding.UTF8String;
|
||||
} else {
|
||||
return self.ivar.typeEncoding.UTF8String;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSString *)fieldDescription {
|
||||
if (self.property) {
|
||||
return self.property.fullDescription;
|
||||
} else {
|
||||
return self.ivar.description;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
16
Tweaks/FLEX/Editing/FLEXMethodCallingViewController.h
Normal file
16
Tweaks/FLEX/Editing/FLEXMethodCallingViewController.h
Normal file
@@ -0,0 +1,16 @@
|
||||
//
|
||||
// FLEXMethodCallingViewController.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 5/23/14.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXVariableEditorViewController.h"
|
||||
#import "FLEXMethod.h"
|
||||
|
||||
@interface FLEXMethodCallingViewController : FLEXVariableEditorViewController
|
||||
|
||||
+ (instancetype)target:(id)target method:(FLEXMethod *)method;
|
||||
|
||||
@end
|
||||
110
Tweaks/FLEX/Editing/FLEXMethodCallingViewController.m
Normal file
110
Tweaks/FLEX/Editing/FLEXMethodCallingViewController.m
Normal file
@@ -0,0 +1,110 @@
|
||||
//
|
||||
// FLEXMethodCallingViewController.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 5/23/14.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXMethodCallingViewController.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
#import "FLEXFieldEditorView.h"
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
#import "FLEXObjectExplorerViewController.h"
|
||||
#import "FLEXArgumentInputView.h"
|
||||
#import "FLEXArgumentInputViewFactory.h"
|
||||
#import "FLEXUtility.h"
|
||||
|
||||
@interface FLEXMethodCallingViewController ()
|
||||
@property (nonatomic, readonly) FLEXMethod *method;
|
||||
@end
|
||||
|
||||
@implementation FLEXMethodCallingViewController
|
||||
|
||||
+ (instancetype)target:(id)target method:(FLEXMethod *)method {
|
||||
return [[self alloc] initWithTarget:target method:method];
|
||||
}
|
||||
|
||||
- (id)initWithTarget:(id)target method:(FLEXMethod *)method {
|
||||
NSParameterAssert(method.isInstanceMethod == !object_isClass(target));
|
||||
|
||||
self = [super initWithTarget:target data:method commitHandler:nil];
|
||||
if (self) {
|
||||
self.title = method.isInstanceMethod ? @"Method: " : @"Class Method: ";
|
||||
self.title = [self.title stringByAppendingString:method.selectorString];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
self.actionButton.title = @"Call";
|
||||
|
||||
// Configure field editor view
|
||||
self.fieldEditorView.argumentInputViews = [self argumentInputViews];
|
||||
self.fieldEditorView.fieldDescription = [NSString stringWithFormat:
|
||||
@"Signature:\n%@\n\nReturn Type:\n%s",
|
||||
self.method.description, (char *)self.method.returnType
|
||||
];
|
||||
}
|
||||
|
||||
- (NSArray<FLEXArgumentInputView *> *)argumentInputViews {
|
||||
Method method = self.method.objc_method;
|
||||
NSArray *methodComponents = [FLEXRuntimeUtility prettyArgumentComponentsForMethod:method];
|
||||
NSMutableArray<FLEXArgumentInputView *> *argumentInputViews = [NSMutableArray new];
|
||||
unsigned int argumentIndex = kFLEXNumberOfImplicitArgs;
|
||||
|
||||
for (NSString *methodComponent in methodComponents) {
|
||||
char *argumentTypeEncoding = method_copyArgumentType(method, argumentIndex);
|
||||
FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:argumentTypeEncoding];
|
||||
free(argumentTypeEncoding);
|
||||
|
||||
inputView.backgroundColor = self.view.backgroundColor;
|
||||
inputView.title = methodComponent;
|
||||
[argumentInputViews addObject:inputView];
|
||||
argumentIndex++;
|
||||
}
|
||||
|
||||
return argumentInputViews;
|
||||
}
|
||||
|
||||
- (void)actionButtonPressed:(id)sender {
|
||||
// Gather arguments
|
||||
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];
|
||||
}
|
||||
|
||||
// Call method
|
||||
NSError *error = nil;
|
||||
id returnValue = [FLEXRuntimeUtility
|
||||
performSelector:self.method.selector
|
||||
onObject:self.target
|
||||
withArguments:arguments
|
||||
error:&error
|
||||
];
|
||||
|
||||
// Dismiss keyboard and handle committed changes
|
||||
[super actionButtonPressed:sender];
|
||||
|
||||
// Display return value or error
|
||||
if (error) {
|
||||
[FLEXAlert showAlert:@"Method Call Failed" message:error.localizedDescription from:self];
|
||||
} else if (returnValue) {
|
||||
// For non-nil (or void) return types, push an explorer view controller to display the returned object
|
||||
returnValue = [FLEXRuntimeUtility potentiallyUnwrapBoxedPointer:returnValue type:self.method.returnType];
|
||||
FLEXObjectExplorerViewController *explorer = [FLEXObjectExplorerFactory explorerViewControllerForObject:returnValue];
|
||||
[self.navigationController pushViewController:explorer animated:YES];
|
||||
} else {
|
||||
[self exploreObjectOrPopViewController:returnValue];
|
||||
}
|
||||
}
|
||||
|
||||
- (FLEXMethod *)method {
|
||||
return _data;
|
||||
}
|
||||
|
||||
@end
|
||||
55
Tweaks/FLEX/Editing/FLEXVariableEditorViewController.h
Normal file
55
Tweaks/FLEX/Editing/FLEXVariableEditorViewController.h
Normal file
@@ -0,0 +1,55 @@
|
||||
//
|
||||
// FLEXVariableEditorViewController.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 5/16/14.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@class FLEXFieldEditorView;
|
||||
@class FLEXArgumentInputView;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
/// An abstract screen for editing or configuring one or more variables.
|
||||
/// "Target" is the target of the edit operation, and "data" is the data
|
||||
/// you want to mutate or pass to the target when the action is performed.
|
||||
/// The action may be something like calling a method, setting an ivar, etc.
|
||||
@interface FLEXVariableEditorViewController : UIViewController {
|
||||
@protected
|
||||
id _target;
|
||||
_Nullable id _data;
|
||||
void (^_Nullable _commitHandler)(void);
|
||||
}
|
||||
|
||||
/// @param target The target of the operation
|
||||
/// @param data The data associated with the operation
|
||||
/// @param onCommit An action to perform when the data changes
|
||||
+ (instancetype)target:(id)target data:(nullable id)data commitHandler:(void(^_Nullable)(void))onCommit;
|
||||
/// @param target The target of the operation
|
||||
/// @param data The data associated with the operation
|
||||
/// @param onCommit An action to perform when the data changes
|
||||
- (id)initWithTarget:(id)target data:(nullable id)data commitHandler:(void(^_Nullable)(void))onCommit;
|
||||
|
||||
@property (nonatomic, readonly) id target;
|
||||
|
||||
/// Convenience accessor since many subclasses only use one input view
|
||||
@property (nonatomic, readonly, nullable) FLEXArgumentInputView *firstInputView;
|
||||
|
||||
@property (nonatomic, readonly) FLEXFieldEditorView *fieldEditorView;
|
||||
/// Subclasses can change the button title via the button's \c title property
|
||||
@property (nonatomic, readonly) UIBarButtonItem *actionButton;
|
||||
|
||||
/// Subclasses should override to provide "set" functionality.
|
||||
/// The commit handler--if present--is called here.
|
||||
- (void)actionButtonPressed:(nullable id)sender;
|
||||
|
||||
/// Pushes an explorer view controller for the given object
|
||||
/// or pops the current view controller.
|
||||
- (void)exploreObjectOrPopViewController:(nullable id)objectOrNil;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
141
Tweaks/FLEX/Editing/FLEXVariableEditorViewController.m
Normal file
141
Tweaks/FLEX/Editing/FLEXVariableEditorViewController.m
Normal file
@@ -0,0 +1,141 @@
|
||||
//
|
||||
// FLEXVariableEditorViewController.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 5/16/14.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXColor.h"
|
||||
#import "FLEXVariableEditorViewController.h"
|
||||
#import "FLEXFieldEditorView.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
#import "FLEXArgumentInputView.h"
|
||||
#import "FLEXArgumentInputViewFactory.h"
|
||||
#import "FLEXObjectExplorerViewController.h"
|
||||
#import "UIBarButtonItem+FLEX.h"
|
||||
|
||||
@interface FLEXVariableEditorViewController () <UIScrollViewDelegate>
|
||||
@property (nonatomic) UIScrollView *scrollView;
|
||||
@end
|
||||
|
||||
@implementation FLEXVariableEditorViewController
|
||||
|
||||
#pragma mark - Initialization
|
||||
|
||||
+ (instancetype)target:(id)target data:(nullable id)data commitHandler:(void(^_Nullable)(void))onCommit {
|
||||
return [[self alloc] initWithTarget:target data:data commitHandler:onCommit];
|
||||
}
|
||||
|
||||
- (id)initWithTarget:(id)target data:(nullable id)data commitHandler:(void(^_Nullable)(void))onCommit {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_target = target;
|
||||
_data = data;
|
||||
_commitHandler = onCommit;
|
||||
[NSNotificationCenter.defaultCenter
|
||||
addObserver:self selector:@selector(keyboardDidShow:)
|
||||
name:UIKeyboardDidShowNotification object:nil
|
||||
];
|
||||
[NSNotificationCenter.defaultCenter
|
||||
addObserver:self selector:@selector(keyboardWillHide:)
|
||||
name:UIKeyboardWillHideNotification object:nil
|
||||
];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[NSNotificationCenter.defaultCenter removeObserver:self];
|
||||
}
|
||||
|
||||
#pragma mark - UIViewController methods
|
||||
|
||||
- (void)keyboardDidShow:(NSNotification *)notification {
|
||||
CGRect keyboardRectInWindow = [[[notification userInfo] objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
|
||||
CGSize keyboardSize = [self.view convertRect:keyboardRectInWindow fromView:nil].size;
|
||||
UIEdgeInsets scrollInsets = self.scrollView.contentInset;
|
||||
scrollInsets.bottom = keyboardSize.height;
|
||||
self.scrollView.contentInset = scrollInsets;
|
||||
self.scrollView.scrollIndicatorInsets = scrollInsets;
|
||||
|
||||
// Find the active input view and scroll to make sure it's visible.
|
||||
for (FLEXArgumentInputView *argumentInputView in self.fieldEditorView.argumentInputViews) {
|
||||
if (argumentInputView.inputViewIsFirstResponder) {
|
||||
CGRect scrollToVisibleRect = [self.scrollView convertRect:argumentInputView.bounds fromView:argumentInputView];
|
||||
[self.scrollView scrollRectToVisible:scrollToVisibleRect animated:YES];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)keyboardWillHide:(NSNotification *)notification {
|
||||
UIEdgeInsets scrollInsets = self.scrollView.contentInset;
|
||||
scrollInsets.bottom = 0.0;
|
||||
self.scrollView.contentInset = scrollInsets;
|
||||
self.scrollView.scrollIndicatorInsets = scrollInsets;
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
|
||||
self.view.backgroundColor = FLEXColor.scrollViewBackgroundColor;
|
||||
|
||||
self.scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds];
|
||||
self.scrollView.backgroundColor = self.view.backgroundColor;
|
||||
self.scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
||||
self.scrollView.delegate = self;
|
||||
[self.view addSubview:self.scrollView];
|
||||
|
||||
_fieldEditorView = [FLEXFieldEditorView new];
|
||||
self.fieldEditorView.targetDescription = [NSString stringWithFormat:@"%@ %p", [self.target class], self.target];
|
||||
[self.scrollView addSubview:self.fieldEditorView];
|
||||
|
||||
_actionButton = [[UIBarButtonItem alloc]
|
||||
initWithTitle:@"Set"
|
||||
style:UIBarButtonItemStyleDone
|
||||
target:self
|
||||
action:@selector(actionButtonPressed:)
|
||||
];
|
||||
|
||||
self.navigationController.toolbarHidden = NO;
|
||||
self.toolbarItems = @[UIBarButtonItem.flex_flexibleSpace, self.actionButton];
|
||||
}
|
||||
|
||||
- (void)viewWillLayoutSubviews {
|
||||
CGSize constrainSize = CGSizeMake(self.scrollView.bounds.size.width, CGFLOAT_MAX);
|
||||
CGSize fieldEditorSize = [self.fieldEditorView sizeThatFits:constrainSize];
|
||||
self.fieldEditorView.frame = CGRectMake(0, 0, fieldEditorSize.width, fieldEditorSize.height);
|
||||
self.scrollView.contentSize = fieldEditorSize;
|
||||
}
|
||||
|
||||
#pragma mark - Public
|
||||
|
||||
- (FLEXArgumentInputView *)firstInputView {
|
||||
return [self.fieldEditorView argumentInputViews].firstObject;
|
||||
}
|
||||
|
||||
- (void)actionButtonPressed:(id)sender {
|
||||
// Subclasses can override
|
||||
[self.fieldEditorView endEditing:YES];
|
||||
if (_commitHandler) {
|
||||
_commitHandler();
|
||||
}
|
||||
}
|
||||
|
||||
- (void)exploreObjectOrPopViewController:(id)objectOrNil {
|
||||
if (objectOrNil) {
|
||||
// For non-nil (or void) return types, push an explorer view controller to display the object
|
||||
FLEXObjectExplorerViewController *explorerViewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:objectOrNil];
|
||||
[self.navigationController pushViewController:explorerViewController animated:YES];
|
||||
} else {
|
||||
// If we didn't get a returned object but the method call succeeded,
|
||||
// pop this view controller off the stack to indicate that the call went through.
|
||||
[self.navigationController popViewControllerAnimated:YES];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// FLEXBookmarkManager.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 2/6/20.
|
||||
// Copyright © 2020 FLEX Team. 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 FLEX Team. 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 FLEX Team. 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 FLEX Team. 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 flex_itemWithTitle:@"Close All" target:self action:@selector(closeAllButtonPressed:)],
|
||||
UIBarButtonItem.flex_flexibleSpace,
|
||||
// We use a non-system done item because we change its title dynamically
|
||||
[UIBarButtonItem flex_doneStyleitemWithTitle:@"Done" target:self action:@selector(toggleEditing)]
|
||||
];
|
||||
|
||||
self.toolbarItems.firstObject.tintColor = FLEXColor.destructiveColor;
|
||||
}
|
||||
|
||||
- (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:(UIBarButtonItem *)sender {
|
||||
[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 source:sender];
|
||||
}
|
||||
|
||||
- (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
|
||||
61
Tweaks/FLEX/ExplorerInterface/FLEXExplorerViewController.h
Normal file
61
Tweaks/FLEX/ExplorerInterface/FLEXExplorerViewController.h
Normal file
@@ -0,0 +1,61 @@
|
||||
//
|
||||
// FLEXExplorerViewController.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 4/4/14.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXExplorerToolbar.h"
|
||||
|
||||
@class FLEXWindow;
|
||||
@protocol FLEXExplorerViewControllerDelegate;
|
||||
|
||||
/// A view controller that manages the FLEX toolbar.
|
||||
@interface FLEXExplorerViewController : UIViewController
|
||||
|
||||
@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.
|
||||
///
|
||||
/// If a tool is already presented, this method simply dismisses it and calls the completion block.
|
||||
/// If no tool is presented, @code future() @endcode is presented and the completion block is called.
|
||||
- (void)toggleToolWithViewControllerProvider:(UINavigationController *(^)(void))future
|
||||
completion:(void (^)(void))completion;
|
||||
|
||||
/// @brief Used to present (or dismiss) a modal view controller ("tool"),
|
||||
/// typically triggered by pressing a button in the toolbar.
|
||||
///
|
||||
/// If a tool is already presented, this method dismisses it and presents the given tool.
|
||||
/// The completion block is called once the tool has been presented.
|
||||
- (void)presentTool:(UINavigationController *(^)(void))future
|
||||
completion:(void (^)(void))completion;
|
||||
|
||||
// Keyboard shortcut helpers
|
||||
|
||||
- (void)toggleSelectTool;
|
||||
- (void)toggleMoveTool;
|
||||
- (void)toggleViewsTool;
|
||||
- (void)toggleMenuTool;
|
||||
|
||||
/// @return YES if the explorer used the key press to perform an action, NO otherwise
|
||||
- (BOOL)handleDownArrowKeyPressed;
|
||||
/// @return YES if the explorer used the key press to perform an action, NO otherwise
|
||||
- (BOOL)handleUpArrowKeyPressed;
|
||||
/// @return YES if the explorer used the key press to perform an action, NO otherwise
|
||||
- (BOOL)handleRightArrowKeyPressed;
|
||||
/// @return YES if the explorer used the key press to perform an action, NO otherwise
|
||||
- (BOOL)handleLeftArrowKeyPressed;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
@protocol FLEXExplorerViewControllerDelegate <NSObject>
|
||||
- (void)explorerViewControllerDidFinish:(FLEXExplorerViewController *)explorerViewController;
|
||||
@end
|
||||
1050
Tweaks/FLEX/ExplorerInterface/FLEXExplorerViewController.m
Normal file
1050
Tweaks/FLEX/ExplorerInterface/FLEXExplorerViewController.m
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// FLEXViewControllersViewController.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 2/13/20.
|
||||
// Copyright © 2020 FLEX Team. 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,79 @@
|
||||
//
|
||||
// FLEXViewControllersViewController.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner Bennett on 2/13/20.
|
||||
// Copyright © 2020 FLEX Team. 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.showsSearchBar = YES;
|
||||
[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
|
||||
29
Tweaks/FLEX/ExplorerInterface/FLEXWindow.h
Normal file
29
Tweaks/FLEX/ExplorerInterface/FLEXWindow.h
Normal file
@@ -0,0 +1,29 @@
|
||||
//
|
||||
// FLEXWindow.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 4/13/14.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@protocol FLEXWindowEventDelegate <NSObject>
|
||||
|
||||
- (BOOL)shouldHandleTouchAtPoint:(CGPoint)pointInWindow;
|
||||
- (BOOL)canBecomeKeyWindow;
|
||||
|
||||
@end
|
||||
|
||||
#pragma mark -
|
||||
@interface FLEXWindow : UIWindow
|
||||
|
||||
@property (nonatomic, weak) id <FLEXWindowEventDelegate> eventDelegate;
|
||||
|
||||
/// Tracked so we can restore the key window after dismissing a modal.
|
||||
/// We need to become key after modal presentation so we can correctly capture input.
|
||||
/// If we're just showing the toolbar, we want the main app's window to remain key
|
||||
/// so that we don't interfere with input, status bar, etc.
|
||||
@property (nonatomic, readonly) UIWindow *previousKeyWindow;
|
||||
|
||||
@end
|
||||
72
Tweaks/FLEX/ExplorerInterface/FLEXWindow.m
Normal file
72
Tweaks/FLEX/ExplorerInterface/FLEXWindow.m
Normal file
@@ -0,0 +1,72 @@
|
||||
//
|
||||
// FLEXWindow.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 4/13/14.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXWindow.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import <objc/runtime.h>
|
||||
|
||||
@implementation FLEXWindow
|
||||
|
||||
- (id)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
// Some apps have windows at UIWindowLevelStatusBar + n.
|
||||
// If we make the window level too high, we block out UIAlertViews.
|
||||
// There's a balance between staying above the app's windows and staying below alerts.
|
||||
// UIWindowLevelStatusBar + 100 seems to hit that balance.
|
||||
self.windowLevel = UIWindowLevelStatusBar + 100.0;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
|
||||
BOOL pointInside = NO;
|
||||
if ([self.eventDelegate shouldHandleTouchAtPoint:point]) {
|
||||
pointInside = [super pointInside:point withEvent:event];
|
||||
}
|
||||
return pointInside;
|
||||
}
|
||||
|
||||
- (BOOL)shouldAffectStatusBarAppearance {
|
||||
return [self isKeyWindow];
|
||||
}
|
||||
|
||||
- (BOOL)canBecomeKeyWindow {
|
||||
return [self.eventDelegate canBecomeKeyWindow];
|
||||
}
|
||||
|
||||
- (void)makeKeyWindow {
|
||||
_previousKeyWindow = FLEXUtility.appKeyWindow;
|
||||
[super makeKeyWindow];
|
||||
}
|
||||
|
||||
- (void)resignKeyWindow {
|
||||
[super resignKeyWindow];
|
||||
_previousKeyWindow = nil;
|
||||
}
|
||||
|
||||
+ (void)initialize {
|
||||
// This adds a method (superclass override) at runtime which gives us the status bar behavior we want.
|
||||
// The FLEX window is intended to be an overlay that generally doesn't affect the app underneath.
|
||||
// Most of the time, we want the app's main window(s) to be in control of status bar behavior.
|
||||
// Done at runtime with an obfuscated selector because it is private API. But you shouldn't ship this to the App Store anyways...
|
||||
NSString *canAffectSelectorString = [@[@"_can", @"Affect", @"Status", @"Bar", @"Appearance"] componentsJoinedByString:@""];
|
||||
SEL canAffectSelector = NSSelectorFromString(canAffectSelectorString);
|
||||
Method shouldAffectMethod = class_getInstanceMethod(self, @selector(shouldAffectStatusBarAppearance));
|
||||
IMP canAffectImplementation = method_getImplementation(shouldAffectMethod);
|
||||
class_addMethod(self, canAffectSelector, canAffectImplementation, method_getTypeEncoding(shouldAffectMethod));
|
||||
|
||||
// One more...
|
||||
NSString *canBecomeKeySelectorString = [NSString stringWithFormat:@"_%@", NSStringFromSelector(@selector(canBecomeKeyWindow))];
|
||||
SEL canBecomeKeySelector = NSSelectorFromString(canBecomeKeySelectorString);
|
||||
Method canBecomeKeyMethod = class_getInstanceMethod(self, @selector(canBecomeKeyWindow));
|
||||
IMP canBecomeKeyImplementation = method_getImplementation(canBecomeKeyMethod);
|
||||
class_addMethod(self, canBecomeKeySelector, canBecomeKeyImplementation, method_getTypeEncoding(canBecomeKeyMethod));
|
||||
}
|
||||
|
||||
@end
|
||||
17
Tweaks/FLEX/ExplorerInterface/FLEXWindowManagerController.h
Normal file
17
Tweaks/FLEX/ExplorerInterface/FLEXWindowManagerController.h
Normal file
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// FLEXWindowManagerController.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 2/6/20.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXTableViewController.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FLEXWindowManagerController : FLEXTableViewController
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
302
Tweaks/FLEX/ExplorerInterface/FLEXWindowManagerController.m
Normal file
302
Tweaks/FLEX/ExplorerInterface/FLEXWindowManagerController.m
Normal file
@@ -0,0 +1,302 @@
|
||||
//
|
||||
// FLEXWindowManagerController.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 2/6/20.
|
||||
// Copyright © 2020 FLEX Team. 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(^)(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
|
||||
45
Tweaks/FLEX/ExplorerInterface/Tabs/FLEXTabList.h
Normal file
45
Tweaks/FLEX/ExplorerInterface/Tabs/FLEXTabList.h
Normal file
@@ -0,0 +1,45 @@
|
||||
//
|
||||
// FLEXTabList.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 2/1/20.
|
||||
// Copyright © 2020 FLEX Team. 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
|
||||
133
Tweaks/FLEX/ExplorerInterface/Tabs/FLEXTabList.m
Normal file
133
Tweaks/FLEX/ExplorerInterface/Tabs/FLEXTabList.m
Normal file
@@ -0,0 +1,133 @@
|
||||
//
|
||||
// FLEXTabList.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 2/1/20.
|
||||
// Copyright © 2020 FLEX Team. 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);
|
||||
NSInteger idx = [self.openTabs indexOfObject:tab];
|
||||
if (idx != NSNotFound) {
|
||||
[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
|
||||
13
Tweaks/FLEX/ExplorerInterface/Tabs/FLEXTabsViewController.h
Normal file
13
Tweaks/FLEX/ExplorerInterface/Tabs/FLEXTabsViewController.h
Normal file
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// FLEXTabsViewController.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 2/4/20.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXTableViewController.h"
|
||||
|
||||
@interface FLEXTabsViewController : FLEXTableViewController
|
||||
|
||||
@end
|
||||
335
Tweaks/FLEX/ExplorerInterface/Tabs/FLEXTabsViewController.m
Normal file
335
Tweaks/FLEX/ExplorerInterface/Tabs/FLEXTabsViewController.m
Normal file
@@ -0,0 +1,335 @@
|
||||
//
|
||||
// FLEXTabsViewController.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 2/4/20.
|
||||
// Copyright © 2020 FLEX Team. 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 flex_itemWithTitle:@"Close All" target:self action:@selector(closeAllButtonPressed:)],
|
||||
UIBarButtonItem.flex_flexibleSpace,
|
||||
[UIBarButtonItem flex_disabledSystemItem:UIBarButtonSystemItemAdd],
|
||||
UIBarButtonItem.flex_flexibleSpace,
|
||||
// We use a non-system done item because we change its title dynamically
|
||||
[UIBarButtonItem flex_doneStyleitemWithTitle:@"Done" target:self action:@selector(toggleEditing)]
|
||||
];
|
||||
|
||||
self.toolbarItems.firstObject.tintColor = FLEXColor.destructiveColor;
|
||||
}
|
||||
|
||||
- (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:(UIBarButtonItem *)sender {
|
||||
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 source:sender];
|
||||
} 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:(UIBarButtonItem *)sender {
|
||||
[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 source:sender];
|
||||
}
|
||||
|
||||
- (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
|
||||
20
Tweaks/FLEX/FLEX-Categories.h
Normal file
20
Tweaks/FLEX/FLEX-Categories.h
Normal file
@@ -0,0 +1,20 @@
|
||||
//
|
||||
// FLEX-Categories.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 3/12/20.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "UIBarButtonItem+FLEX.h"
|
||||
#import "CALayer+FLEX.h"
|
||||
#import "UIFont+FLEX.h"
|
||||
#import "UIGestureRecognizer+Blocks.h"
|
||||
#import "UIPasteboard+FLEX.h"
|
||||
#import "UIMenu+FLEX.h"
|
||||
#import "UITextField+Range.h"
|
||||
|
||||
#import "NSObject+FLEX_Reflection.h"
|
||||
#import "NSArray+FLEX.h"
|
||||
#import "NSUserDefaults+FLEX.h"
|
||||
#import "NSTimer+FLEX.h"
|
||||
22
Tweaks/FLEX/FLEX-Core.h
Normal file
22
Tweaks/FLEX/FLEX-Core.h
Normal file
@@ -0,0 +1,22 @@
|
||||
//
|
||||
// FLEX-Core.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 3/11/20.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXFilteringTableViewController.h"
|
||||
#import "FLEXNavigationController.h"
|
||||
#import "FLEXTableViewController.h"
|
||||
#import "FLEXTableView.h"
|
||||
|
||||
#import "FLEXSingleRowSection.h"
|
||||
#import "FLEXTableViewSection.h"
|
||||
|
||||
#import "FLEXCodeFontCell.h"
|
||||
#import "FLEXSubtitleTableViewCell.h"
|
||||
#import "FLEXTableViewCell.h"
|
||||
#import "FLEXMultilineTableViewCell.h"
|
||||
#import "FLEXKeyValueTableViewCell.h"
|
||||
|
||||
22
Tweaks/FLEX/FLEX-ObjectExploring.h
Normal file
22
Tweaks/FLEX/FLEX-ObjectExploring.h
Normal file
@@ -0,0 +1,22 @@
|
||||
//
|
||||
// FLEX-ObjectExploring.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 3/11/20.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
#import "FLEXObjectExplorerViewController.h"
|
||||
|
||||
#import "FLEXObjectExplorer.h"
|
||||
|
||||
#import "FLEXShortcut.h"
|
||||
#import "FLEXShortcutsSection.h"
|
||||
|
||||
#import "FLEXCollectionContentSection.h"
|
||||
#import "FLEXColorPreviewSection.h"
|
||||
#import "FLEXDefaultsContentSection.h"
|
||||
#import "FLEXMetadataSection.h"
|
||||
#import "FLEXMutableListSection.h"
|
||||
#import "FLEXObjectInfoSection.h"
|
||||
25
Tweaks/FLEX/FLEX-Runtime.h
Normal file
25
Tweaks/FLEX/FLEX-Runtime.h
Normal file
@@ -0,0 +1,25 @@
|
||||
//
|
||||
// FLEX-Runtime.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 3/11/20.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXObjcInternal.h"
|
||||
#import "FLEXRuntimeSafety.h"
|
||||
#import "FLEXBlockDescription.h"
|
||||
#import "FLEXTypeEncodingParser.h"
|
||||
|
||||
#import "FLEXMirror.h"
|
||||
#import "FLEXProtocol.h"
|
||||
#import "FLEXProperty.h"
|
||||
#import "FLEXIvar.h"
|
||||
#import "FLEXMethodBase.h"
|
||||
#import "FLEXMethod.h"
|
||||
#import "FLEXPropertyAttributes.h"
|
||||
#import "FLEXRuntime+Compare.h"
|
||||
#import "FLEXRuntime+UIKitHelpers.h"
|
||||
|
||||
#import "FLEXProtocolBuilder.h"
|
||||
#import "FLEXClassBuilder.h"
|
||||
25
Tweaks/FLEX/FLEX.h
Normal file
25
Tweaks/FLEX/FLEX.h
Normal file
@@ -0,0 +1,25 @@
|
||||
//
|
||||
// FLEX.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Eric Horacek on 7/18/15.
|
||||
// Modified by Tanner Bennett on 3/12/20.
|
||||
// Copyright (c) 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXManager.h"
|
||||
#import "FLEXManager+Extensibility.h"
|
||||
#import "FLEXManager+Networking.h"
|
||||
|
||||
#import "FLEXExplorerToolbar.h"
|
||||
#import "FLEXExplorerToolbarItem.h"
|
||||
#import "FLEXGlobalsEntry.h"
|
||||
|
||||
#import "FLEX-Core.h"
|
||||
#import "FLEX-Runtime.h"
|
||||
#import "FLEX-Categories.h"
|
||||
#import "FLEX-ObjectExploring.h"
|
||||
|
||||
#import "FLEXMacros.h"
|
||||
#import "FLEXAlert.h"
|
||||
#import "FLEXResources.h"
|
||||
@@ -0,0 +1,28 @@
|
||||
//
|
||||
// FLEXDBQueryRowCell.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Peng Tao on 15/11/24.
|
||||
// Copyright © 2015年 f. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@class FLEXDBQueryRowCell;
|
||||
|
||||
extern NSString * const kFLEXDBQueryRowCellReuse;
|
||||
|
||||
@protocol FLEXDBQueryRowCellLayoutSource <NSObject>
|
||||
|
||||
- (CGFloat)dbQueryRowCell:(FLEXDBQueryRowCell *)dbQueryRowCell minXForColumn:(NSUInteger)column;
|
||||
- (CGFloat)dbQueryRowCell:(FLEXDBQueryRowCell *)dbQueryRowCell widthForColumn:(NSUInteger)column;
|
||||
|
||||
@end
|
||||
|
||||
@interface FLEXDBQueryRowCell : UITableViewCell
|
||||
|
||||
/// An array of NSString, NSNumber, or NSData objects
|
||||
@property (nonatomic) NSArray *data;
|
||||
@property (nonatomic, weak) id<FLEXDBQueryRowCellLayoutSource> layoutSource;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,75 @@
|
||||
//
|
||||
// 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+FLEX.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 height = self.contentView.frame.size.height;
|
||||
|
||||
[self.labels flex_forEach:^(UILabel *label, NSUInteger i) {
|
||||
CGFloat width = [self.layoutSource dbQueryRowCell:self widthForColumn:i];
|
||||
CGFloat minX = [self.layoutSource dbQueryRowCell:self minXForColumn:i];
|
||||
label.frame = CGRectMake(minX + 5, 0, (width - 10), height);
|
||||
}];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,35 @@
|
||||
//
|
||||
// PTDatabaseManager.h
|
||||
// Derived from:
|
||||
//
|
||||
// FMDatabase.h
|
||||
// FMDB( https://github.com/ccgus/fmdb )
|
||||
//
|
||||
// Created by Peng Tao on 15/11/23.
|
||||
//
|
||||
// Licensed to Flying Meat Inc. under one or more contributor license agreements.
|
||||
// See the LICENSE file distributed with this work for the terms under
|
||||
// which Flying Meat Inc. licenses this file to you.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "FLEXSQLResult.h"
|
||||
|
||||
/// Conformers should automatically open and close the database
|
||||
@protocol FLEXDatabaseManager <NSObject>
|
||||
|
||||
@required
|
||||
|
||||
/// @return \c nil if the database couldn't be opened
|
||||
+ (instancetype)managerForDatabase:(NSString *)path;
|
||||
|
||||
/// @return a list of all table names
|
||||
- (NSArray<NSString *> *)queryAllTables;
|
||||
- (NSArray<NSString *> *)queryAllColumnsOfTable:(NSString *)tableName;
|
||||
- (NSArray<NSArray *> *)queryAllDataInTable:(NSString *)tableName;
|
||||
|
||||
@optional
|
||||
|
||||
- (NSArray<NSString *> *)queryRowIDsInTable:(NSString *)tableName;
|
||||
- (FLEXSQLResult *)executeStatement:(NSString *)SQLStatement;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,47 @@
|
||||
//
|
||||
// PTMultiColumnTableView.h
|
||||
// PTMultiColumnTableViewDemo
|
||||
//
|
||||
// Created by Peng Tao on 15/11/16.
|
||||
// Copyright © 2015年 Peng Tao. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "FLEXTableColumnHeader.h"
|
||||
|
||||
@class FLEXMultiColumnTableView;
|
||||
|
||||
@protocol FLEXMultiColumnTableViewDelegate <NSObject>
|
||||
|
||||
@required
|
||||
- (void)multiColumnTableView:(FLEXMultiColumnTableView *)tableView didSelectRow:(NSInteger)row;
|
||||
- (void)multiColumnTableView:(FLEXMultiColumnTableView *)tableView didSelectHeaderForColumn:(NSInteger)column sortType:(FLEXTableColumnHeaderSortType)sortType;
|
||||
|
||||
@end
|
||||
|
||||
@protocol FLEXMultiColumnTableViewDataSource <NSObject>
|
||||
|
||||
@required
|
||||
|
||||
- (NSInteger)numberOfColumnsInTableView:(FLEXMultiColumnTableView *)tableView;
|
||||
- (NSInteger)numberOfRowsInTableView:(FLEXMultiColumnTableView *)tableView;
|
||||
- (NSString *)columnTitle:(NSInteger)column;
|
||||
- (NSString *)rowTitle:(NSInteger)row;
|
||||
- (NSArray<NSString *> *)contentForRow:(NSInteger)row;
|
||||
|
||||
- (CGFloat)multiColumnTableView:(FLEXMultiColumnTableView *)tableView minWidthForContentCellInColumn:(NSInteger)column;
|
||||
- (CGFloat)multiColumnTableView:(FLEXMultiColumnTableView *)tableView heightForContentCellInRow:(NSInteger)row;
|
||||
- (CGFloat)heightForTopHeaderInTableView:(FLEXMultiColumnTableView *)tableView;
|
||||
- (CGFloat)widthForLeftHeaderInTableView:(FLEXMultiColumnTableView *)tableView;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface FLEXMultiColumnTableView : UIView
|
||||
|
||||
@property (nonatomic, weak) id<FLEXMultiColumnTableViewDataSource> dataSource;
|
||||
@property (nonatomic, weak) id<FLEXMultiColumnTableViewDelegate> delegate;
|
||||
|
||||
- (void)reloadData;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,339 @@
|
||||
//
|
||||
// PTMultiColumnTableView.m
|
||||
// PTMultiColumnTableViewDemo
|
||||
//
|
||||
// Created by Peng Tao on 15/11/16.
|
||||
// Copyright © 2015年 Peng Tao. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXMultiColumnTableView.h"
|
||||
#import "FLEXDBQueryRowCell.h"
|
||||
#import "FLEXTableLeftCell.h"
|
||||
#import "NSArray+FLEX.h"
|
||||
#import "FLEXColor.h"
|
||||
|
||||
@interface FLEXMultiColumnTableView () <
|
||||
UITableViewDataSource, UITableViewDelegate,
|
||||
UIScrollViewDelegate, FLEXDBQueryRowCellLayoutSource
|
||||
>
|
||||
|
||||
@property (nonatomic) UIScrollView *contentScrollView;
|
||||
@property (nonatomic) UIScrollView *headerScrollView;
|
||||
@property (nonatomic) UITableView *leftTableView;
|
||||
@property (nonatomic) UITableView *contentTableView;
|
||||
@property (nonatomic) UIView *leftHeader;
|
||||
|
||||
@property (nonatomic) NSArray<UIView *> *headerViews;
|
||||
|
||||
/// \c NSNotFound if no column selected
|
||||
@property (nonatomic) NSInteger sortColumn;
|
||||
@property (nonatomic) FLEXTableColumnHeaderSortType sortType;
|
||||
|
||||
@property (nonatomic, 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.autoresizingMask |= UIViewAutoresizingFlexibleWidth;
|
||||
self.autoresizingMask |= UIViewAutoresizingFlexibleHeight;
|
||||
self.autoresizingMask |= UIViewAutoresizingFlexibleTopMargin;
|
||||
self.backgroundColor = FLEXColor.groupedBackgroundColor;
|
||||
|
||||
[self loadHeaderScrollView];
|
||||
[self loadContentScrollView];
|
||||
[self loadLeftView];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (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 topInsets = 0.f;
|
||||
|
||||
if (@available (iOS 11.0, *)) {
|
||||
topInsets = self.safeAreaInsets.top;
|
||||
}
|
||||
|
||||
CGFloat contentWidth = 0.0;
|
||||
NSInteger columnsCount = self.numberOfColumns;
|
||||
for (int i = 0; i < columnsCount; i++) {
|
||||
contentWidth += CGRectGetWidth(self.headerViews[i].bounds);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - UI
|
||||
|
||||
- (void)loadHeaderScrollView {
|
||||
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;
|
||||
|
||||
UITableView *tableView = [UITableView new];
|
||||
tableView.delegate = self;
|
||||
tableView.dataSource = self;
|
||||
tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
|
||||
[tableView registerClass:[FLEXDBQueryRowCell class]
|
||||
forCellReuseIdentifier:kFLEXDBQueryRowCellReuse
|
||||
];
|
||||
|
||||
[scrollView addSubview:tableView];
|
||||
[self addSubview:scrollView];
|
||||
|
||||
self.contentScrollView = scrollView;
|
||||
self.contentTableView = tableView;
|
||||
}
|
||||
|
||||
- (void)loadLeftView {
|
||||
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 = FLEXColor.secondaryBackgroundColor;
|
||||
self.leftHeader = leftHeader;
|
||||
[self addSubview:leftHeader];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Data
|
||||
|
||||
- (void)reloadData {
|
||||
[self loadHeaderData];
|
||||
[self loadLeftViewData];
|
||||
[self loadContentData];
|
||||
}
|
||||
|
||||
- (void)loadHeaderData {
|
||||
// Remove existing headers, if any
|
||||
for (UIView *subview in self.headerViews) {
|
||||
[subview removeFromSuperview];
|
||||
}
|
||||
|
||||
__block CGFloat xOffset = 0;
|
||||
|
||||
self.headerViews = [NSArray flex_forEachUpTo:self.numberOfColumns map:^id(NSUInteger column) {
|
||||
FLEXTableColumnHeader *header = [FLEXTableColumnHeader new];
|
||||
header.titleLabel.text = [self columnTitle:column];
|
||||
|
||||
CGSize fittingSize = CGSizeMake(CGFLOAT_MAX, self.topHeaderHeight - 1);
|
||||
CGFloat width = self.columnMargin + MAX(
|
||||
[self minContentWidthForColumn:column],
|
||||
[header sizeThatFits:fittingSize].width
|
||||
);
|
||||
header.frame = CGRectMake(xOffset, 0, width, self.topHeaderHeight - 1);
|
||||
|
||||
if (column == self.sortColumn) {
|
||||
header.sortType = self.sortType;
|
||||
}
|
||||
|
||||
// Header tap gesture
|
||||
UITapGestureRecognizer *gesture = [[UITapGestureRecognizer alloc]
|
||||
initWithTarget:self action:@selector(contentHeaderTap:)
|
||||
];
|
||||
[header addGestureRecognizer:gesture];
|
||||
header.userInteractionEnabled = YES;
|
||||
|
||||
xOffset += width;
|
||||
[self.headerScrollView addSubview:header];
|
||||
return header;
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)contentHeaderTap:(UIGestureRecognizer *)gesture {
|
||||
NSInteger newSortColumn = [self.headerViews indexOfObject:gesture.view];
|
||||
FLEXTableColumnHeaderSortType newType = FLEXNextTableColumnHeaderSortType(self.sortType);
|
||||
|
||||
// Reset old header
|
||||
FLEXTableColumnHeader *oldHeader = (id)self.headerViews[self.sortColumn];
|
||||
oldHeader.sortType = FLEXTableColumnHeaderSortTypeNone;
|
||||
|
||||
// Update new header
|
||||
FLEXTableColumnHeader *newHeader = (id)self.headerViews[newSortColumn];
|
||||
newHeader.sortType = newType;
|
||||
|
||||
// Update self
|
||||
self.sortColumn = newSortColumn;
|
||||
self.sortType = newType;
|
||||
|
||||
// Notify delegate
|
||||
[self.delegate multiColumnTableView:self didSelectHeaderForColumn:newSortColumn sortType:newType];
|
||||
}
|
||||
|
||||
- (void)loadContentData {
|
||||
[self.contentTableView reloadData];
|
||||
}
|
||||
|
||||
- (void)loadLeftViewData {
|
||||
[self.leftTableView reloadData];
|
||||
}
|
||||
|
||||
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
// Alternating background color
|
||||
UIColor *backgroundColor = FLEXColor.primaryBackgroundColor;
|
||||
if (indexPath.row % 2 != 0) {
|
||||
backgroundColor = FLEXColor.secondaryBackgroundColor;
|
||||
}
|
||||
|
||||
// Left side table view for row numbers
|
||||
if (tableView == self.leftTableView) {
|
||||
FLEXTableLeftCell *cell = [FLEXTableLeftCell cellWithTableView:tableView];
|
||||
cell.contentView.backgroundColor = backgroundColor;
|
||||
cell.titlelabel.text = [self rowTitle:indexPath.row];
|
||||
return cell;
|
||||
}
|
||||
// Right side table view for data
|
||||
else {
|
||||
FLEXDBQueryRowCell *cell = [tableView
|
||||
dequeueReusableCellWithIdentifier:kFLEXDBQueryRowCellReuse forIndexPath:indexPath
|
||||
];
|
||||
|
||||
cell.contentView.backgroundColor = backgroundColor;
|
||||
cell.data = [self.dataSource contentForRow:indexPath.row];
|
||||
cell.layoutSource = self;
|
||||
NSAssert(cell.data.count == self.numberOfColumns, @"Count of data provided was incorrect");
|
||||
return cell;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
|
||||
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;
|
||||
}
|
||||
else if (scrollView == self.headerScrollView) {
|
||||
self.contentScrollView.contentOffset = scrollView.contentOffset;
|
||||
}
|
||||
else if (scrollView == self.leftTableView) {
|
||||
self.contentTableView.contentOffset = scrollView.contentOffset;
|
||||
}
|
||||
else if (scrollView == self.contentTableView) {
|
||||
self.leftTableView.contentOffset = scrollView.contentOffset;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark UITableView Delegate
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
if (tableView == self.leftTableView) {
|
||||
[self.contentTableView
|
||||
selectRowAtIndexPath:indexPath
|
||||
animated:NO
|
||||
scrollPosition:UITableViewScrollPositionNone
|
||||
];
|
||||
}
|
||||
else if (tableView == self.contentTableView) {
|
||||
[self.delegate multiColumnTableView:self didSelectRow:indexPath.row];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark FLEXDBQueryRowCellLayoutSource
|
||||
|
||||
- (CGFloat)dbQueryRowCell:(FLEXDBQueryRowCell *)dbQueryRowCell minXForColumn:(NSUInteger)column {
|
||||
return CGRectGetMinX(self.headerViews[column].frame);
|
||||
}
|
||||
|
||||
- (CGFloat)dbQueryRowCell:(FLEXDBQueryRowCell *)dbQueryRowCell widthForColumn:(NSUInteger)column {
|
||||
return CGRectGetWidth(self.headerViews[column].bounds);
|
||||
}
|
||||
|
||||
|
||||
#pragma mark DataSource Accessor
|
||||
|
||||
- (NSInteger)numberOfRows {
|
||||
return [self.dataSource numberOfRowsInTableView:self];
|
||||
}
|
||||
|
||||
- (NSInteger)numberOfColumns {
|
||||
return [self.dataSource numberOfColumnsInTableView:self];
|
||||
}
|
||||
|
||||
- (NSString *)columnTitle:(NSInteger)column {
|
||||
return [self.dataSource columnTitle:column];
|
||||
}
|
||||
|
||||
- (NSString *)rowTitle:(NSInteger)row {
|
||||
return [self.dataSource rowTitle:row];
|
||||
}
|
||||
|
||||
- (CGFloat)minContentWidthForColumn:(NSInteger)column {
|
||||
return [self.dataSource multiColumnTableView:self minWidthForContentCellInColumn:column];
|
||||
}
|
||||
|
||||
- (CGFloat)contentHeightForRow:(NSInteger)row {
|
||||
return [self.dataSource multiColumnTableView:self heightForContentCellInRow:row];
|
||||
}
|
||||
|
||||
- (CGFloat)topHeaderHeight {
|
||||
return [self.dataSource heightForTopHeaderInTableView:self];
|
||||
}
|
||||
|
||||
- (CGFloat)leftHeaderWidth {
|
||||
return [self.dataSource widthForLeftHeaderInTableView:self];
|
||||
}
|
||||
|
||||
- (CGFloat)columnMargin {
|
||||
return kColumnMargin;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,14 @@
|
||||
//
|
||||
// FLEXRealmDatabaseManager.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tim Oliver on 28/01/2016.
|
||||
// Copyright © 2016 Realm. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "FLEXDatabaseManager.h"
|
||||
|
||||
@interface FLEXRealmDatabaseManager : NSObject <FLEXDatabaseManager>
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,102 @@
|
||||
//
|
||||
// FLEXRealmDatabaseManager.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tim Oliver on 28/01/2016.
|
||||
// Copyright © 2016 Realm. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXRealmDatabaseManager.h"
|
||||
#import "NSArray+FLEX.h"
|
||||
#import "FLEXSQLResult.h"
|
||||
|
||||
#if __has_include(<Realm/Realm.h>)
|
||||
#import <Realm/Realm.h>
|
||||
#import <Realm/RLMRealm_Dynamic.h>
|
||||
#else
|
||||
#import "FLEXRealmDefines.h"
|
||||
#endif
|
||||
|
||||
@interface FLEXRealmDatabaseManager ()
|
||||
|
||||
@property (nonatomic, copy) NSString *path;
|
||||
@property (nonatomic) RLMRealm *realm;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXRealmDatabaseManager
|
||||
static Class RLMRealmClass = 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 = path;
|
||||
|
||||
if (![self open]) {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL)open {
|
||||
Class configurationClass = NSClassFromString(@"RLMRealmConfiguration");
|
||||
if (!RLMRealmClass || !configurationClass) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSError *error = nil;
|
||||
id configuration = [configurationClass new];
|
||||
[(RLMRealmConfiguration *)configuration setFileURL:[NSURL fileURLWithPath:self.path]];
|
||||
self.realm = [RLMRealmClass realmWithConfiguration:configuration error:&error];
|
||||
|
||||
return (error == nil);
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)queryAllTables {
|
||||
// Map each schema to its name
|
||||
NSArray<NSString *> *tableNames = [self.realm.schema.objectSchema flex_mapped:^id(RLMObjectSchema *schema, NSUInteger idx) {
|
||||
return schema.className ?: nil;
|
||||
}];
|
||||
|
||||
return [tableNames sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)];
|
||||
}
|
||||
|
||||
- (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<NSArray *> *)queryAllDataInTable:(NSString *)tableName {
|
||||
RLMObjectSchema *objectSchema = [self.realm.schema schemaForClassName:tableName];
|
||||
RLMResults *results = [self.realm allObjects:tableName];
|
||||
if (results.count == 0 || !objectSchema) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
// 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,46 @@
|
||||
//
|
||||
// Realm.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tim Oliver on 16/02/2016.
|
||||
// Copyright © 2016 Realm. All rights reserved.
|
||||
//
|
||||
|
||||
#if __has_include(<Realm/Realm.h>)
|
||||
#else
|
||||
|
||||
@class RLMObject, RLMResults, RLMRealm, RLMRealmConfiguration, RLMSchema, RLMObjectSchema, RLMProperty;
|
||||
|
||||
@interface RLMRealmConfiguration : NSObject
|
||||
@property (nonatomic, copy) NSURL *fileURL;
|
||||
@end
|
||||
|
||||
@interface RLMRealm : NSObject
|
||||
@property (nonatomic, readonly) RLMSchema *schema;
|
||||
+ (RLMRealm *)realmWithConfiguration:(RLMRealmConfiguration *)configuration error:(NSError **)error;
|
||||
- (RLMResults *)allObjects:(NSString *)className;
|
||||
@end
|
||||
|
||||
@interface RLMSchema : NSObject
|
||||
@property (nonatomic, readonly) NSArray<RLMObjectSchema *> *objectSchema;
|
||||
- (RLMObjectSchema *)schemaForClassName:(NSString *)className;
|
||||
@end
|
||||
|
||||
@interface RLMObjectSchema : NSObject
|
||||
@property (nonatomic, readonly) NSString *className;
|
||||
@property (nonatomic, readonly) NSArray<RLMProperty *> *properties;
|
||||
@end
|
||||
|
||||
@interface RLMProperty : NSString
|
||||
@property (nonatomic, readonly) NSString *name;
|
||||
@end
|
||||
|
||||
@interface RLMResults : NSObject <NSFastEnumeration>
|
||||
@property (nonatomic, readonly) NSInteger count;
|
||||
@end
|
||||
|
||||
@interface RLMObject : NSObject
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,48 @@
|
||||
//
|
||||
// FLEXSQLResult.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 3/3/20.
|
||||
// Copyright © 2020 FLEX Team. 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;
|
||||
/// Describes the result of a known failed execution
|
||||
+ (instancetype)error:(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 value of YES means this is surely an error,
|
||||
/// but it still might be an error even with a value of NO
|
||||
@property (nonatomic, readonly) BOOL isError;
|
||||
|
||||
/// 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,53 @@
|
||||
//
|
||||
// FLEXSQLResult.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tanner on 3/3/20.
|
||||
// Copyright © 2020 FLEX Team. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXSQLResult.h"
|
||||
#import "NSArray+FLEX.h"
|
||||
|
||||
@implementation FLEXSQLResult
|
||||
@synthesize keyedRows = _keyedRows;
|
||||
|
||||
+ (instancetype)message:(NSString *)message {
|
||||
return [[self alloc] initWithMessage:message columns:nil rows:nil];
|
||||
}
|
||||
|
||||
+ (instancetype)error:(NSString *)message {
|
||||
FLEXSQLResult *result = [self message:message];
|
||||
result->_isError = YES;
|
||||
return result;
|
||||
}
|
||||
|
||||
+ (instancetype)columns:(NSArray<NSString *> *)columnNames rows:(NSArray<NSArray<NSString *> *> *)rowData {
|
||||
return [[self alloc] initWithMessage:nil columns:columnNames rows:rowData];
|
||||
}
|
||||
|
||||
- (instancetype)initWithMessage:(NSString *)message columns:(NSArray<NSString *> *)columns rows:(NSArray<NSArray<NSString *> *> *)rows {
|
||||
NSParameterAssert(message || (columns && rows));
|
||||
NSParameterAssert(rows.count == 0 || 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
|
||||
@@ -0,0 +1,32 @@
|
||||
//
|
||||
// PTDatabaseManager.h
|
||||
// Derived from:
|
||||
//
|
||||
// FMDatabase.h
|
||||
// FMDB( https://github.com/ccgus/fmdb )
|
||||
//
|
||||
// Created by Peng Tao on 15/11/23.
|
||||
//
|
||||
// Licensed to Flying Meat Inc. under one or more contributor license agreements.
|
||||
// See the LICENSE file distributed with this work for the terms under
|
||||
// which Flying Meat Inc. licenses this file to you.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "FLEXDatabaseManager.h"
|
||||
#import "FLEXSQLResult.h"
|
||||
|
||||
@interface FLEXSQLiteDatabaseManager : NSObject <FLEXDatabaseManager>
|
||||
|
||||
/// Contains the result of the last operation, which may be an error
|
||||
@property (nonatomic, readonly) FLEXSQLResult *lastResult;
|
||||
/// Calls into \c sqlite3_last_insert_rowid()
|
||||
@property (nonatomic, readonly) NSInteger lastRowID;
|
||||
|
||||
/// Given a statement like 'SELECT * from @table where @col = @val' and arguments
|
||||
/// like { @"table": @"Album", @"col": @"year", @"val" @1 } this method will
|
||||
/// invoke the statement and properly bind the given arguments to the statement.
|
||||
///
|
||||
/// You may pass NSStrings, NSData, NSNumbers, or NSNulls as values.
|
||||
- (FLEXSQLResult *)executeStatement:(NSString *)statement arguments:(NSDictionary<NSString *, id> *)args;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,329 @@
|
||||
//
|
||||
// PTDatabaseManager.m
|
||||
// PTDatabaseReader
|
||||
//
|
||||
// Created by Peng Tao on 15/11/23.
|
||||
// Copyright © 2015年 Peng Tao. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXSQLiteDatabaseManager.h"
|
||||
#import "FLEXManager.h"
|
||||
#import "NSArray+FLEX.h"
|
||||
#import "FLEXRuntimeConstants.h"
|
||||
#import <sqlite3.h>
|
||||
|
||||
#define kQuery(name, str) static NSString * const QUERY_##name = str
|
||||
|
||||
kQuery(TABLENAMES, @"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name");
|
||||
kQuery(ROWIDS, @"SELECT rowid FROM \"%@\" ORDER BY rowid ASC");
|
||||
|
||||
@interface FLEXSQLiteDatabaseManager ()
|
||||
@property (nonatomic) sqlite3 *db;
|
||||
@property (nonatomic, copy) NSString *path;
|
||||
@end
|
||||
|
||||
@implementation FLEXSQLiteDatabaseManager
|
||||
|
||||
#pragma mark - FLEXDatabaseManager
|
||||
|
||||
+ (instancetype)managerForDatabase:(NSString *)path {
|
||||
return [[self alloc] initWithPath:path];
|
||||
}
|
||||
|
||||
- (instancetype)initWithPath:(NSString *)path {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
self.path = path;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc {
|
||||
[self close];
|
||||
}
|
||||
|
||||
- (BOOL)open {
|
||||
if (self.db) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
int err = sqlite3_open(self.path.UTF8String, &_db);
|
||||
|
||||
#if SQLITE_HAS_CODEC
|
||||
NSString *defaultSqliteDatabasePassword = FLEXManager.sharedManager.defaultSqliteDatabasePassword;
|
||||
if (defaultSqliteDatabasePassword) {
|
||||
const char *key = defaultSqliteDatabasePassword.UTF8String;
|
||||
sqlite3_key(_db, key, (int)strlen(key));
|
||||
}
|
||||
#endif
|
||||
|
||||
if (err != SQLITE_OK) {
|
||||
return [self storeErrorForLastTask:@"Open"];
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)close {
|
||||
if (!self.db) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
int rc;
|
||||
BOOL retry, triedFinalizingOpenStatements = NO;
|
||||
|
||||
do {
|
||||
retry = NO;
|
||||
rc = sqlite3_close(_db);
|
||||
if (SQLITE_BUSY == rc || SQLITE_LOCKED == rc) {
|
||||
if (!triedFinalizingOpenStatements) {
|
||||
triedFinalizingOpenStatements = YES;
|
||||
sqlite3_stmt *pStmt;
|
||||
while ((pStmt = sqlite3_next_stmt(_db, nil)) !=0) {
|
||||
NSLog(@"Closing leaked statement");
|
||||
sqlite3_finalize(pStmt);
|
||||
retry = YES;
|
||||
}
|
||||
}
|
||||
} else if (SQLITE_OK != rc) {
|
||||
[self storeErrorForLastTask:@"Close"];
|
||||
self.db = nil;
|
||||
return NO;
|
||||
}
|
||||
} while (retry);
|
||||
|
||||
self.db = nil;
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (NSInteger)lastRowID {
|
||||
return (NSInteger)sqlite3_last_insert_rowid(self.db);
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)queryAllTables {
|
||||
return [[self executeStatement:QUERY_TABLENAMES].rows flex_mapped:^id(NSArray *table, NSUInteger idx) {
|
||||
return table.firstObject;
|
||||
}] ?: @[];
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)queryAllColumnsOfTable:(NSString *)tableName {
|
||||
NSString *sql = [NSString stringWithFormat:@"PRAGMA table_info('%@')",tableName];
|
||||
FLEXSQLResult *results = [self executeStatement:sql];
|
||||
|
||||
// https://github.com/FLEXTool/FLEX/issues/554
|
||||
if (!results.keyedRows.count) {
|
||||
sql = [NSString stringWithFormat:@"SELECT * FROM pragma_table_info('%@')", tableName];
|
||||
results = [self executeStatement:sql];
|
||||
|
||||
// Fallback to empty query
|
||||
if (!results.keyedRows.count) {
|
||||
sql = [NSString stringWithFormat:@"SELECT * FROM \"%@\" where 0=1", tableName];
|
||||
return [self executeStatement:sql].columns ?: @[];
|
||||
}
|
||||
}
|
||||
|
||||
return [results.keyedRows flex_mapped:^id(NSDictionary *column, NSUInteger idx) {
|
||||
return column[@"name"];
|
||||
}] ?: @[];
|
||||
}
|
||||
|
||||
- (NSArray<NSArray *> *)queryAllDataInTable:(NSString *)tableName {
|
||||
NSString *command = [NSString stringWithFormat:@"SELECT * FROM \"%@\"", tableName];
|
||||
return [self executeStatement:command].rows ?: @[];
|
||||
}
|
||||
|
||||
- (NSArray<NSString *> *)queryRowIDsInTable:(NSString *)tableName {
|
||||
NSString *command = [NSString stringWithFormat:QUERY_ROWIDS, tableName];
|
||||
NSArray<NSArray<NSString *> *> *data = [self executeStatement:command].rows ?: @[];
|
||||
|
||||
return [data flex_mapped:^id(NSArray<NSString *> *obj, NSUInteger idx) {
|
||||
return obj.firstObject;
|
||||
}];
|
||||
}
|
||||
|
||||
- (FLEXSQLResult *)executeStatement:(NSString *)sql {
|
||||
return [self executeStatement:sql arguments:nil];
|
||||
}
|
||||
|
||||
- (FLEXSQLResult *)executeStatement:(NSString *)sql arguments:(NSDictionary *)args {
|
||||
[self open];
|
||||
|
||||
FLEXSQLResult *result = nil;
|
||||
|
||||
sqlite3_stmt *pstmt;
|
||||
int status;
|
||||
if ((status = sqlite3_prepare_v2(_db, sql.UTF8String, -1, &pstmt, 0)) == SQLITE_OK) {
|
||||
NSMutableArray<NSArray *> *rows = [NSMutableArray new];
|
||||
|
||||
// Bind parameters, if any
|
||||
if (![self bindParameters:args toStatement:pstmt]) {
|
||||
return self.lastResult;
|
||||
}
|
||||
|
||||
// Grab columns (columnCount will be 0 for insert/update/delete)
|
||||
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
|
||||
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) {
|
||||
// columnCount will be 0 for insert/update/delete
|
||||
if (rows.count || columnCount > 0) {
|
||||
// We executed a SELECT query
|
||||
result = _lastResult = [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 = _lastResult = [FLEXSQLResult message:message];
|
||||
}
|
||||
} else {
|
||||
// An error occured executing the query
|
||||
result = _lastResult = [self errorResult:@"Execution"];
|
||||
}
|
||||
} else {
|
||||
// An error occurred creating the prepared statement
|
||||
result = _lastResult = [self errorResult:@"Prepared statement"];
|
||||
}
|
||||
|
||||
sqlite3_finalize(pstmt);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
/// @return YES on success, NO if an error was encountered and stored in \c lastResult
|
||||
- (BOOL)bindParameters:(NSDictionary *)args toStatement:(sqlite3_stmt *)pstmt {
|
||||
for (NSString *param in args.allKeys) {
|
||||
int status = SQLITE_OK, idx = sqlite3_bind_parameter_index(pstmt, param.UTF8String);
|
||||
id value = args[param];
|
||||
|
||||
if (idx == 0) {
|
||||
// No parameter matching that arg
|
||||
@throw NSInternalInconsistencyException;
|
||||
}
|
||||
|
||||
// Null
|
||||
if ([value isKindOfClass:[NSNull class]]) {
|
||||
status = sqlite3_bind_null(pstmt, idx);
|
||||
}
|
||||
// String params
|
||||
else if ([value isKindOfClass:[NSString class]]) {
|
||||
const char *str = [value UTF8String];
|
||||
status = sqlite3_bind_text(pstmt, idx, str, (int)strlen(str), SQLITE_TRANSIENT);
|
||||
}
|
||||
// Data params
|
||||
else if ([value isKindOfClass:[NSData class]]) {
|
||||
const void *blob = [value bytes];
|
||||
status = sqlite3_bind_blob64(pstmt, idx, blob, [value length], SQLITE_TRANSIENT);
|
||||
}
|
||||
// Primitive params
|
||||
else if ([value isKindOfClass:[NSNumber class]]) {
|
||||
FLEXTypeEncoding type = [value objCType][0];
|
||||
switch (type) {
|
||||
case FLEXTypeEncodingCBool:
|
||||
case FLEXTypeEncodingChar:
|
||||
case FLEXTypeEncodingUnsignedChar:
|
||||
case FLEXTypeEncodingShort:
|
||||
case FLEXTypeEncodingUnsignedShort:
|
||||
case FLEXTypeEncodingInt:
|
||||
case FLEXTypeEncodingUnsignedInt:
|
||||
case FLEXTypeEncodingLong:
|
||||
case FLEXTypeEncodingUnsignedLong:
|
||||
case FLEXTypeEncodingLongLong:
|
||||
case FLEXTypeEncodingUnsignedLongLong:
|
||||
status = sqlite3_bind_int64(pstmt, idx, (sqlite3_int64)[value longValue]);
|
||||
break;
|
||||
|
||||
case FLEXTypeEncodingFloat:
|
||||
case FLEXTypeEncodingDouble:
|
||||
status = sqlite3_bind_double(pstmt, idx, [value doubleValue]);
|
||||
break;
|
||||
|
||||
default:
|
||||
@throw NSInternalInconsistencyException;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Unsupported type
|
||||
else {
|
||||
@throw NSInternalInconsistencyException;
|
||||
}
|
||||
|
||||
if (status != SQLITE_OK) {
|
||||
return [self storeErrorForLastTask:
|
||||
[NSString stringWithFormat:@"Binding param named '%@'", param]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)storeErrorForLastTask:(NSString *)action {
|
||||
_lastResult = [self errorResult:action];
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (FLEXSQLResult *)errorResult:(NSString *)description {
|
||||
const char *error = sqlite3_errmsg(_db);
|
||||
NSString *message = error ? @(error) : [NSString
|
||||
stringWithFormat:@"(%@: empty error)", description
|
||||
];
|
||||
|
||||
return [FLEXSQLResult error:message];
|
||||
}
|
||||
|
||||
- (id)objectForColumnIndex:(int)columnIdx stmt:(sqlite3_stmt*)stmt {
|
||||
int columnType = sqlite3_column_type(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;
|
||||
}
|
||||
}
|
||||
|
||||
- (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 void *blob = sqlite3_column_blob(stmt, columnIdx);
|
||||
NSInteger size = (NSInteger)sqlite3_column_bytes(stmt, columnIdx);
|
||||
|
||||
return blob ? [NSData dataWithBytes:blob length:size] : nil;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,38 @@
|
||||
//
|
||||
// FLEXTableContentHeaderCell.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Peng Tao on 15/11/26.
|
||||
// Copyright © 2015年 f. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
typedef NS_ENUM(NSUInteger, FLEXTableColumnHeaderSortType) {
|
||||
FLEXTableColumnHeaderSortTypeNone = 0,
|
||||
FLEXTableColumnHeaderSortTypeAsc,
|
||||
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) NSInteger index;
|
||||
@property (nonatomic, readonly) UILabel *titleLabel;
|
||||
|
||||
@property (nonatomic) FLEXTableColumnHeaderSortType sortType;
|
||||
|
||||
@end
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
//
|
||||
// FLEXTableContentHeaderCell.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Peng Tao on 15/11/26.
|
||||
// Copyright © 2015年 f. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXTableColumnHeader.h"
|
||||
#import "FLEXColor.h"
|
||||
#import "UIFont+FLEX.h"
|
||||
#import "FLEXUtility.h"
|
||||
|
||||
static const CGFloat kMargin = 5;
|
||||
static const CGFloat kArrowWidth = 20;
|
||||
|
||||
@interface FLEXTableColumnHeader ()
|
||||
@property (nonatomic, readonly) UILabel *arrowLabel;
|
||||
@property (nonatomic, readonly) UIView *lineView;
|
||||
@end
|
||||
|
||||
@implementation FLEXTableColumnHeader
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame {
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
self.backgroundColor = FLEXColor.secondaryBackgroundColor;
|
||||
|
||||
_titleLabel = [UILabel new];
|
||||
_titleLabel.font = UIFont.flex_defaultTableCellFont;
|
||||
[self addSubview:_titleLabel];
|
||||
|
||||
_arrowLabel = [UILabel new];
|
||||
_arrowLabel.font = UIFont.flex_defaultTableCellFont;
|
||||
[self addSubview:_arrowLabel];
|
||||
|
||||
_lineView = [UIView new];
|
||||
_lineView.backgroundColor = FLEXColor.hairlineColor;
|
||||
[self addSubview:_lineView];
|
||||
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setSortType:(FLEXTableColumnHeaderSortType)type {
|
||||
_sortType = type;
|
||||
|
||||
switch (type) {
|
||||
case FLEXTableColumnHeaderSortTypeNone:
|
||||
_arrowLabel.text = @"";
|
||||
break;
|
||||
case FLEXTableColumnHeaderSortTypeAsc:
|
||||
_arrowLabel.text = @"⬆️";
|
||||
break;
|
||||
case FLEXTableColumnHeaderSortTypeDesc:
|
||||
_arrowLabel.text = @"⬇️";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)layoutSubviews {
|
||||
[super layoutSubviews];
|
||||
|
||||
CGSize size = self.frame.size;
|
||||
|
||||
self.titleLabel.frame = CGRectMake(kMargin, 0, size.width - kArrowWidth - kMargin, size.height);
|
||||
self.arrowLabel.frame = CGRectMake(size.width - kArrowWidth, 0, kArrowWidth, size.height);
|
||||
self.lineView.frame = CGRectMake(size.width - 1, 2, FLEXPointsToPixels(1), size.height - 4);
|
||||
}
|
||||
|
||||
- (CGSize)sizeThatFits:(CGSize)size {
|
||||
CGFloat margins = kArrowWidth - 2 * kMargin;
|
||||
size = CGSizeMake(size.width - margins, size.height);
|
||||
CGFloat width = [_titleLabel sizeThatFits:size].width + margins;
|
||||
return CGSizeMake(width, size.height);
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,36 @@
|
||||
//
|
||||
// PTTableContentViewController.h
|
||||
// PTDatabaseReader
|
||||
//
|
||||
// Created by Peng Tao on 15/11/23.
|
||||
// Copyright © 2015年 Peng Tao. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "FLEXDatabaseManager.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FLEXTableContentViewController : UIViewController
|
||||
|
||||
/// Display a mutable table with the given columns, rows, and name.
|
||||
///
|
||||
/// @param columnNames self explanatory.
|
||||
/// @param rowData an array of rows, where each row is an array of column data.
|
||||
/// @param rowIDs an array of stringy row IDs. Required for deleting rows.
|
||||
/// @param tableName an optional name of the table being viewed, if any. Enables adding rows.
|
||||
/// @param databaseManager an optional manager to allow modifying the table.
|
||||
/// Required for deleting rows. Required for adding rows if \c tableName is supplied.
|
||||
+ (instancetype)columns:(NSArray<NSString *> *)columnNames
|
||||
rows:(NSArray<NSArray<NSString *> *> *)rowData
|
||||
rowIDs:(NSArray<NSString *> *)rowIDs
|
||||
tableName:(NSString *)tableName
|
||||
database:(id<FLEXDatabaseManager>)databaseManager;
|
||||
|
||||
/// Display an immutable table with the given columns and rows.
|
||||
+ (instancetype)columns:(NSArray<NSString *> *)columnNames
|
||||
rows:(NSArray<NSArray<NSString *> *> *)rowData;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -0,0 +1,359 @@
|
||||
//
|
||||
// PTTableContentViewController.m
|
||||
// PTDatabaseReader
|
||||
//
|
||||
// Created by Peng Tao on 15/11/23.
|
||||
// Copyright © 2015年 Peng Tao. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXTableContentViewController.h"
|
||||
#import "FLEXTableRowDataViewController.h"
|
||||
#import "FLEXMultiColumnTableView.h"
|
||||
#import "FLEXWebViewController.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "UIBarButtonItem+FLEX.h"
|
||||
|
||||
@interface FLEXTableContentViewController () <
|
||||
FLEXMultiColumnTableViewDataSource, FLEXMultiColumnTableViewDelegate
|
||||
>
|
||||
@property (nonatomic, readonly) NSArray<NSString *> *columns;
|
||||
@property (nonatomic) NSMutableArray<NSArray *> *rows;
|
||||
@property (nonatomic, readonly) NSString *tableName;
|
||||
@property (nonatomic, nullable) NSMutableArray<NSString *> *rowIDs;
|
||||
@property (nonatomic, readonly, nullable) id<FLEXDatabaseManager> databaseManager;
|
||||
|
||||
@property (nonatomic, readonly) BOOL canRefresh;
|
||||
|
||||
@property (nonatomic) FLEXMultiColumnTableView *multiColumnView;
|
||||
@end
|
||||
|
||||
@implementation FLEXTableContentViewController
|
||||
|
||||
+ (instancetype)columns:(NSArray<NSString *> *)columnNames
|
||||
rows:(NSArray<NSArray<NSString *> *> *)rowData
|
||||
rowIDs:(NSArray<NSString *> *)rowIDs
|
||||
tableName:(NSString *)tableName
|
||||
database:(id<FLEXDatabaseManager>)databaseManager {
|
||||
return [[self alloc]
|
||||
initWithColumns:columnNames
|
||||
rows:rowData
|
||||
rowIDs:rowIDs
|
||||
tableName:tableName
|
||||
database:databaseManager
|
||||
];
|
||||
}
|
||||
|
||||
+ (instancetype)columns:(NSArray<NSString *> *)cols
|
||||
rows:(NSArray<NSArray<NSString *> *> *)rowData {
|
||||
return [[self alloc] initWithColumns:cols rows:rowData rowIDs:nil tableName:nil database:nil];
|
||||
}
|
||||
|
||||
- (instancetype)initWithColumns:(NSArray<NSString *> *)columnNames
|
||||
rows:(NSArray<NSArray<NSString *> *> *)rowData
|
||||
rowIDs:(nullable NSArray<NSString *> *)rowIDs
|
||||
tableName:(nullable NSString *)tableName
|
||||
database:(nullable id<FLEXDatabaseManager>)databaseManager {
|
||||
// Must supply all optional parameters as one, or none
|
||||
BOOL all = rowIDs && tableName && databaseManager;
|
||||
BOOL none = !rowIDs && !tableName && !databaseManager;
|
||||
NSParameterAssert(all || none);
|
||||
|
||||
self = [super init];
|
||||
if (self) {
|
||||
self->_columns = columnNames.copy;
|
||||
self->_rows = rowData.mutableCopy;
|
||||
self->_rowIDs = rowIDs.mutableCopy;
|
||||
self->_tableName = tableName.copy;
|
||||
self->_databaseManager = databaseManager;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)loadView {
|
||||
[super loadView];
|
||||
|
||||
[self.view addSubview:self.multiColumnView];
|
||||
}
|
||||
|
||||
- (void)viewDidLoad {
|
||||
[super viewDidLoad];
|
||||
self.title = self.tableName;
|
||||
[self.multiColumnView reloadData];
|
||||
[self setupToolbarItems];
|
||||
}
|
||||
|
||||
- (FLEXMultiColumnTableView *)multiColumnView {
|
||||
if (!_multiColumnView) {
|
||||
_multiColumnView = [[FLEXMultiColumnTableView alloc]
|
||||
initWithFrame:FLEXRectSetSize(CGRectZero, self.view.frame.size)
|
||||
];
|
||||
|
||||
_multiColumnView.dataSource = self;
|
||||
_multiColumnView.delegate = self;
|
||||
}
|
||||
|
||||
return _multiColumnView;
|
||||
}
|
||||
|
||||
- (BOOL)canRefresh {
|
||||
return self.databaseManager && self.tableName;
|
||||
}
|
||||
|
||||
#pragma mark MultiColumnTableView DataSource
|
||||
|
||||
- (NSInteger)numberOfColumnsInTableView:(FLEXMultiColumnTableView *)tableView {
|
||||
return self.columns.count;
|
||||
}
|
||||
|
||||
- (NSInteger)numberOfRowsInTableView:(FLEXMultiColumnTableView *)tableView {
|
||||
return self.rows.count;
|
||||
}
|
||||
|
||||
- (NSString *)columnTitle:(NSInteger)column {
|
||||
return self.columns[column];
|
||||
}
|
||||
|
||||
- (NSString *)rowTitle:(NSInteger)row {
|
||||
return @(row).stringValue;
|
||||
}
|
||||
|
||||
- (NSArray *)contentForRow:(NSInteger)row {
|
||||
return self.rows[row];
|
||||
}
|
||||
|
||||
- (CGFloat)multiColumnTableView:(FLEXMultiColumnTableView *)tableView
|
||||
heightForContentCellInRow:(NSInteger)row {
|
||||
return 40;
|
||||
}
|
||||
|
||||
- (CGFloat)multiColumnTableView:(FLEXMultiColumnTableView *)tableView
|
||||
minWidthForContentCellInColumn:(NSInteger)column {
|
||||
return 100;
|
||||
}
|
||||
|
||||
- (CGFloat)heightForTopHeaderInTableView:(FLEXMultiColumnTableView *)tableView {
|
||||
return 40;
|
||||
}
|
||||
|
||||
- (CGFloat)widthForLeftHeaderInTableView:(FLEXMultiColumnTableView *)tableView {
|
||||
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 MultiColumnTableView Delegate
|
||||
|
||||
- (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];
|
||||
}];
|
||||
|
||||
NSArray<NSString *> *values = [self.rows[row] flex_mapped:^id(NSString *value, NSUInteger idx) {
|
||||
return [NSString stringWithFormat:@"'%@'", value];
|
||||
}];
|
||||
|
||||
[FLEXAlert makeAlert:^(FLEXAlert *make) {
|
||||
make.title([@"Row " stringByAppendingString:@(row).stringValue]);
|
||||
NSString *message = [fields componentsJoinedByString:@"\n\n"];
|
||||
make.message(message);
|
||||
make.button(@"Copy").handler(^(NSArray<NSString *> *strings) {
|
||||
UIPasteboard.generalPasteboard.string = message;
|
||||
});
|
||||
make.button(@"Copy as CSV").handler(^(NSArray<NSString *> *strings) {
|
||||
UIPasteboard.generalPasteboard.string = [values componentsJoinedByString:@", "];
|
||||
});
|
||||
make.button(@"Focus on Row").handler(^(NSArray<NSString *> *strings) {
|
||||
UIViewController *focusedRow = [FLEXTableRowDataViewController
|
||||
rows:[NSDictionary dictionaryWithObjects:self.rows[row] forKeys:self.columns]
|
||||
];
|
||||
[self.navigationController pushViewController:focusedRow animated:YES];
|
||||
});
|
||||
|
||||
// Option to delete row
|
||||
BOOL hasRowID = self.rows.count && row < self.rows.count;
|
||||
if (hasRowID && self.canRefresh) {
|
||||
make.button(@"Delete").destructiveStyle().handler(^(NSArray<NSString *> *strings) {
|
||||
NSString *deleteRow = [NSString stringWithFormat:
|
||||
@"DELETE FROM %@ WHERE rowid = %@",
|
||||
self.tableName, self.rowIDs[row]
|
||||
];
|
||||
|
||||
[self executeStatementAndShowResult:deleteRow completion:^(BOOL success) {
|
||||
// Remove deleted row and reload view
|
||||
if (success) {
|
||||
[self reloadTableDataFromDB];
|
||||
}
|
||||
}];
|
||||
});
|
||||
}
|
||||
|
||||
make.button(@"Dismiss").cancelStyle();
|
||||
} showFrom:self];
|
||||
}
|
||||
|
||||
- (void)multiColumnTableView:(FLEXMultiColumnTableView *)tableView
|
||||
didSelectHeaderForColumn:(NSInteger)column
|
||||
sortType:(FLEXTableColumnHeaderSortType)sortType {
|
||||
|
||||
NSArray<NSArray *> *sortContentData = [self.rows
|
||||
sortedArrayWithOptions:NSSortStable
|
||||
usingComparator:^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:options:)] &&
|
||||
[b respondsToSelector:@selector(compare:options:)]) {
|
||||
return [a compare:b options:NSNumericSearch];
|
||||
}
|
||||
|
||||
if ([a respondsToSelector:@selector(compare:)] && [b respondsToSelector:@selector(compare:)]) {
|
||||
return [a compare:b];
|
||||
}
|
||||
|
||||
return NSOrderedSame;
|
||||
}
|
||||
];
|
||||
|
||||
if (sortType == FLEXTableColumnHeaderSortTypeDesc) {
|
||||
sortContentData = sortContentData.reverseObjectEnumerator.allObjects.copy;
|
||||
}
|
||||
|
||||
self.rows = sortContentData.mutableCopy;
|
||||
[self.multiColumnView reloadData];
|
||||
}
|
||||
|
||||
#pragma mark - About Transition
|
||||
|
||||
- (void)willTransitionToTraitCollection:(UITraitCollection *)newCollection
|
||||
withTransitionCoordinator:(id <UIViewControllerTransitionCoordinator>)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);
|
||||
}
|
||||
else {
|
||||
self.multiColumnView.frame = CGRectMake(0, 64, self.view.frame.size.width, self.view.frame.size.height - 64);
|
||||
}
|
||||
|
||||
[self.view setNeedsLayout];
|
||||
} completion:nil];
|
||||
}
|
||||
|
||||
#pragma mark - Toolbar
|
||||
|
||||
- (void)setupToolbarItems {
|
||||
// We do not support modifying realm databases
|
||||
if (![self.databaseManager respondsToSelector:@selector(executeStatement:)]) {
|
||||
return;
|
||||
}
|
||||
|
||||
UIBarButtonItem *trashButton = FLEXBarButtonItemSystem(Trash, self, @selector(trashPressed));
|
||||
UIBarButtonItem *addButton = FLEXBarButtonItemSystem(Add, self, @selector(addPressed));
|
||||
|
||||
// Only allow adding rows or deleting rows if we have a table name
|
||||
trashButton.enabled = self.canRefresh;
|
||||
addButton.enabled = self.canRefresh;
|
||||
|
||||
self.toolbarItems = @[
|
||||
UIBarButtonItem.flex_flexibleSpace,
|
||||
addButton,
|
||||
UIBarButtonItem.flex_flexibleSpace,
|
||||
[trashButton flex_withTintColor:UIColor.redColor],
|
||||
];
|
||||
}
|
||||
|
||||
- (void)trashPressed {
|
||||
NSParameterAssert(self.tableName);
|
||||
|
||||
[FLEXAlert makeAlert:^(FLEXAlert *make) {
|
||||
make.title(@"Delete All Rows");
|
||||
make.message(@"All rows in this table will be permanently deleted.\nDo you want to proceed?");
|
||||
|
||||
make.button(@"Yes, I'm sure").destructiveStyle().handler(^(NSArray<NSString *> *strings) {
|
||||
NSString *deleteAll = [NSString stringWithFormat:@"DELETE FROM %@", self.tableName];
|
||||
[self executeStatementAndShowResult:deleteAll completion:^(BOOL success) {
|
||||
// Only dismiss on success
|
||||
if (success) {
|
||||
[self.navigationController popViewControllerAnimated:YES];
|
||||
}
|
||||
}];
|
||||
});
|
||||
make.button(@"Cancel").cancelStyle();
|
||||
} showFrom:self];
|
||||
}
|
||||
|
||||
- (void)addPressed {
|
||||
NSParameterAssert(self.tableName);
|
||||
|
||||
[FLEXAlert makeAlert:^(FLEXAlert *make) {
|
||||
make.title(@"Add a New Row");
|
||||
make.message(@"Comma separate values to use in an INSERT statement.\n\n");
|
||||
make.message(@"INSERT INTO [table] VALUES (your_input)");
|
||||
make.textField(@"5, 'John Smith', 14,...");
|
||||
make.button(@"Insert").handler(^(NSArray<NSString *> *strings) {
|
||||
NSString *statement = [NSString stringWithFormat:
|
||||
@"INSERT INTO %@ VALUES (%@)", self.tableName, strings[0]
|
||||
];
|
||||
|
||||
[self executeStatementAndShowResult:statement completion:^(BOOL success) {
|
||||
if (success) {
|
||||
[self reloadTableDataFromDB];
|
||||
}
|
||||
}];
|
||||
});
|
||||
make.button(@"Cancel").cancelStyle();
|
||||
} showFrom:self];
|
||||
}
|
||||
|
||||
#pragma mark - Helpers
|
||||
|
||||
- (void)executeStatementAndShowResult:(NSString *)statement
|
||||
completion:(void (^_Nullable)(BOOL success))completion {
|
||||
NSParameterAssert(self.databaseManager);
|
||||
|
||||
FLEXSQLResult *result = [self.databaseManager executeStatement:statement];
|
||||
|
||||
[FLEXAlert makeAlert:^(FLEXAlert *make) {
|
||||
if (result.isError) {
|
||||
make.title(@"Error");
|
||||
}
|
||||
|
||||
make.message(result.message ?: @"<no output>");
|
||||
make.button(@"Dismiss").cancelStyle().handler(^(NSArray<NSString *> *_) {
|
||||
if (completion) {
|
||||
completion(!result.isError);
|
||||
}
|
||||
});
|
||||
} showFrom:self];
|
||||
}
|
||||
|
||||
- (void)reloadTableDataFromDB {
|
||||
if (!self.canRefresh) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSArray<NSArray *> *rows = [self.databaseManager queryAllDataInTable:self.tableName];
|
||||
NSArray<NSString *> *rowIDs = nil;
|
||||
if ([self.databaseManager respondsToSelector:@selector(queryRowIDsInTable:)]) {
|
||||
rowIDs = [self.databaseManager queryRowIDsInTable:self.tableName];
|
||||
}
|
||||
|
||||
self.rows = rows.mutableCopy;
|
||||
self.rowIDs = rowIDs.mutableCopy;
|
||||
[self.multiColumnView reloadData];
|
||||
}
|
||||
|
||||
@end
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user