Browse Source

Search upgrades

- Add ability to search object lists (instances, references, subclasses)
- Fix broken search for collection exploreres
Tanner Bennett 4 years ago
parent
commit
89010395de

+ 0 - 16
Classes/GlobalStateExplorers/FLEXInstancesViewController.h

@@ -1,16 +0,0 @@
-//
-//  FLEXInstancesViewController.h
-//  Flipboard
-//
-//  Created by Ryan Olson on 5/28/14.
-//  Copyright (c) 2014 Flipboard. All rights reserved.
-//
-
-#import <UIKit/UIKit.h>
-
-@interface FLEXInstancesViewController : UITableViewController
-
-+ (instancetype)instancesTableViewControllerForClassName:(NSString *)className;
-+ (instancetype)instancesTableViewControllerForInstancesReferencingObject:(id)object;
-
-@end

+ 2 - 2
Classes/GlobalStateExplorers/FLEXLiveObjectsTableViewController.m

@@ -8,7 +8,7 @@
 
 #import "FLEXLiveObjectsTableViewController.h"
 #import "FLEXHeapEnumerator.h"
-#import "FLEXInstancesViewController.h"
+#import "FLEXObjectListViewController.h"
 #import "FLEXUtility.h"
 #import "FLEXScopeCarousel.h"
 #import "FLEXTableView.h"
@@ -225,7 +225,7 @@ static const NSInteger kFLEXLiveObjectsSortBySizeIndex = 2;
 
 - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
     NSString *className = self.filteredClassNames[indexPath.row];
-    FLEXInstancesViewController *instancesViewController = [FLEXInstancesViewController instancesTableViewControllerForClassName:className];
+    FLEXObjectListViewController *instancesViewController = [FLEXObjectListViewController instancesOfClassWithName:className];
     [self.navigationController pushViewController:instancesViewController animated:YES];
 }
 

+ 17 - 0
Classes/GlobalStateExplorers/FLEXObjectListViewController.h

@@ -0,0 +1,17 @@
+//
+//  FLEXObjectListViewController.h
+//  Flipboard
+//
+//  Created by Ryan Olson on 5/28/14.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import "FLEXTableViewController.h"
+
+@interface FLEXObjectListViewController : FLEXTableViewController
+
++ (instancetype)instancesOfClassWithName:(NSString *)className;
++ (instancetype)subclassesOfClassWithName:(NSString *)className;
++ (instancetype)objectsWithReferencesToObject:(id)object;
+
+@end

+ 164 - 104
Classes/GlobalStateExplorers/FLEXInstancesViewController.m

@@ -1,35 +1,96 @@
 //
-//  FLEXInstancesViewController.m
+//  FLEXObjectListViewController.m
 //  Flipboard
 //
 //  Created by Ryan Olson on 5/28/14.
 //  Copyright (c) 2014 Flipboard. All rights reserved.
 //
 
-#import "FLEXInstancesViewController.h"
+#import "FLEXObjectListViewController.h"
 #import "FLEXObjectExplorerFactory.h"
 #import "FLEXObjectExplorerViewController.h"
+#import "FLEXCollectionContentSection.h"
 #import "FLEXRuntimeUtility.h"
 #import "FLEXUtility.h"
 #import "FLEXHeapEnumerator.h"
 #import "FLEXObjectRef.h"
 #import "NSString+FLEX.h"
+#import "NSObject+Reflection.h"
+#import "FLEXTableViewCell.h"
 #import <malloc/malloc.h>
 
 
-@interface FLEXInstancesViewController ()
+@interface FLEXObjectListViewController ()
+
+@property (nonatomic) NSArray<FLEXCollectionContentSection *> *sections;
+@property (nonatomic, readonly) NSArray<FLEXCollectionContentSection *> *allSections;
 
 /// Array of [[section], [section], ...]
 /// where [section] is [["row title", instance], ["row title", instance], ...]
-@property (nonatomic) NSArray<FLEXObjectRef *> *instances;
-@property (nonatomic) NSArray<NSArray<FLEXObjectRef*>*> *sections;
-@property (nonatomic) NSArray<NSString *> *sectionTitles;
-@property (nonatomic) NSArray<NSPredicate *> *predicates;
-@property (nonatomic, readonly) NSInteger maxSections;
+@property (nonatomic) NSArray<FLEXObjectRef *> *references;
 
 @end
 
