TBKeyPathSearchController.m 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. //
  2. // TBKeyPathSearchController.m
  3. // TBTweakViewController
  4. //
  5. // Created by Tanner on 3/23/17.
  6. // Copyright © 2017 Tanner Bennett. All rights reserved.
  7. //
  8. #import "TBKeyPathSearchController.h"
  9. #import "TBKeyPathTokenizer.h"
  10. #import "TBRuntimeController.h"
  11. //#import "TBCodeFontCell.h"
  12. #import "NSString+KeyPaths.h"
  13. #import "Categories.h"
  14. #import "TBConfigureHookViewController.h"
  15. #import "TBTweakManager.h"
  16. #import "TBMethodHook.h"
  17. @interface TBKeyPathSearchController ()
  18. @property (nonatomic, readonly, weak) id<TBKeyPathSearchControllerDelegate> delegate;
  19. @property (nonatomic, readonly) NSTimer *timer;
  20. @property (nonatomic) NSArray<NSString*> *bundlesOrClasses;
  21. @property (nonatomic) TBKeyPath *keyPath;
  22. // We use this when the target class is not absolute
  23. @property (nonatomic) NSArray<FLEXMethod*> *methods;
  24. // We use these when the target class is absolute and has superclasses.
  25. // Contrary to the name, superclasses contains the origin class name as well.
  26. @property (nonatomic) NSArray<NSString*> *superclasses;
  27. @property (nonatomic) NSDictionary<NSString*, NSArray*> *classesToMethods;
  28. @end
  29. #warning TODO there's no code to handle refreshing the table after manually appending ".bar" to "Bundle"
  30. @implementation TBKeyPathSearchController
  31. + (instancetype)delegate:(id<TBKeyPathSearchControllerDelegate>)delegate {
  32. TBKeyPathSearchController *controller = [self new];
  33. controller->_bundlesOrClasses = [TBRuntimeController allBundleNames];
  34. controller->_delegate = delegate;
  35. NSParameterAssert(delegate.tableView);
  36. NSParameterAssert(delegate.searchBar);
  37. delegate.tableView.delegate = controller;
  38. delegate.tableView.dataSource = controller;
  39. delegate.searchBar.delegate = controller;
  40. return controller;
  41. }
  42. - (void)scrollViewDidScroll:(UIScrollView *)scrollView {
  43. if (scrollView.isTracking || scrollView.isDragging || scrollView.isDecelerating) {
  44. [self.delegate.searchBar resignFirstResponder];
  45. }
  46. }
  47. #pragma mark Long press on class cell
  48. - (void)longPressedRect:(CGRect)rect at:(NSIndexPath *)indexPath {
  49. UIMenuController *menuController = [UIMenuController sharedMenuController];
  50. menuController.menuItems = [self menuItemsForRow:indexPath.row];
  51. if (menuController.menuItems) {
  52. [self.delegate.searchBar resignFirstResponder];
  53. [menuController setTargetRect:rect inView:self.delegate.tableView];
  54. [menuController setMenuVisible:YES animated:YES];
  55. }
  56. }
  57. - (NSArray *)menuItemsForRow:(NSUInteger)row {
  58. if (!self.keyPath.methodKey && self.keyPath.classKey) {
  59. NSArray<NSString*> *superclasses = [self superclassesOf:self.bundlesOrClasses[row]];
  60. // Map to UIMenuItems, will delegate call into didSelectKeyPathOption:
  61. return [superclasses flex_mapped:^id(NSString *cls, NSUInteger idx) {
  62. NSString *sel = [self.delegate.longPressItemSELPrefix stringByAppendingString:cls];
  63. return [[UIMenuItem alloc] initWithTitle:cls action:NSSelectorFromString(sel)];
  64. }];
  65. }
  66. return nil;
  67. }
  68. - (NSArray<NSString*> *)superclassesOf:(NSString *)className {
  69. Class baseClass = NSClassFromString(className);
  70. // Find superclasses
  71. NSMutableArray<NSString*> *superclasses = [NSMutableArray array];
  72. while ([baseClass superclass]) {
  73. [superclasses addObject:NSStringFromClass([baseClass superclass])];
  74. baseClass = [baseClass superclass];
  75. }
  76. return superclasses;
  77. }
  78. #pragma mark Key path stuff
  79. - (void)didSelectSuperclass:(NSString *)name {
  80. NSString *bundle = [TBRuntimeController shortBundleNameForClass:name];
  81. bundle = [bundle stringByReplacingOccurrencesOfString:@"." withString:@"\\."];
  82. NSString *newText = [NSString stringWithFormat:@"%@.%@.", bundle, name];
  83. self.delegate.searchBar.text = newText;
  84. // Update list
  85. self.keyPath = [TBKeyPathTokenizer tokenizeString:newText];
  86. [self didSelectAbsoluteClass:name];
  87. [self updateTable];
  88. }
  89. - (void)didSelectKeyPathOption:(NSString *)text {
  90. [_timer invalidate]; // Still might be waiting to refresh when method is selected
  91. // Change "Bundle.fooba" to "Bundle.foobar."
  92. NSString *orig = self.delegate.searchBar.text;
  93. NSString *keyPath = [orig stringByReplacingLastKeyPathComponent:text];
  94. self.delegate.searchBar.text = keyPath;
  95. self.keyPath = [TBKeyPathTokenizer tokenizeString:keyPath];
  96. // Get superclasses if class was selected
  97. if (self.keyPath.classKey.isAbsolute && self.keyPath.methodKey.isAny) {
  98. [self didSelectAbsoluteClass:text];
  99. } else {
  100. self.superclasses = nil;
  101. }
  102. [self updateTable];
  103. }
  104. - (void)didSelectMethod:(FLEXMethod *)method {
  105. // If the user selects a method implemented only by a superclass,
  106. // we're going to be adding a method. We need to take the given
  107. // method and change it's target class to the base class.
  108. Class target = NSClassFromString(self.keyPath.classKey.string);
  109. if (self.keyPath.classKey.isAbsolute && method.targetClass != target) {
  110. #warning TODO clean this up
  111. method = [FLEXMethod method:method.objc_method class:target isInstanceMethod:method.isInstanceMethod];
  112. }
  113. [self.delegate didSelectMethod:method];
  114. }
  115. - (void)didSelectAbsoluteClass:(NSString *)name {
  116. NSMutableArray *superclasses = [NSMutableArray array];
  117. [superclasses addObject:name];
  118. [superclasses addObjectsFromArray:[self superclassesOf:name]];
  119. self.superclasses = superclasses;
  120. self.bundlesOrClasses = nil;
  121. self.methods = nil;
  122. }
  123. - (void)didPressButton:(NSString *)text insertInto:(UISearchBar *)searchBar {
  124. UITextField *field = [searchBar valueForKey:@"_searchField"];
  125. if ([self searchBar:searchBar shouldChangeTextInRange:field.selectedRange replacementText:text]) {
  126. [field replaceRange:field.selectedTextRange withText:text];
  127. }
  128. }
  129. #pragma mark - Filtering + UISearchBarDelegate
  130. - (void)updateTable {
  131. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
  132. if (self.superclasses) {
  133. // Compute methods list, reload table
  134. self.classesToMethods = [TBRuntimeController methodsForToken:_keyPath.methodKey
  135. instance:_keyPath.instanceMethods
  136. inClasses:_superclasses];
  137. dispatch_async(dispatch_get_main_queue(), ^{
  138. [self.delegate.tableView reloadData];
  139. });
  140. }
  141. else {
  142. NSArray *models = [TBRuntimeController dataForKeyPath:_keyPath];
  143. dispatch_async(dispatch_get_main_queue(), ^{
  144. if (_keyPath.methodKey) {
  145. _bundlesOrClasses = nil;
  146. _methods = models;
  147. } else {
  148. _bundlesOrClasses = models;
  149. _methods = nil;
  150. }
  151. [self.delegate.tableView reloadData];
  152. });
  153. }
  154. });
  155. }
  156. - (BOOL)searchBar:(UISearchBar *)searchBar shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
  157. // Check if character is even legal
  158. if (![TBKeyPathTokenizer allowedInKeyPath:text]) {
  159. return NO;
  160. }
  161. // Actually parse input
  162. @try {
  163. text = [searchBar.text stringByReplacingCharactersInRange:range withString:text] ?: text;
  164. self.keyPath = [TBKeyPathTokenizer tokenizeString:text];
  165. } @catch (id e) {
  166. return NO;
  167. }
  168. return YES;
  169. }
  170. - (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText {
  171. [_timer invalidate];
  172. // Update toolbar buttons
  173. [self.toolbar setKeyPath:self.keyPath animated:YES];
  174. // Schedule update timer
  175. if (searchText.length) {
  176. if (!self.keyPath.methodKey) {
  177. self.superclasses = nil;
  178. }
  179. _timer = [NSTimer fireSecondsFromNow:0.15 block:^{
  180. [self updateTable];
  181. }];
  182. }
  183. // ... or remove all rows
  184. else {
  185. _bundlesOrClasses = [TBRuntimeController allBundleNames];
  186. _methods = nil;
  187. [self.delegate.tableView reloadData];
  188. }
  189. }
  190. - (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar {
  191. [_timer invalidate];
  192. [searchBar resignFirstResponder];
  193. [self updateTable];
  194. }
  195. #pragma mark UITableViewDataSource
  196. - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
  197. return _superclasses.count ?: 1;
  198. }
  199. - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
  200. if (_superclasses) {
  201. return _classesToMethods[_superclasses[section]].count;
  202. }
  203. NSArray *models = (id)_bundlesOrClasses ?: (id)_methods;
  204. return models.count;
  205. }
  206. - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
  207. UITableViewCell *cell = [TBCodeFontCell dequeue:tableView indexPath:indexPath];
  208. if (self.bundlesOrClasses) {
  209. cell.accessoryType = UITableViewCellAccessoryNone;
  210. cell.textLabel.text = self.bundlesOrClasses[indexPath.row];
  211. cell.detailTextLabel.text = nil;
  212. }
  213. else if (self.superclasses) {
  214. NSString *className = self.superclasses[indexPath.section];
  215. FLEXMethod *method = self.classesToMethods[className][indexPath.row];
  216. cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
  217. cell.textLabel.text = method.fullName;
  218. cell.detailTextLabel.text = method.selectorString;
  219. }
  220. else {
  221. cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
  222. cell.textLabel.text = self.methods[indexPath.row].fullName;
  223. cell.detailTextLabel.text = self.methods[indexPath.row].selectorString;
  224. }
  225. return cell;
  226. }
  227. - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
  228. if (self.superclasses) {
  229. return [self.superclasses[section] stringByAppendingString:@" methods"];
  230. }
  231. return nil;
  232. }
  233. #pragma mark UITableViewDelegate
  234. - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
  235. if (self.bundlesOrClasses) {
  236. tableView.contentOffset = CGPointMake(0, - self.delegate.searchBar.frame.size.height - 20);
  237. [self didSelectKeyPathOption:self.bundlesOrClasses[indexPath.row]];
  238. } else {
  239. if (self.superclasses) {
  240. NSString *superclass = self.superclasses[indexPath.section];
  241. [self didSelectMethod:self.classesToMethods[superclass][indexPath.row]];
  242. } else {
  243. assert(indexPath.section == 0);
  244. [self didSelectMethod:self.methods[indexPath.row]];
  245. }
  246. }
  247. }
  248. @end