| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300 |
- //
- // FLEXTableViewController.m
- // FLEX
- //
- // Created by Tanner on 7/5/19.
- // Copyright © 2019 Flipboard. All rights reserved.
- //
- #import "FLEXTableViewController.h"
- #import "FLEXScopeCarousel.h"
- #import "FLEXTableView.h"
- #import "FLEXUtility.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;
- @end
- @implementation FLEXTableViewController
- @synthesize automaticallyShowsSearchBarCancelButton = _automaticallyShowsSearchBarCancelButton;
- #pragma mark - Public
- - (id)init {
- #if FLEX_AT_LEAST_IOS13_SDK
- if (@available(iOS 13.0, *)) {
- self = [self initWithStyle:UITableViewStyleInsetGrouped];
- } 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;
- }
-
- return self;
- }
- - (void)setShowsSearchBar:(BOOL)showsSearchBar {
- if (_showsSearchBar == showsSearchBar) return;
- _showsSearchBar = 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 FLEX_AT_LEAST_IOS13_SDK
- if (@available(iOS 13, *)) {
- self.searchController.automaticallyShowsScopeBar = NO;
- }
- #endif
-
- if (@available(iOS 11.0, *)) {
- self.navigationItem.searchController = self.searchController;
- } else {
- self.tableView.tableHeaderView = self.searchController.searchBar;
- }
- }
- - (void)setShowsCarousel:(BOOL)showsCarousel {
- if (_showsCarousel == showsCarousel) return;
- _showsCarousel = showsCarousel;
- _carousel = ({
- __weak __typeof(self) weakSelf = self;
- FLEXScopeCarousel *carousel = [FLEXScopeCarousel new];
- carousel.selectedIndexChangedAction = ^(NSInteger idx) {
- __typeof(self) self = weakSelf;
- [self updateSearchResults:self.searchText];
- };
- self.tableView.tableHeaderView = carousel;
- [self.tableView layoutIfNeeded];
- // UITableView won't update the header size unless you reset the header view
- [carousel registerBlockForDynamicTypeChanges:^(FLEXScopeCarousel *carousel) {
- __typeof(self) self = weakSelf;
- self.tableView.tableHeaderView = carousel;
- [self.tableView layoutIfNeeded];
- }];
- carousel;
- });
- }
- - (NSInteger)selectedScope {
- if (self.searchController.searchBar.showsScopeBar) {
- return self.searchController.searchBar.selectedScopeButtonIndex;
- } else if (self.showsCarousel) {
- return self.carousel.selectedIndex;
- } else {
- return NSNotFound;
- }
- }
- - (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)updateSearchResults:(NSString *)newText { }
- - (void)onBackgroundQueue:(NSArray *(^)(void))backgroundBlock thenOnMainQueue:(void(^)(NSArray *))mainBlock {
- dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
- NSArray *items = backgroundBlock();
- dispatch_async(dispatch_get_main_queue(), ^{
- mainBlock(items);
- });
- });
- }
- #pragma mark - View Controller Lifecycle
- - (void)loadView {
- self.view = [FLEXTableView style:self.style];
- self.tableView.dataSource = self;
- self.tableView.delegate = self;
- }
- - (void)viewDidLoad {
- [super viewDidLoad];
-
- self.tableView.keyboardDismissMode = UIScrollViewKeyboardDismissModeOnDrag;
- // 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) {
- self.navigationItem.hidesSearchBarWhenScrolling = NO;
- }
- }
- }
- - (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];
- }];
- }
- }
- // We only want to reveal the search bar when the view controller first appears.
- self.didInitiallyRevealSearchBar = YES;
- }
- - (void)willMoveToParentViewController:(UIViewController *)parent {
- [super willMoveToParentViewController: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 - Private
- - (void)debounce:(void(^)(void))block {
- [self.debounceTimer invalidate];
-
- self.debounceTimer = [NSTimer
- scheduledTimerWithTimeInterval:self.searchBarDebounceInterval
- target:block
- selector:@selector(invoke)
- userInfo:nil
- repeats:NO
- ];
- }
- #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 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, *)) {
- return @" "; // For inset grouped style
- }
- return nil; // For plain/gropued style
- }
- @end
|