-@implementation FLEXInstancesViewController
+@implementation FLEXObjectListViewController
+
+#pragma mark - Reference Grouping
+
++ (NSPredicate *)defaultPredicateForSection:(NSInteger)section {
+    // These are the types of references that we typically don't care about.
+    // We want this list of "object-ivar pairs" split into two sections.
+    BOOL(^isObserver)(FLEXObjectRef *, NSDictionary *) = ^BOOL(FLEXObjectRef *ref, NSDictionary *bindings) {
+        NSString *row = ref.reference;
+        return [row isEqualToString:@"__NSObserver object"] ||
+               [row isEqualToString:@"_CFXNotificationObjcObserverRegistration _object"];
+    };
+
+    /// These are common AutoLayout related references we also rarely care about.
+    BOOL(^isConstraintRelated)(FLEXObjectRef *, NSDictionary *) = ^BOOL(FLEXObjectRef *ref, NSDictionary *bindings) {
+        static NSSet *ignored = nil;
+        static dispatch_once_t onceToken;
+        dispatch_once(&onceToken, ^{
+            ignored = [NSSet setWithArray:@[
+                @"NSLayoutConstraint _container",
+                @"NSContentSizeLayoutConstraint _container",
+                @"NSAutoresizingMaskLayoutConstraint _container",
+                @"MASViewConstraint _installedView",
+                @"MASLayoutConstraint _container",
+                @"MASViewAttribute _view"
+            ]];
+        });
+
+        NSString *row = ref.reference;
+        return ([row hasPrefix:@"NSLayout"] && [row hasSuffix:@" _referenceItem"]) ||
+               ([row hasPrefix:@"NSIS"] && [row hasSuffix:@" _delegate"])  ||
+               ([row hasPrefix:@"_NSAutoresizingMask"] && [row hasSuffix:@" _referenceItem"]) ||
+               [ignored containsObject:row];
+    };
+
+    BOOL(^isEssential)(FLEXObjectRef *, NSDictionary *) = ^BOOL(FLEXObjectRef *ref, NSDictionary *bindings) {
+        return !(isObserver(ref, bindings) || isConstraintRelated(ref, bindings));
+    };
+
+    switch (section) {
+        case 0: return [NSPredicate predicateWithBlock:isEssential];
+        case 1: return [NSPredicate predicateWithBlock:isConstraintRelated];
+        case 2: return [NSPredicate predicateWithBlock:isObserver];
+
+        default: return nil;
+    }
+}
+
++ (NSArray<NSPredicate *> *)defaultPredicates {
+    return @[[self defaultPredicateForSection:0],
+             [self defaultPredicateForSection:1],
+             [self defaultPredicateForSection:2]];
+}
+
++ (NSArray<NSString *> *)defaultSectionTitles {
+    return @[@"", @"AutoLayout", @"Trivial"];
+}
+
+
+#pragma mark - Initialization
 
 - (id)initWithReferences:(NSArray<FLEXObjectRef *> *)references {
     return [self initWithReferences:references predicates:nil sectionTitles:nil];
@@ -40,23 +101,21 @@
            sectionTitles:(NSArray<NSString *> *)sectionTitles {
     NSParameterAssert(predicates.count == sectionTitles.count);
 
-    self = [super init];
+    self = [super initWithStyle:UITableViewStylePlain];
     if (self) {
-        self.instances = references;
-        self.predicates = predicates;
-        self.sectionTitles = sectionTitles;
+        self.references = references;
 
         if (predicates.count) {
-            [self buildSections];
+            [self buildSections:sectionTitles predicates:predicates];
         } else {
-            self.sections = @[references];
+            _sections = _allSections = @[[self makeSection:references title:nil]];
         }
     }
 
     return self;
 }
 
-+ (instancetype)instancesTableViewControllerForClassName:(NSString *)className {
++ (instancetype)instancesOfClassWithName:(NSString *)className {
     const char *classNameCString = className.UTF8String;
     NSMutableArray *instances = [NSMutableArray array];
     [FLEXHeapEnumerator enumerateLiveObjectsUsingBlock:^(__unsafe_unretained id object, __unsafe_unretained Class actualClass) {
@@ -70,13 +129,25 @@
             }
         }
     }];
+    
     NSArray<FLEXObjectRef *> *references = [FLEXObjectRef referencingAll:instances];
-    FLEXInstancesViewController *viewController = [[self alloc] initWithReferences:references];
-    viewController.title = [NSString stringWithFormat:@"%@ (%lu)", className, (unsigned long)instances.count];
-    return viewController;
+    FLEXObjectListViewController *controller = [[self alloc] initWithReferences:references];
+    controller.title = [NSString stringWithFormat:@"%@ (%lu)", className, (unsigned long)instances.count];
+    return controller;
+}
+
++ (instancetype)subclassesOfClassWithName:(NSString *)className {
+    NSArray<Class> *classes = FLEXGetAllSubclasses(NSClassFromString(className), NO);
+    NSArray<FLEXObjectRef *> *references = [FLEXObjectRef referencingClasses:classes];
+    FLEXObjectListViewController *controller = [[self alloc] initWithReferences:references];
+    controller.title = [NSString stringWithFormat:@"Subclasses of %@ (%lu)",
+        className, (unsigned long)classes.count
+    ];
+    
+    return controller;
 }
 
