123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421 |
- //
- // FLEXKeyPathSearchController.m
- // FLEX
- //
- // Created by Tanner on 3/23/17.
- // Copyright © 2017 Tanner Bennett. All rights reserved.
- //
- #import "FLEXKeyPathSearchController.h"
- #import "FLEXRuntimeKeyPathTokenizer.h"
- #import "FLEXRuntimeController.h"
- #import "NSString+FLEX.h"
- #import "NSArray+FLEX.h"
- #import "UITextField+Range.h"
- #import "NSTimer+FLEX.h"
- #import "FLEXTableView.h"
- #import "FLEXUtility.h"
- #import "FLEXObjectExplorerFactory.h"
- @interface FLEXKeyPathSearchController ()
- @property (nonatomic, readonly, weak) id<FLEXKeyPathSearchControllerDelegate> delegate;
- @property (nonatomic) NSTimer *timer;
- /// If \c keyPath is \c nil or if it only has a \c bundleKey, this is
- /// a list of bundle key path components like \c UICatalog or \c UIKit\.framework
- /// If \c keyPath has more than a \c bundleKey then it is a list of class names.
- @property (nonatomic) NSArray<NSString *> *bundlesOrClasses;
- /// nil when search bar is empty
- @property (nonatomic) FLEXRuntimeKeyPath *keyPath;
- @property (nonatomic, readonly) NSString *emptySuggestion;
- /// Used to track which methods go with which classes. This is used in
- /// two scenarios: (1) when the target class is absolute and has classes,
- /// (this list will include the "leaf" class as well as parent classes in this case)
- /// or (2) when the class key is a wildcard and we're searching methods in many
- /// classes at once. Each list in \c classesToMethods correspnds to a class here.
- @property (nonatomic) NSArray<NSString *> *classes;
- /// A filtered version of \c classes used when searching for a specific attribute.
- /// Classes with no matching ivars/properties/methods are not shown.
- @property (nonatomic) NSArray<NSString *> *filteredClasses;
- // We use this regardless of whether the target class is absolute, just as above
- @property (nonatomic) NSArray<NSArray<FLEXMethod *> *> *classesToMethods;
- @end
- @implementation FLEXKeyPathSearchController
- + (instancetype)delegate:(id<FLEXKeyPathSearchControllerDelegate>)delegate {
- FLEXKeyPathSearchController *controller = [self new];
- controller->_bundlesOrClasses = [FLEXRuntimeController allBundleNames];
- controller->_delegate = delegate;
- controller->_emptySuggestion = NSBundle.mainBundle.executablePath.lastPathComponent;
- NSParameterAssert(delegate.tableView);
- NSParameterAssert(delegate.searchController);
- delegate.tableView.delegate = controller;
- delegate.tableView.dataSource = controller;
-
- UISearchBar *searchBar = delegate.searchController.searchBar;
- searchBar.delegate = controller;
- searchBar.keyboardType = UIKeyboardTypeWebSearch;
- searchBar.autocorrectionType = UITextAutocorrectionTypeNo;
- if (@available(iOS 11, *)) {
- searchBar.smartInsertDeleteType = UITextSmartInsertDeleteTypeNo;
- }
- return controller;
- }
- - (void)scrollViewDidScroll:(UIScrollView *)scrollView {
- if (scrollView.isTracking || scrollView.isDragging || scrollView.isDecelerating) {
- [self.delegate.searchController.searchBar resignFirstResponder];
- }
- }
- - (void)setToolbar:(FLEXRuntimeBrowserToolbar *)toolbar {
- _toolbar = toolbar;
- self.delegate.searchController.searchBar.inputAccessoryView = toolbar;
- }
- - (NSArray<NSString *> *)classesOf:(NSString *)className {
- Class baseClass = NSClassFromString(className);
- if (!baseClass) {
- return @[];
- }
- // Find classes
- NSMutableArray<NSString*> *classes = [NSMutableArray arrayWithObject:className];
- while ([baseClass superclass]) {
- [classes addObject:NSStringFromClass([baseClass superclass])];
- baseClass = [baseClass superclass];
- }
- return classes;
- }
- #pragma mark Key path stuff
- - (void)didSelectKeyPathOption:(NSString *)text {
- [_timer invalidate]; // Still might be waiting to refresh when method is selected
- // Change "Bundle.fooba" to "Bundle.foobar."
- NSString *orig = self.delegate.searchController.searchBar.text;
- NSString *keyPath = [orig flex_stringByReplacingLastKeyPathComponent:text];
- self.delegate.searchController.searchBar.text = keyPath;
- self.keyPath = [FLEXRuntimeKeyPathTokenizer tokenizeString:keyPath];
- // Get classes if class was selected
- if (self.keyPath.classKey.isAbsolute && self.keyPath.methodKey.isAny) {
- [self didSelectAbsoluteClass:text];
- } else {
- self.classes = nil;
- self.filteredClasses = nil;
- }
- [self updateTable];
- }
- - (void)didSelectAbsoluteClass:(NSString *)name {
- self.classes = [self classesOf:name];
- self.filteredClasses = self.classes;
- self.bundlesOrClasses = nil;
- self.classesToMethods = nil;
- }
- - (void)didPressButton:(NSString *)text insertInto:(UISearchBar *)searchBar {
- [self.toolbar setKeyPath:self.keyPath suggestions:nil];
-
- // Available since at least iOS 9, still present in iOS 13
- UITextField *field = [searchBar valueForKey:@"_searchBarTextField"];
- if ([self searchBar:searchBar shouldChangeTextInRange:field.flex_selectedRange replacementText:text]) {
- [field replaceRange:field.selectedTextRange withText:text];
- }
- }
- - (NSArray<NSString *> *)suggestions {
- if (self.bundlesOrClasses) {
- if (self.classes) {
- if (self.classesToMethods) {
- // We have selected a class and are searching metadata
- return nil;
- }
-
- // We are currently searching classes
- return [self.filteredClasses flex_subArrayUpto:10];
- }
-
- if (!self.keyPath) {
- // Search bar is empty
- return @[self.emptySuggestion];
- }
-
- // We are currently searching bundles
- return [self.bundlesOrClasses flex_subArrayUpto:10];
- }
-
- // We have nothing at all to even search
- return nil;
- }
- #pragma mark - Filtering + UISearchBarDelegate
- - (void)updateTable {
- // Compute the method, class, or bundle lists on a background thread
- dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
- if (self.classes) {
- // Here, our class key is 'absolute'; .classes is a list of superclasses
- // and we want to show the methods for those classes specifically
- // TODO: add caching to this somehow
- NSMutableArray *methods = [FLEXRuntimeController
- methodsForToken:self.keyPath.methodKey
- instance:self.keyPath.instanceMethods
- inClasses:self.classes
- ].mutableCopy;
-
- // Remove classes without results if we're searching for a method
- //
- // Note: this will remove classes without any methods or overrides
- // even if the query doesn't specify a method, like `*.*.`
- if (self.keyPath.methodKey) {
- [self setNonEmptyMethodLists:methods withClasses:self.classes.mutableCopy];
- } else {
- self.filteredClasses = self.classes;
- }
- }
- else {
- FLEXRuntimeKeyPath *keyPath = self.keyPath;
- NSArray *models = [FLEXRuntimeController dataForKeyPath:keyPath];
- if (keyPath.methodKey) { // We're looking at methods
- self.bundlesOrClasses = nil;
-
- NSMutableArray *methods = models.mutableCopy;
- NSMutableArray<NSString *> *classes = [
- FLEXRuntimeController classesForKeyPath:keyPath
- ];
- self.classes = classes;
- [self setNonEmptyMethodLists:methods withClasses:classes];
- } else { // We're looking at bundles or classes
- self.bundlesOrClasses = models;
- self.classesToMethods = nil;
- }
- }
-
- // Finally, reload the table on the main thread
- dispatch_async(dispatch_get_main_queue(), ^{
- [self updateToolbarButtons];
- [self.delegate.tableView reloadData];
- });
- });
- }
- - (void)updateToolbarButtons {
- // Update toolbar buttons
- [self.toolbar setKeyPath:self.keyPath suggestions:self.suggestions];
- }
- /// Assign assign .filteredClasses and .classesToMethods after removing empty sections
- - (void)setNonEmptyMethodLists:(NSMutableArray<NSArray<FLEXMethod *> *> *)methods
- withClasses:(NSMutableArray<NSString *> *)classes {
- // Remove sections with no methods
- NSIndexSet *allEmpty = [methods indexesOfObjectsPassingTest:^BOOL(NSArray *list, NSUInteger idx, BOOL *stop) {
- return list.count == 0;
- }];
- [methods removeObjectsAtIndexes:allEmpty];
- [classes removeObjectsAtIndexes:allEmpty];
-
- self.filteredClasses = classes;
- self.classesToMethods = methods;
- }
- - (BOOL)searchBar:(UISearchBar *)searchBar shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text {
- // Check if character is even legal
- if (![FLEXRuntimeKeyPathTokenizer allowedInKeyPath:text]) {
- return NO;
- }
-
- BOOL terminatedToken = NO;
- BOOL isAppending = range.length == 0 && range.location == searchBar.text.length;
- if (isAppending && [text isEqualToString:@"."]) {
- terminatedToken = YES;
- }
- // Actually parse input
- @try {
- text = [searchBar.text stringByReplacingCharactersInRange:range withString:text] ?: text;
- self.keyPath = [FLEXRuntimeKeyPathTokenizer tokenizeString:text];
- if (self.keyPath.classKey.isAbsolute && terminatedToken) {
- [self didSelectAbsoluteClass:self.keyPath.classKey.string];
- }
- } @catch (id e) {
- return NO;
- }
- return YES;
- }
- - (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText {
- [_timer invalidate];
- // Schedule update timer
- if (searchText.length) {
- if (!self.keyPath.methodKey) {
- self.classes = nil;
- self.filteredClasses = nil;
- }
- self.timer = [NSTimer flex_fireSecondsFromNow:0.15 block:^{
- [self updateTable];
- }];
- }
- // ... or remove all rows
- else {
- _bundlesOrClasses = [FLEXRuntimeController allBundleNames];
- _classesToMethods = nil;
- _classes = nil;
- _keyPath = nil;
- [self updateToolbarButtons];
- [self.delegate.tableView reloadData];
- }
- }
- - (void)searchBarCancelButtonClicked:(UISearchBar *)searchBar {
- self.keyPath = FLEXRuntimeKeyPath.empty;
- [self updateTable];
- }
- /// Restore key path when going "back" and activating search bar again
- - (void)searchBarTextDidBeginEditing:(UISearchBar *)searchBar {
- searchBar.text = self.keyPath.description;
- }
- - (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar {
- [_timer invalidate];
- [searchBar resignFirstResponder];
- [self updateTable];
- }
- #pragma mark UITableViewDataSource
- - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
- return self.filteredClasses.count ?: self.bundlesOrClasses.count;
- }
- - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
- UITableViewCell *cell = [tableView
- dequeueReusableCellWithIdentifier:kFLEXMultilineDetailCell
- forIndexPath:indexPath
- ];
-
- if (self.bundlesOrClasses.count) {
- #if !TARGET_OS_TV
- cell.accessoryType = UITableViewCellAccessoryDetailButton;
- #else
- cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
- #endif
- cell.textLabel.text = self.bundlesOrClasses[indexPath.row];
- cell.detailTextLabel.text = nil;
- if (self.keyPath.classKey) {
- cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
- }
- }
- // One row per section
- else if (self.filteredClasses.count) {
- NSArray<FLEXMethod *> *methods = self.classesToMethods[indexPath.row];
- NSMutableString *summary = [NSMutableString new];
- [methods enumerateObjectsUsingBlock:^(FLEXMethod *method, NSUInteger idx, BOOL *stop) {
- NSString *format = nil;
- if (idx == methods.count-1) {
- format = @"%@%@";
- *stop = YES;
- } else if (idx < 3) {
- format = @"%@%@\n";
- } else {
- format = @"%@%@\n…";
- *stop = YES;
- }
- [summary appendFormat:format, method.isInstanceMethod ? @"-" : @"+", method.selectorString];
- }];
- cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
- cell.textLabel.text = self.filteredClasses[indexPath.row];
- if (@available(iOS 10, *)) {
- cell.detailTextLabel.text = summary.length ? summary : nil;
- }
- }
- else {
- @throw NSInternalInconsistencyException;
- }
- return cell;
- }
- - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
- if (self.filteredClasses || self.keyPath.methodKey) {
- return @" ";
- } else if (self.bundlesOrClasses) {
- NSInteger count = self.bundlesOrClasses.count;
- if (self.keyPath.classKey) {
- return FLEXPluralString(count, @"classes", @"class");
- } else {
- return FLEXPluralString(count, @"bundles", @"bundle");
- }
- }
- return [self.delegate tableView:tableView titleForHeaderInSection:section];
- }
- - (CGFloat)tableView:(UITableView *)tableView heightForHeaderInSection:(NSInteger)section {
- if (self.filteredClasses || self.keyPath.methodKey) {
- if (section == 0) {
- return 55;
- }
- return 0;
- }
- return 55;
- }
- #pragma mark UITableViewDelegate
- - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
- if (self.bundlesOrClasses) {
- NSString *bundleSuffixOrClass = self.bundlesOrClasses[indexPath.row];
- if (self.keyPath.classKey) {
- NSParameterAssert(NSClassFromString(bundleSuffixOrClass));
- [self.delegate didSelectClass:NSClassFromString(bundleSuffixOrClass)];
- } else {
- // Selected a bundle
- [self didSelectKeyPathOption:bundleSuffixOrClass];
- }
- } else {
- if (self.filteredClasses.count) {
- Class cls = NSClassFromString(self.filteredClasses[indexPath.row]);
- NSParameterAssert(cls);
- [self.delegate didSelectClass:cls];
- } else {
- @throw NSInternalInconsistencyException;
- }
- }
- }
- - (void)tableView:(UITableView *)tableView accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)indexPath {
- NSString *bundleSuffixOrClass = self.bundlesOrClasses[indexPath.row];
- NSString *imagePath = [FLEXRuntimeController imagePathWithShortName:bundleSuffixOrClass];
- NSBundle *bundle = [NSBundle bundleWithPath:imagePath.stringByDeletingLastPathComponent];
- if (bundle) {
- [self.delegate didSelectBundle:bundle];
- } else {
- [self.delegate didSelectImagePath:imagePath shortName:bundleSuffixOrClass];
- }
- }
- @end
|