mirror of
https://github.com/SoPat712/YTLitePlus.git
synced 2025-10-30 04:20:37 -04:00
added files via upload
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user