-+ (instancetype)instancesTableViewControllerForInstancesReferencingObject:(id)object {
++ (instancetype)objectsWithReferencesToObject:(id)object {
     static Class SwiftObjectClass = nil;
     static dispatch_once_t onceToken;
     dispatch_once(&onceToken, ^{
@@ -117,131 +188,120 @@
 
     NSArray<NSPredicate *> *predicates = [self defaultPredicates];
     NSArray<NSString *> *sectionTitles = [self defaultSectionTitles];
-    FLEXInstancesViewController *viewController = [[self alloc] initWithReferences:instances
-                                                                             predicates:predicates
-                                                                          sectionTitles:sectionTitles];
-    viewController.title = [NSString stringWithFormat:@"Referencing %@ %p", NSStringFromClass(object_getClass(object)), object];
+    FLEXObjectListViewController *viewController = [[self alloc]
+        initWithReferences:instances
+        predicates:predicates
+        sectionTitles:sectionTitles
+    ];
+    viewController.title = [NSString stringWithFormat:@"Referencing %@ %p",
+        NSStringFromClass(object_getClass(object)), object
+    ];
     return viewController;
 }
 
-+ (NSPredicate *)defaultPredicateForSection:(NSInteger)section {
-    // These are the types of references that we typically don't care about.
-    // We want this list of "object-ivar pairs" split into two sections.
-    BOOL(^isObserver)(FLEXObjectRef *, NSDictionary *) = ^BOOL(FLEXObjectRef *ref, NSDictionary *bindings) {
-        NSString *row = ref.reference;
-        return [row isEqualToString:@"__NSObserver object"] ||
-               [row isEqualToString:@"_CFXNotificationObjcObserverRegistration _object"];
-    };
 
-    /// These are common AutoLayout related references we also rarely care about.
-    BOOL(^isConstraintRelated)(FLEXObjectRef *, NSDictionary *) = ^BOOL(FLEXObjectRef *ref, NSDictionary *bindings) {
-        static NSSet *ignored = nil;
-        static dispatch_once_t onceToken;
-        dispatch_once(&onceToken, ^{
-            ignored = [NSSet setWithArray:@[
-                @"NSLayoutConstraint _container",
-                @"NSContentSizeLayoutConstraint _container",
-                @"NSAutoresizingMaskLayoutConstraint _container",
-                @"MASViewConstraint _installedView",
-                @"MASLayoutConstraint _container",
-                @"MASViewAttribute _view"
-            ]];
-        });
+#pragma mark - Lifecycle
 
-        NSString *row = ref.reference;
-        return ([row hasPrefix:@"NSLayout"] && [row hasSuffix:@" _referenceItem"]) ||
-               ([row hasPrefix:@"NSIS"] && [row hasSuffix:@" _delegate"])  ||
-               ([row hasPrefix:@"_NSAutoresizingMask"] && [row hasSuffix:@" _referenceItem"]) ||
-               [ignored containsObject:row];
-    };
+- (void)viewDidLoad {
+    [super viewDidLoad];
+    
+    self.showsSearchBar = YES;
+}
 
-    BOOL(^isEssential)(FLEXObjectRef *, NSDictionary *) = ^BOOL(FLEXObjectRef *ref, NSDictionary *bindings) {
-        return !(isObserver(ref, bindings) || isConstraintRelated(ref, bindings));
-    };
 
-    switch (section) {
-        case 0: return [NSPredicate predicateWithBlock:isEssential];
-        case 1: return [NSPredicate predicateWithBlock:isConstraintRelated];
-        case 2: return [NSPredicate predicateWithBlock:isObserver];
+#pragma mark - Private
 
-        default: return nil;
+- (void)buildSections:(NSArray<NSString *> *)titles predicates:(NSArray<NSPredicate *> *)predicates {
+    NSParameterAssert(titles.count == predicates.count);
+    NSParameterAssert(titles); NSParameterAssert(predicates);
+    
+    _sections = _allSections = [NSArray flex_forEachUpTo:titles.count map:^id(NSUInteger i) {
+        NSArray *rows = [self.references filteredArrayUsingPredicate:predicates[i]];
+        return [self makeSection:rows title:titles[i]];
+    }];
+}
+
+- (FLEXCollectionContentSection *)makeSection:(NSArray *)rows title:(NSString *)title {
+    FLEXCollectionContentSection *section = [FLEXCollectionContentSection forCollection:rows];
+    // We need custom filtering because we do custom cell configuration
+    section.customFilter = ^BOOL(NSString *filterText, FLEXObjectRef *ref) {
+        if (ref.summary && [ref.summary localizedCaseInsensitiveContainsString:filterText]) {
+            return YES;
+        }
+        
+        return [ref.reference localizedCaseInsensitiveContainsString:filterText];
+    };
+    
+    // Use custom title, or hide title entirely
+    if (title) {
+        section.customTitle = title;
+    } else {
+        section.hideSectionTitle = YES;
     }
+    
+    return section;
 }
 
-+ (NSArray<NSPredicate *> *)defaultPredicates {
-    return @[[self defaultPredicateForSection:0],
-             [self defaultPredicateForSection:1],
-             [self defaultPredicateForSection:2]];
+- (NSArray *)nonemptySections {
+    return [self.allSections flex_filtered:^BOOL(FLEXTableViewSection *section, NSUInteger idx) {
+        return section.numberOfRows > 0;
+    }];
 }
 
-+ (NSArray<NSString *> *)defaultSectionTitles {
-    return @[@"", @"AutoLayout", @"Trivial"];
+- (FLEXObjectRef *)referenceForIndexPath:(NSIndexPath *)ip {
+    return [self.sections[ip.section] objectForRow:ip.row];
 }
 
-- (void)buildSections {
-    NSInteger maxSections = self.maxSections;
-    NSMutableArray *sections = [NSMutableArray array];
-    for (NSInteger i = 0; i < maxSections; i++) {
-        NSPredicate *predicate = self.predicates[i];
-        [sections addObject:[self.instances filteredArrayUsingPredicate:predicate]];
+
+#pragma mark - Search
+
+- (void)updateSearchResults:(NSString *)newText; {
+    // Sections will adjust data based on this property
+    for (FLEXTableViewSection *section in self.allSections) {
+        section.filterText = newText;
     }
 
-    self.sections = sections;
-}
+    // Recalculate empty sections
+    self.sections = [self nonemptySections];
 
-- (NSInteger)maxSections {
-    return self.predicates.count ?: 1;
+    // Refresh table view
+    if (self.isViewLoaded) {
+        [self.tableView reloadData];
+    }
 }
 
 
 #pragma mark - Table View Data Source
 
 - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
-    return self.maxSections;
+    return self.sections.count;
 }
 
 - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
-    return self.sections[section].count;
+    return self.sections[section].numberOfRows;
 }
 
 - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
-    static NSString *CellIdentifier = @"Cell";
-    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
-    if (!cell) {
-        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];
-        cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
-        UIFont *cellFont = UIFont.flex_defaultTableCellFont;
-        cell.textLabel.font = cellFont;
-        cell.detailTextLabel.font = cellFont;
-        cell.detailTextLabel.textColor = UIColor.grayColor;
-    }
-
-    FLEXObjectRef *row = self.sections[indexPath.section][indexPath.row];
-    cell.textLabel.text = row.reference;
-    cell.detailTextLabel.text = [FLEXRuntimeUtility summaryForObject:row.object];
+    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kFLEXDetailCell forIndexPath:indexPath];
+    FLEXObjectRef *ref = [self referenceForIndexPath:indexPath];
+    cell.textLabel.text = ref.reference;
+    cell.detailTextLabel.text = ref.summary;
+    cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
     
     return cell;
 }
 
 - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
-    if (self.sectionTitles.count) {
-        // Return nil instead of empty strings
-        NSString *title = self.sectionTitles[section];
-        if (title.length) {
-            return title;
-        }
-    }
-
-    return nil;
+    return self.sections[section].title;
 }
 
 
 #pragma mark - Table View Delegate
 
 - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
-    id instance = self.instances[indexPath.row].object;
-    FLEXObjectExplorerViewController *drillInViewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:instance];
-    [self.navigationController pushViewController:drillInViewController animated:YES];
+    [self.navigationController pushViewController:[FLEXObjectExplorerFactory
+        explorerViewControllerForObject:[self referenceForIndexPath:indexPath].object
+    ] animated:YES];
 }
 
 @end

+ 5 - 0
Classes/GlobalStateExplorers/FLEXObjectRef.h

@@ -14,9 +14,14 @@
 + (instancetype)referencing:(id)object ivar:(NSString *)ivarName;
 
 + (NSArray<FLEXObjectRef *> *)referencingAll:(NSArray *)objects;
+/// Classes do not have a summary, and the reference is just the class name.
++ (NSArray<FLEXObjectRef *> *)referencingClasses:(NSArray<Class> *)classes;
 
 /// For example, "NSString 0x1d4085d0" or "NSLayoutConstraint _object"
 @property (nonatomic, readonly) NSString *reference;
+/// For instances, this is the result of -[FLEXRuntimeUtility summaryForObject:]
+/// For classes, there is no summary.
+@property (nonatomic, readonly) NSString *summary;
 @property (nonatomic, readonly) id object;
 
 @end

+ 39 - 10
Classes/GlobalStateExplorers/FLEXObjectRef.m

@@ -7,41 +7,70 @@
 //
 
 #import "FLEXObjectRef.h"
-#import <objc/runtime.h>
+#import "FLEXRuntimeUtility.h"
+#import "NSArray+Functional.h"
+
+@interface FLEXObjectRef ()
+@property (nonatomic, readonly) BOOL wantsSummary;
+@end
 
 @implementation FLEXObjectRef
+@synthesize summary = _summary;
 
 + (instancetype)referencing:(id)object {
-    return [[self alloc] initWithObject:object ivarName:nil];
+    return [self referencing:object showSummary:YES];
+}
+
++ (instancetype)referencing:(id)object showSummary:(BOOL)showSummary {
+    return [[self alloc] initWithObject:object ivarName:nil showSummary:showSummary];
 }
 
 + (instancetype)referencing:(id)object ivar:(NSString *)ivarName {
-    return [[self alloc] initWithObject:object ivarName:ivarName];
+    return [[self alloc] initWithObject:object ivarName:ivarName showSummary:YES];
 }
 
 + (NSArray<FLEXObjectRef *> *)referencingAll:(NSArray *)objects {
-    NSMutableArray<FLEXObjectRef *> *refs = [NSMutableArray array];
-    for (id obj in objects) {
-        [refs addObject:[self referencing:obj]];
-    }
+    return [objects flex_mapped:^id(id obj, NSUInteger idx) {
+        return [self referencing:obj showSummary:YES];
+    }];
+}
 
-    return refs;
++ (NSArray<FLEXObjectRef *> *)referencingClasses:(NSArray<Class> *)classes {
+    return [classes flex_mapped:^id(id obj, NSUInteger idx) {
+        return [self referencing:obj showSummary:NO];
+    }];
 }
 
-- (id)initWithObject:(id)object ivarName:(NSString *)ivar {
+- (id)initWithObject:(id)object ivarName:(NSString *)ivar showSummary:(BOOL)showSummary {
     self = [super init];
     if (self) {
         _object = object;
+        _wantsSummary = showSummary;
 
         NSString *class = NSStringFromClass(object_getClass(object));
         if (ivar) {
             _reference = [NSString stringWithFormat:@"%@ %@", class, ivar];
-        } else {
+        } else if (showSummary) {
             _reference = [NSString stringWithFormat:@"%@ %p", class, object];
+        } else {
+            _reference = class;
         }
     }
 
     return self;
 }
 
+- (NSString *)summary {
+    if (self.wantsSummary) {
+        if (!_summary) {
+            _summary = [FLEXRuntimeUtility summaryForObject:self.object];
+        }
+        
+        return _summary;
+    }
+    else {
+        return nil;
+    }
+}
+
 @end

+ 15 - 21
Classes/ObjectExplorers/FLEXObjectExplorerViewController.m

@@ -14,7 +14,7 @@
 #import "FLEXObjectExplorerFactory.h"
 #import "FLEXFieldEditorViewController.h"
 #import "FLEXMethodCallingViewController.h"
-#import "FLEXInstancesViewController.h"
+#import "FLEXObjectListViewController.h"
 #import "FLEXTabsViewController.h"
 #import "FLEXBookmarkManager.h"
 #import "FLEXTableView.h"
@@ -146,8 +146,8 @@
         }
     ];
     referencesSection.selectionAction = ^(UIViewController *host) {
-        UIViewController *references = [FLEXInstancesViewController
-            instancesTableViewControllerForInstancesReferencingObject:explorer.object
+        UIViewController *references = [FLEXObjectListViewController
+            objectsWithReferencesToObject:explorer.object
         ];
         [host.navigationController pushViewController:references animated:YES];
     };
@@ -176,6 +176,10 @@
 
 - (NSArray<FLEXTableViewSection *> *)nonemptySections {
     return [self.allSections flex_filtered:^BOOL(FLEXTableViewSection *section, NSUInteger idx) {
+        if (!self.shouldShowDescription && section == self.descriptionSection) {
+            return NO;
+        }
+        
         return section.numberOfRows > 0;
     }];
 }
@@ -269,32 +273,16 @@
     // Check to see if class scope changed, update accordingly
     if (self.explorer.classScope != self.selectedScope) {
         self.explorer.classScope = self.selectedScope;
-        for (FLEXTableViewSection *section in self.allSections) {
-            [section reloadData];
-        }
+        [self reloadSections];
     }
 
-    // Recalculate empty sections
-    self.sections = [self nonemptySections];
-
-    // Refresh table view
-    if (self.isViewLoaded) {
-        [self.tableView reloadData];
-    }
+    [self reloadData];
 }
 
 
 #pragma mark - Reloading
 
 - (void)reloadData {
-    // Reload explorer
-    [self.explorer reloadMetadata];
-
-    // Reload sections
-    for (FLEXTableViewSection *section in self.allSections) {
-        [section reloadData];
-    }
-
     // Recalculate displayed sections
     self.sections = [self nonemptySections];
 
@@ -304,6 +292,12 @@
     }
 }
 
