FLEXTableViewController.m 6.3 KB

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