added files via upload

This commit is contained in:
Balackburn
2023-06-27 09:54:41 +02:00
commit 2ff6aac218
1420 changed files with 88898 additions and 0 deletions

View File

@@ -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

View 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

View 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

View 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

View 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

View 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