+- (void)reloadSections {
+    for (FLEXTableViewSection *section in self.allSections) {
+        [section reloadData];
+    }
+}
+
 
 #pragma mark - UITableViewDataSource
 

+ 39 - 6
Classes/ObjectExplorers/Sections/FLEXCollectionContentSection.h

@@ -8,8 +8,8 @@
 
 #import "FLEXTableViewSection.h"
 #import "FLEXObjectInfoSection.h"
-@class FLEXCollectionContentSection;
-@protocol FLEXCollection;
+@class FLEXCollectionContentSection, FLEXTableViewCell;
+@protocol FLEXCollection, FLEXMutableCollection;
 
 typedef id<FLEXCollection>(^FLEXCollectionContentFuture)(__kindof FLEXCollectionContentSection *section);
 
@@ -20,8 +20,8 @@ typedef id<FLEXCollection>(^FLEXCollectionContentFuture)(__kindof FLEXCollection
 
 @property (nonatomic, readonly) NSUInteger count;
 
-- (id)copy;
-- (id)mutableCopy;
+- (id<FLEXCollection>)copy;
+- (id<FLEXMutableCollection>)mutableCopy;
 
 @optional
 
@@ -37,16 +37,28 @@ typedef id<FLEXCollection>(^FLEXCollectionContentFuture)(__kindof FLEXCollection
 
 @end
 
+@protocol FLEXMutableCollection <FLEXCollection>
+- (void)filterUsingPredicate:(NSPredicate *)predicate;
+@end
+
 @interface NSArray (FLEXCollection) <FLEXCollection> @end
-@interface NSDictionary (FLEXCollection) <FLEXCollection> @end
 @interface NSSet (FLEXCollection) <FLEXCollection> @end
 @interface NSOrderedSet (FLEXCollection) <FLEXCollection> @end
+@interface NSDictionary (FLEXCollection) <FLEXCollection> @end
+
+@interface NSMutableArray (FLEXMutableCollection) <FLEXMutableCollection> @end
+@interface NSMutableSet (FLEXMutableCollection) <FLEXMutableCollection> @end
+@interface NSMutableOrderedSet (FLEXMutableCollection) <FLEXMutableCollection> @end
+@interface NSMutableDictionary (FLEXMutableCollection) <FLEXMutableCollection>
+- (void)filterUsingPredicate:(NSPredicate *)predicate;
+@end
+
 
 #pragma mark - FLEXCollectionContentSection
 /// A custom section for viewing collection elements.
 ///
 /// Tapping on a row pushes an object explorer for that element.
-@interface FLEXCollectionContentSection : FLEXTableViewSection <FLEXObjectInfoSection>
+@interface FLEXCollectionContentSection<__covariant ObjectType> : FLEXTableViewSection <FLEXObjectInfoSection>
 
 + (instancetype)forCollection:(id<FLEXCollection>)collection;
 /// The future given should be safe to call more than once.
@@ -54,4 +66,25 @@ typedef id<FLEXCollection>(^FLEXCollectionContentFuture)(__kindof FLEXCollection
 /// different results each time if the data is changing by nature.
 + (instancetype)forReusableFuture:(FLEXCollectionContentFuture)collectionFuture;
 
+/// Defaults to \c NO
+@property (nonatomic) BOOL hideSectionTitle;
+/// Defaults to \c nil
+@property (nonatomic) NSString *customTitle;
+/// Defaults to \c NO
+///
+/// Settings this to \c NO will not display the element index for ordered collections.
+/// This property only applies to \c NSArray or \c NSOrderedSet and their subclasses.
+@property (nonatomic) BOOL hideOrderIndexes;
+
+/// Set this property to provide a custom filter matcher.
+///
+/// By default, the collection will filter on the title and subtitle of the row.
+/// So if you don't ever call \c configureCell: for example, you will need to set
+/// this property so that your filter logic will match how you're setting up the cell. 
+@property (nonatomic) BOOL (^customFilter)(NSString *filterText, ObjectType element);
+
+/// Get the object in the collection associated with the given row.
+/// For dictionaries, this returns the value, not the key.
+- (ObjectType)objectForRow:(NSInteger)row;
+
 @end

+ 66 - 8
Classes/ObjectExplorers/Sections/FLEXCollectionContentSection.m

@@ -21,13 +21,14 @@ typedef NS_ENUM(NSUInteger, FLEXCollectionType) {
 };
 
 @interface FLEXCollectionContentSection ()
-@property (nonatomic) id<FLEXCollection> cachedCollection;
+@property (nonatomic, copy) id<FLEXCollection> cachedCollection;
 @property (nonatomic, readonly) id<FLEXCollection> collection;
 @property (nonatomic, readonly) FLEXCollectionContentFuture collectionFuture;
 @property (nonatomic, readonly) FLEXCollectionType collectionType;
 @end
 
 @implementation FLEXCollectionContentSection
+@synthesize filterText = _filterText;
 
 #pragma mark Initialization
 
@@ -39,7 +40,7 @@ typedef NS_ENUM(NSUInteger, FLEXCollectionType) {
     FLEXCollectionContentSection *section = [self new];
     section->_collectionType = [self typeForCollection:collection];
     section->_collection = collection;
-    section.cachedCollection = collection.copy;
+    section.cachedCollection = collection;
     return section;
 }
 
@@ -51,14 +52,15 @@ typedef NS_ENUM(NSUInteger, FLEXCollectionType) {
     return section;
 }
 
+
 #pragma mark - Misc
 
 + (FLEXCollectionType)typeForCollection:(id<FLEXCollection>)collection {
     // Order matters here, as NSDictionary is keyed but it responds to allObjects
-    if ([collection respondsToSelector:@selector(objectAtIndexedSubscript:)]) {
+    if ([collection respondsToSelector:@selector(objectAtIndex:)]) {
         return FLEXOrderedCollection;
     }
-    if ([collection respondsToSelector:@selector(objectForKeyedSubscript:)]) {
+    if ([collection respondsToSelector:@selector(objectForKey:)]) {
         return FLEXKeyedCollection;
     }
     if ([collection respondsToSelector:@selector(allObjects)]) {
@@ -77,7 +79,10 @@ typedef NS_ENUM(NSUInteger, FLEXCollectionType) {
 - (NSString *)titleForRow:(NSInteger)row {
     switch (self.collectionType) {
         case FLEXOrderedCollection:
-            return @(row).stringValue;
+            if (!self.hideOrderIndexes) {
+                return @(row).stringValue;
+            }
+            // Fall-through
         case FLEXUnorderedCollection:
             return [self describe:[self objectForRow:row]];
         case FLEXKeyedCollection:
@@ -95,6 +100,10 @@ typedef NS_ENUM(NSUInteger, FLEXCollectionType) {
 - (NSString *)subtitleForRow:(NSInteger)row {
     switch (self.collectionType) {
         case FLEXOrderedCollection:
+            if (!self.hideOrderIndexes) {
+                nil;
+            }
+            // Fall-through
         case FLEXKeyedCollection:
             return [self describe:[self objectForRow:row]];
         case FLEXUnorderedCollection:
@@ -126,13 +135,41 @@ typedef NS_ENUM(NSUInteger, FLEXCollectionType) {
 #pragma mark - Overrides
 
 - (NSString *)title {
-    return FLEXPluralString(self.cachedCollection.count, @"Entries", @"Entry");
+    if (!self.hideSectionTitle) {
+        if (self.customTitle) {
+            return self.customTitle;
+        }
+        
+        return FLEXPluralString(self.cachedCollection.count, @"Entries", @"Entry");
+    }
+    
+    return nil;
 }
 
 - (NSInteger)numberOfRows {
     return self.cachedCollection.count;
 }
 
+- (void)setFilterText:(NSString *)filterText {
+    super.filterText = filterText;
+    
+    if (filterText.length) {
+        BOOL (^matcher)(id, id) = self.customFilter ?: ^BOOL(NSString *query, id obj) {
+            return [[self describe:obj] localizedCaseInsensitiveContainsString:query];
+        };
+        
+        NSPredicate *filter = [NSPredicate predicateWithBlock:^BOOL(id obj, NSDictionary *bindings) {
+            return matcher(filterText, obj);
+        }];
+        
+        id<FLEXMutableCollection> tmp = self.collection.mutableCopy;
+        [tmp filterUsingPredicate:filter];
+        self.cachedCollection = tmp;
+    } else {
+        self.cachedCollection = self.collection;
+    }
+}
+
 - (void)reloadData {
     if (self.collectionFuture) {
         self.cachedCollection = self.collectionFuture(self);
@@ -150,8 +187,7 @@ typedef NS_ENUM(NSUInteger, FLEXCollectionType) {
 }
 
 - (NSString *)reuseIdentifierForRow:(NSInteger)row {
-    // Default for unordered, subtitle for others
-    return self.collectionType == FLEXUnorderedCollection ? kFLEXDefaultCell : kFLEXDetailCell;
+    return kFLEXDetailCell;
 }
 
 - (void)configureCell:(__kindof FLEXTableViewCell *)cell forRow:(NSInteger)row {
@@ -161,3 +197,25 @@ typedef NS_ENUM(NSUInteger, FLEXCollectionType) {
 }
 
 @end
+
+
+#pragma mark - NSMutableDictionary
+
+@implementation NSMutableDictionary (FLEXMutableCollection)
+
+- (void)filterUsingPredicate:(NSPredicate *)predicate {
+    id test = ^BOOL(id key, NSUInteger idx, BOOL *stop) {
+        if ([predicate evaluateWithObject:key]) {
+            return NO;
+        }
+        
+        return ![predicate evaluateWithObject:self[key]];
+    };
+    
+    NSArray *keys = self.allKeys;
+    NSIndexSet *remove = [keys indexesOfObjectsPassingTest:test];
+    
+    [self removeObjectsForKeys:[keys objectsAtIndexes:remove]];
+}
+
+@end

+ 8 - 8
FLEX.xcodeproj/project.pbxproj

@@ -76,8 +76,8 @@
 		3A4C95251B5B21410088C3F2 /* FLEXFileBrowserTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3A4C94A21B5B21410088C3F2 /* FLEXFileBrowserTableViewController.m */; };
 		3A4C95261B5B21410088C3F2 /* FLEXGlobalsViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 3A4C94A31B5B21410088C3F2 /* FLEXGlobalsViewController.h */; };
 		3A4C95271B5B21410088C3F2 /* FLEXGlobalsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3A4C94A41B5B21410088C3F2 /* FLEXGlobalsViewController.m */; };
-		3A4C95281B5B21410088C3F2 /* FLEXInstancesViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 3A4C94A51B5B21410088C3F2 /* FLEXInstancesViewController.h */; settings = {ATTRIBUTES = (Private, ); }; };
-		3A4C95291B5B21410088C3F2 /* FLEXInstancesViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3A4C94A61B5B21410088C3F2 /* FLEXInstancesViewController.m */; };
+		3A4C95281B5B21410088C3F2 /* FLEXObjectListViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 3A4C94A51B5B21410088C3F2 /* FLEXObjectListViewController.h */; settings = {ATTRIBUTES = (Private, ); }; };
+		3A4C95291B5B21410088C3F2 /* FLEXObjectListViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3A4C94A61B5B21410088C3F2 /* FLEXObjectListViewController.m */; };
 		3A4C952C1B5B21410088C3F2 /* FLEXLiveObjectsTableViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 3A4C94A91B5B21410088C3F2 /* FLEXLiveObjectsTableViewController.h */; settings = {ATTRIBUTES = (Private, ); }; };
 		3A4C952D1B5B21410088C3F2 /* FLEXLiveObjectsTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3A4C94AA1B5B21410088C3F2 /* FLEXLiveObjectsTableViewController.m */; };
 		3A4C952E1B5B21410088C3F2 /* FLEXWebViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 3A4C94AB1B5B21410088C3F2 /* FLEXWebViewController.h */; settings = {ATTRIBUTES = (Private, ); }; };
@@ -417,8 +417,8 @@
 		3A4C94A21B5B21410088C3F2 /* FLEXFileBrowserTableViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXFileBrowserTableViewController.m; sourceTree = "<group>"; };
 		3A4C94A31B5B21410088C3F2 /* FLEXGlobalsViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXGlobalsViewController.h; sourceTree = "<group>"; };
 		3A4C94A41B5B21410088C3F2 /* FLEXGlobalsViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXGlobalsViewController.m; sourceTree = "<group>"; };
-		3A4C94A51B5B21410088C3F2 /* FLEXInstancesViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXInstancesViewController.h; sourceTree = "<group>"; };
-		3A4C94A61B5B21410088C3F2 /* FLEXInstancesViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXInstancesViewController.m; sourceTree = "<group>"; };
+		3A4C94A51B5B21410088C3F2 /* FLEXObjectListViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXObjectListViewController.h; sourceTree = "<group>"; };
+		3A4C94A61B5B21410088C3F2 /* FLEXObjectListViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXObjectListViewController.m; sourceTree = "<group>"; };
 		3A4C94A91B5B21410088C3F2 /* FLEXLiveObjectsTableViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXLiveObjectsTableViewController.h; sourceTree = "<group>"; };
 		3A4C94AA1B5B21410088C3F2 /* FLEXLiveObjectsTableViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXLiveObjectsTableViewController.m; sourceTree = "<group>"; };
 		3A4C94AB1B5B21410088C3F2 /* FLEXWebViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXWebViewController.h; sourceTree = "<group>"; };
@@ -873,8 +873,8 @@
 				779B1EBF1C0C4D7C001F5E49 /* DatabaseBrowser */,
 				3A4C94AD1B5B21410088C3F2 /* SystemLog */,
 				C33CF16B22D664E600F9C6C0 /* FileBrowser */,
-				3A4C94A51B5B21410088C3F2 /* FLEXInstancesViewController.h */,
-				3A4C94A61B5B21410088C3F2 /* FLEXInstancesViewController.m */,
+				3A4C94A51B5B21410088C3F2 /* FLEXObjectListViewController.h */,
+				3A4C94A61B5B21410088C3F2 /* FLEXObjectListViewController.m */,
 				C3DB9F622107FC9600B46809 /* FLEXObjectRef.h */,
 				C3DB9F632107FC9600B46809 /* FLEXObjectRef.m */,
 				3A4C94A91B5B21410088C3F2 /* FLEXLiveObjectsTableViewController.h */,
@@ -1414,7 +1414,7 @@
 				3A4C95091B5B21410088C3F2 /* FLEXFieldEditorView.h in Headers */,
 				C3E5D9FD2316E83700E655DB /* FLEXRuntime+Compare.h in Headers */,
 				C3490E1F233BDD73002AE200 /* FLEXSingleRowSection.h in Headers */,
-				3A4C95281B5B21410088C3F2 /* FLEXInstancesViewController.h in Headers */,
+				3A4C95281B5B21410088C3F2 /* FLEXObjectListViewController.h in Headers */,
 				C36B097023E1EDCD008F5D47 /* FLEXTableViewSection.h in Headers */,
 				C3531BAA23E88FAC00A184AD /* FLEXTabList.h in Headers */,
 				3A4C950F1B5B21410088C3F2 /* FLEXMethodCallingViewController.h in Headers */,
@@ -1772,7 +1772,7 @@
 				3A4C94E81B5B21410088C3F2 /* FLEXHierarchyTableViewCell.m in Sources */,
 				3A4C950A1B5B21410088C3F2 /* FLEXFieldEditorView.m in Sources */,
 				3A4C95061B5B21410088C3F2 /* FLEXArgumentInputViewFactory.m in Sources */,
-				3A4C95291B5B21410088C3F2 /* FLEXInstancesViewController.m in Sources */,
+				3A4C95291B5B21410088C3F2 /* FLEXObjectListViewController.m in Sources */,
 				C397E319240EC98F0091E4EC /* FLEXSQLResult.m in Sources */,
 				C31D93E923E38E97005517BF /* FLEXBlockDescription.m in Sources */,
 				71E1C2182307FBB800F5032A /* FLEXKeychainTableViewController.m in Sources */,