FLEXTableViewController.m 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. //
  2. // FLEXTableViewController.m
  3. // FLEX
  4. //
  5. // Created by Tanner on 7/5/19.
  6. // Copyright © 2019 Flipboard. All rights reserved.
  7. //
  8. #import "FLEXTableViewController.h"
  9. #import "FLEXScopeCarousel.h"
  10. #import "FLEXTableView.h"
  11. #import <objc/runtime.h>
  12. @interface Block : NSObject
  13. - (void)invoke;
  14. @end
  15. CGFloat const kFLEXDebounceInstant = 0.f;
  16. CGFloat const kFLEXDebounceFast = 0.05;
  17. CGFloat const kFLEXDebounceForAsyncSearch = 0.15;
  18. CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
  19. @interface FLEXTableViewController ()
  20. @property (nonatomic) NSTimer *debounceTimer;
  21. @end
  22. @implementation FLEXTableViewController
  23. #pragma mark - Public
  24. - (id)init {
  25. #if FLEX_AT_LEAST_IOS13_SDK
  26. if (@available(iOS 13.0, *)) {
  27. self = [self initWithStyle:UITableViewStyleInsetGrouped];
  28. } else {
  29. self = [self initWithStyle:UITableViewStyleGrouped];
  30. }
  31. #else
  32. self = [self initWithStyle:UITableViewStyleGrouped];
  33. #endif
  34. return self;
  35. }
  36. - (id)initWithStyle:(UITableViewStyle)style {
  37. self = [super initWithStyle:style];
  38. if (self) {
  39. self.searchBarDebounceInterval = kFLEXDebounceFast;
  40. }
  41. return self;
  42. }
  43. - (void)setShowsSearchBar:(BOOL)showsSearchBar {
  44. if (_showsSearchBar == showsSearchBar) return;
  45. _showsSearchBar = showsSearchBar;
  46. UIViewController *results = self.searchResultsController;
  47. self.searchController = [[UISearchController alloc] initWithSearchResultsController:results];
  48. self.searchController.searchBar.placeholder = @"Filter";
  49. self.searchController.searchResultsUpdater = (id)self;
  50. self.searchController.delegate = (id)self;
  51. self.searchController.dimsBackgroundDuringPresentation = NO;
  52. self.searchController.hidesNavigationBarDuringPresentation = NO;
  53. /// Not necessary in iOS 13; remove this when iOS 13 is the deployment target
  54. self.searchController.searchBar.delegate = self;
  55. if (@available(iOS 11.0, *)) {
  56. self.navigationItem.searchController = self.searchController;
  57. } else {
  58. self.tableView.tableHeaderView = self.searchController.searchBar;
  59. }
  60. }
  61. - (void)setShowsCarousel:(BOOL)showsCarousel {
  62. if (_showsCarousel == showsCarousel) return;
  63. _showsCarousel = showsCarousel;
  64. _carousel = ({
  65. __weak __typeof(self) weakSelf = self;
  66. FLEXScopeCarousel *carousel = [FLEXScopeCarousel new];
  67. carousel.selectedIndexChangedAction = ^(NSInteger idx) {
  68. __typeof(self) self = weakSelf;
  69. [self updateSearchResults:self.searchText];
  70. };
  71. self.tableView.tableHeaderView = carousel;
  72. [self.tableView layoutIfNeeded];
  73. // UITableView won't update the header size unless you reset the header view
  74. [carousel registerBlockForDynamicTypeChanges:^(FLEXScopeCarousel *carousel) {
  75. __typeof(self) self = weakSelf;
  76. self.tableView.tableHeaderView = carousel;
  77. [self.tableView layoutIfNeeded];
  78. }];
  79. carousel;
  80. });
  81. }
  82. - (NSInteger)selectedScope {
  83. if (self.searchController.searchBar.showsScopeBar) {
  84. return self.searchController.searchBar.selectedScopeButtonIndex;
  85. } else if (self.showsCarousel) {
  86. return self.carousel.selectedIndex;
  87. } else {
  88. return NSNotFound;
  89. }
  90. }
  91. - (NSString *)searchText {
  92. return self.searchController.searchBar.text;
  93. }
  94. - (void)setAutomaticallyShowsSearchBarCancelButton:(BOOL)autoShowCancel {
  95. #if FLEX_AT_LEAST_IOS13_SDK
  96. if (@available(iOS 13.0, *)) {
  97. self.searchController.automaticallyShowsCancelButton = autoShowCancel;
  98. } else {
  99. _automaticallyShowsSearchBarCancelButton = autoShowCancel;
  100. }
  101. #else
  102. _automaticallyShowsSearchBarCancelButton = autoShowCancel;
  103. #endif
  104. }
  105. - (void)updateSearchResults:(NSString *)newText { }
  106. - (void)onBackgroundQueue:(NSArray *(^)(void))backgroundBlock thenOnMainQueue:(void(^)(NSArray *))mainBlock {
  107. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
  108. NSArray *items = backgroundBlock();
  109. dispatch_async(dispatch_get_main_queue(), ^{
  110. mainBlock(items);
  111. });
  112. });
  113. }
  114. #pragma mark - View Controller Lifecycle
  115. - (void)viewDidLoad {
  116. [super viewDidLoad];
  117. self.tableView.keyboardDismissMode = UIScrollViewKeyboardDismissModeOnDrag;
  118. }
  119. - (void)viewWillAppear:(BOOL)animated {
  120. [super viewWillAppear:animated];
  121. // Make the search bar re-appear instead of hiding
  122. if (@available(iOS 11.0, *)) if (!self.hideSearchBarInitially) {
  123. self.navigationItem.hidesSearchBarWhenScrolling = NO;
  124. }
  125. }
  126. - (void)viewDidAppear:(BOOL)animated {
  127. [super viewDidAppear:animated];
  128. // Allow scrolling to collapse the search bar,
  129. // only if we don't want it pinned
  130. if (@available(iOS 11.0, *)) if (!self.hideSearchBarInitially) {
  131. self.navigationItem.hidesSearchBarWhenScrolling = !self.pinSearchBar;
  132. }
  133. }
  134. - (void)viewDidDisappear:(BOOL)animated {
  135. [super viewDidDisappear:animated];
  136. if (self.searchController.active) {
  137. self.searchController.active = NO;
  138. }
  139. }
  140. #pragma mark - Private
  141. - (void)debounce:(void(^)(void))block {
  142. [self.debounceTimer invalidate];
  143. self.debounceTimer = [NSTimer
  144. scheduledTimerWithTimeInterval:self.searchBarDebounceInterval
  145. target:block
  146. selector:@selector(invoke)
  147. userInfo:nil
  148. repeats:NO
  149. ];
  150. }
  151. #pragma mark - Search Bar
  152. #pragma mark UISearchResultsUpdating
  153. - (void)updateSearchResultsForSearchController:(UISearchController *)searchController
  154. {
  155. [self.debounceTimer invalidate];
  156. NSString *text = searchController.searchBar.text;
  157. // Only debounce if we want to, and if we have a non-empty string
  158. // Empty string events are sent instantly
  159. if (text.length && self.searchBarDebounceInterval > kFLEXDebounceInstant) {
  160. [self debounce:^{
  161. [self updateSearchResults:text];
  162. }];
  163. } else {
  164. [self updateSearchResults:text];
  165. }
  166. }
  167. #pragma mark UISearchControllerDelegate
  168. - (void)willPresentSearchController:(UISearchController *)searchController {
  169. if (self.automaticallyShowsSearchBarCancelButton) {
  170. [searchController.searchBar setShowsCancelButton:YES animated:YES];
  171. }
  172. }
  173. - (void)willDismissSearchController:(UISearchController *)searchController {
  174. if (self.automaticallyShowsSearchBarCancelButton) {
  175. [searchController.searchBar setShowsCancelButton:NO animated:YES];
  176. }
  177. }
  178. #pragma mark UISearchBarDelegate
  179. /// Not necessary in iOS 13; remove this when iOS 13 is the deployment target
  180. - (void)searchBar:(UISearchBar *)searchBar selectedScopeButtonIndexDidChange:(NSInteger)selectedScope {
  181. [self updateSearchResultsForSearchController:self.searchController];
  182. }
  183. @end