123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631 |
- //
- // 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>
- #import "fakes.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 FLEX_AT_LEAST_IOS13_SDK
- if (@available(iOS 13.0, *)) {
- #if !TARGET_OS_TV
- self = [self initWithStyle:UITableViewStyleInsetGrouped];
- #else
- self = [self initWithStyle:UITableViewStyleGrouped];
- #endif
- } else {
- self = [self initWithStyle:UITableViewStyleGrouped];
- }
- #else
- self = [self initWithStyle:UITableViewStyleGrouped];
- #endif
- return self;
- }
- - (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;
- }
- /**
-
- search is handled a bit differently on tvOS and i couldnt get its pardigm to cooperate, thankfully the UISearchController never needs to be visible to actually work its magic.
- since 3D snapshot viewing doesn't exist on tvOS the leftBarButtonItem is the perfect spot to add a 'search' button. this search button will present a new text entry controller
- the alpha on this viewController is decreased to 0.6 to make it possible to view the filtering changes underneath in realtime. The zero rect textfield associated with
- the search button acts as a proxy to transfer the text to our search bar as necessary
-
- */
- - (void)setShowsSearchBar:(BOOL)showsSearchBar {
- if (_showsSearchBar == showsSearchBar) return;
- _showsSearchBar = showsSearchBar;
-
- 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;
- #if !TARGET_OS_TV
- self.searchController.searchBar.delegate = self;
- self.searchController.dimsBackgroundDuringPresentation = NO;
- #endif
- self.searchController.hidesNavigationBarDuringPresentation = NO;
- /// Not necessary in iOS 13; remove this when iOS 13 is the minimum deployment target
-
- self.automaticallyShowsSearchBarCancelButton = YES;
- #if FLEX_AT_LEAST_IOS13_SDK
- if (@available(iOS 13, *)) {
- self.searchController.automaticallyShowsScopeBar = NO;
- }
- #endif
- [self addSearchController:self.searchController];
- } else {
- // Search already shown and just set to NO, so remove it
- [self removeSearchController:self.searchController];
- }
- }
- - (void)setShowsCarousel:(BOOL)showsCarousel {
- if (_showsCarousel == showsCarousel) return;
- _showsCarousel = showsCarousel;
-
- if (showsCarousel) {
- _carousel = ({
- __weak __typeof(self) weakSelf = self;
- FLEXScopeCarousel *carousel = [FLEXScopeCarousel new];
- carousel.selectedIndexChangedAction = ^(NSInteger idx) {
- __typeof(self) self = weakSelf;
- [self.searchDelegate updateSearchResults:self.searchText];
- };
- // UITableView won't update the header size unless you reset the header view
- [carousel registerBlockForDynamicTypeChanges:^(FLEXScopeCarousel *carousel) {
- __typeof(self) self = weakSelf;
- [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 FLEX_AT_LEAST_IOS13_SDK
- if (@available(iOS 13, *)) {
- return self.searchController.automaticallyShowsCancelButton;
- }
- #endif
- return _automaticallyShowsSearchBarCancelButton;
- }
- - (void)setAutomaticallyShowsSearchBarCancelButton:(BOOL)value {
- #if FLEX_AT_LEAST_IOS13_SDK
- if (@available(iOS 13, *)) {
- self.searchController.automaticallyShowsCancelButton = value;
- }
- #endif
- _automaticallyShowsSearchBarCancelButton = value;
- }
- - (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 {
- #if !TARGET_OS_TV
- self.navigationController.toolbarHidden = YES;
- self.navigationController.hidesBarsOnSwipe = NO;
- self.toolbarItems = nil;
- #endif
- }
- #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
- #if !TARGET_OS_TV
- self.navigationController.toolbarHidden = NO;
- self.navigationController.hidesBarsOnSwipe = YES;
- #endif
- // On iOS 13, the root view controller shows it's search bar no matter what.
- // Turning this off avoids some weird flash the navigation bar does when we
- // toggle navigationItem.hidesSearchBarWhenScrolling on and off. The flash
- // 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];
-
- // When going back, make the search bar reappear instead of hiding
- if (@available(iOS 11.0, *)) {
- if ((self.pinSearchBar || self.showSearchBarInitially) && !self.didInitiallyRevealSearchBar) {
- #if !TARGET_OS_TV
- self.navigationItem.hidesSearchBarWhenScrolling = NO;
- #endif
- }
- }
- #if TARGET_OS_TV
- UIEdgeInsets insets = self.tableView.contentInset;
- insets.top+=20;
- self.tableView.contentInset = insets;
- #endif
- [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:^{
- #if !TARGET_OS_TV
- self.navigationItem.hidesSearchBarWhenScrolling = YES;
- #endif
- [self.navigationController.view setNeedsLayout];
- [self.navigationController.view layoutIfNeeded];
- }];
- }
- }
- // 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;
- }
- #if !TARGET_OS_TV
- 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;
- }
- #endif
- // 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 TARGET_OS_TV
- KBSearchButton *sb = [KBSearchButton buttonWithType:UIButtonTypeSystem];
- sb.searchBar = self.searchController.searchBar;
- UIBarButtonItem *searchButton = [[UIBarButtonItem alloc] initWithCustomView:sb];
- self.navigationItem.leftBarButtonItem = searchButton;
- #else
- if (@available(iOS 11.0, *)) {
- self.navigationItem.searchController = controller;
- } else {
- 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];
- }
- #endif
- }
- - (void)removeSearchController:(UISearchController *)controller {
- #if TARGET_OS_TV
- self.navigationItem.leftBarButtonItem = nil;
- #else
- 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;
- }
- }
- #endif
- }
- - (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 UISearchResultsUpdating
- - (void)updateSearchResultsForSearchController:(UISearchController *)searchController {
- [self.debounceTimer invalidate];
- NSString *text = searchController.searchBar.text;
-
- void (^updateSearchResults)() = ^{
- 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();
- }
- }
- #if !TARGET_OS_TV
- #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];
- }
- #endif
- #pragma mark Table View
- /// Not having a title in the first section looks weird with a rounded-corner table view style
- - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
- if (@available(iOS 13, *)) {
- #if !TARGET_OS_TV
- if (self.style == UITableViewStyleInsetGrouped) {
- return @" ";
- }
- #endif
- }
- return nil; // For plain/gropued style
- }
- @end
|