Przeglądaj źródła

Add automatic filtering table view controller

Also add FLEXMutableListSection which wraps the collection content section in a way that makes displaying a simple list of content straightforward.

Adopt additions in appropriate view controllers.
Tanner Bennett 6 lat temu
rodzic
commit
da67902cf5
37 zmienionych plików z 886 dodań i 666 usunięć
  1. 89 0
      Classes/Core/Controllers/FLEXFilteringTableViewController.h
  2. 203 0
      Classes/Core/Controllers/FLEXFilteringTableViewController.m
  3. 17 11
      Classes/Core/Controllers/FLEXTableViewController.h
  4. 9 6
      Classes/Core/Controllers/FLEXTableViewController.m
  5. 2 2
      Classes/ExplorerInterface/FLEXViewControllersViewController.h
  6. 26 38
      Classes/ExplorerInterface/FLEXViewControllersViewController.m
  7. 2 0
      Classes/ExplorerInterface/FLEXWindowManagerController.m
  8. 2 2
      Classes/GlobalStateExplorers/DatabaseBrowser/FLEXTableListViewController.h
  9. 31 51
      Classes/GlobalStateExplorers/DatabaseBrowser/FLEXTableListViewController.m
  10. 2 2
      Classes/GlobalStateExplorers/FLEXCookiesTableViewController.h
  11. 34 51
      Classes/GlobalStateExplorers/FLEXCookiesTableViewController.m
  12. 0 1
      Classes/GlobalStateExplorers/FLEXLiveObjectsTableViewController.m
  13. 2 2
      Classes/GlobalStateExplorers/FLEXObjectListViewController.h
  14. 41 95
      Classes/GlobalStateExplorers/FLEXObjectListViewController.m
  15. 1 1
      Classes/GlobalStateExplorers/Globals/FLEXGlobalsEntry.h
  16. 13 1
      Classes/GlobalStateExplorers/Globals/FLEXGlobalsEntry.m
  17. 3 0
      Classes/GlobalStateExplorers/Globals/FLEXGlobalsSection.m
  18. 2 2
      Classes/GlobalStateExplorers/Globals/FLEXGlobalsViewController.h
  19. 17 87
      Classes/GlobalStateExplorers/Globals/FLEXGlobalsViewController.m
  20. 1 1
      Classes/GlobalStateExplorers/Keychain/FLEXKeychain.m
  21. 1 1
      Classes/GlobalStateExplorers/Keychain/FLEXKeychainQuery.m
  22. 2 2
      Classes/GlobalStateExplorers/Keychain/FLEXKeychainTableViewController.h
  23. 89 51
      Classes/GlobalStateExplorers/Keychain/FLEXKeychainTableViewController.m
  24. 3 3
      Classes/GlobalStateExplorers/RuntimeBrowser/FLEXObjcRuntimeViewController.m
  25. 2 2
      Classes/GlobalStateExplorers/SystemLog/FLEXSystemLogCell.h
  26. 8 10
      Classes/GlobalStateExplorers/SystemLog/FLEXSystemLogCell.m
  27. 2 2
      Classes/GlobalStateExplorers/SystemLog/FLEXSystemLogViewController.h
  28. 51 53
      Classes/GlobalStateExplorers/SystemLog/FLEXSystemLogViewController.m
  29. 2 2
      Classes/ObjectExplorers/FLEXObjectExplorerViewController.h
  30. 28 181
      Classes/ObjectExplorers/FLEXObjectExplorerViewController.m
  31. 11 3
      Classes/ObjectExplorers/Sections/FLEXCollectionContentSection.h
  32. 2 1
      Classes/ObjectExplorers/Sections/FLEXCollectionContentSection.m
  33. 58 0
      Classes/ObjectExplorers/Sections/FLEXMutableListSection.h
  34. 107 0
      Classes/ObjectExplorers/Sections/FLEXMutableListSection.m
  35. 1 0
      Classes/Utility/Categories/UIBarButtonItem+FLEX.h
  36. 4 0
      Classes/Utility/Categories/UIBarButtonItem+FLEX.m
  37. 18 2
      FLEX.xcodeproj/project.pbxproj

+ 89 - 0
Classes/Core/Controllers/FLEXFilteringTableViewController.h

@@ -0,0 +1,89 @@
+//
+//  FLEXFilteringTableViewController.h
+//  FLEX
+//
+//  Created by Tanner on 3/9/20.
+//  Copyright © 2020 Flipboard. All rights reserved.
+//
+
+#import "FLEXTableViewController.h"
+
+#pragma mark - FLEXTableViewFiltering
+@protocol FLEXTableViewFiltering <FLEXSearchResultsUpdating>
+
+/// An array of visible, "filtered" sections. For example,
+/// if you have 3 sections in \c allSections and the user searches
+/// for something that matches rows in only one section, then
+/// this property would only contain that on matching section.
+@property (nonatomic, copy) NSArray<FLEXTableViewSection *> *sections;
+/// An array of all possible sections. Empty sections are to be removed
+/// and the resulting array stored in the \c section property. Setting
+/// this property should immediately set \c sections to \c nonemptySections 
+///
+/// Do not manually initialize this property, it will be
+/// initialized for you using the result of \c makeSections.
+@property (nonatomic, copy) NSArray<FLEXTableViewSection *> *allSections;
+
+/// This computed property should filter \c allSections for assignment to \c sections
+@property (nonatomic, readonly) NSArray<FLEXTableViewSection *> *nonemptySections;
+
+/// This should be able to re-initialize \c allSections
+- (NSArray<FLEXTableViewSection *> *)makeSections;
+
+@end
+
+
+#pragma mark - FLEXFilteringTableViewController
+/// A table view which implements \c UITableView* methods using arrays of
+/// \c FLEXTableViewSection objects provied by a special delegate.
+@interface FLEXFilteringTableViewController : FLEXTableViewController <FLEXTableViewFiltering>
+
+/// Stores the current search query.
+@property (nonatomic, copy) NSString *filterText;
+
+/// This property is set to \c self by default.
+///
+/// This property is used to power almost all of the table view's data source
+/// and delegate methods automatically, including row and section filtering
+/// when the user searches, 3D Touch context menus, row selection, etc.
+///
+/// Setting this property will also set \c searchDelegate to that object.
+@property (nonatomic, weak) id<FLEXTableViewFiltering> filterDelegate;
+
+/// Defaults to \c NO. If enabled, all filtering will be done by calling
+/// \c onBackgroundQueue:thenOnMainQueue: with the UI updated on the main queue.
+@property (nonatomic) BOOL filterInBackground;
+
+/// Defaults to \c NO. If enabled, one • will be supplied as an index title for each section.
+@property (nonatomic) BOOL wantsSectionIndexTitles;
+
+/// Recalculates the non-empty sections and reloads the table view.
+///
+/// Subclasses may override to perform additional reloading logic,
+/// such as calling \c -reloadSections if needed. Be sure to call
+/// \c super after any logic that would affect the appearance of 
+/// the table view, since the table view is reloaded last.
+///
+/// Called at the end of this class's implementation of \c updateSearchResults:
+- (void)reloadData;
+
+/// Invoke this method to call \c -reloadData on each section
+/// in \c self.filterDelegate.allSections.
+- (void)reloadSections;
+
+#pragma mark FLEXTableViewFiltering
+
+@property (nonatomic, copy) NSArray<FLEXTableViewSection *> *sections;
+@property (nonatomic, copy) NSArray<FLEXTableViewSection *> *allSections;
+
+/// Subclasses can override to hide specific sections under certain conditions
+/// if using \c self as the \c filterDelegate, as is the default.
+///
+/// For example, the object explorer hides the description section when searching.
+@property (nonatomic, readonly) NSArray<FLEXTableViewSection *> *nonemptySections;
+
+/// If using \c self as the \c filterDelegate, as is the default,
+/// subclasses should override to provide the sections for the table view.
+- (NSArray<FLEXTableViewSection *> *)makeSections;
+
+@end

+ 203 - 0
Classes/Core/Controllers/FLEXFilteringTableViewController.m

@@ -0,0 +1,203 @@
+//
+//  FLEXFilteringTableViewController.m
+//  FLEX
+//
+//  Created by Tanner on 3/9/20.
+//  Copyright © 2020 Flipboard. All rights reserved.
+//
+
+#import "FLEXFilteringTableViewController.h"
+#import "FLEXTableViewSection.h"
+#import "NSArray+Functional.h"
+
+@interface FLEXFilteringTableViewController ()
+
+@end
+
+@implementation FLEXFilteringTableViewController
+@synthesize allSections = _allSections;
+
+#pragma mark - View controller lifecycle
+
+- (void)loadView {
+    [super loadView];
+    
+    if (!self.filterDelegate) {
+        self.filterDelegate = self;
+    } else {
+        [self _registerCellsForReuse];
+    }
+}
+
+- (void)_registerCellsForReuse {
+    for (FLEXTableViewSection *section in self.filterDelegate.allSections) {
+        if (section.cellRegistrationMapping) {
+            [self.tableView registerCells:section.cellRegistrationMapping];
+        }
+    }
+}
+
+
+#pragma mark - Public
+
+- (void)setFilterDelegate:(id<FLEXTableViewFiltering>)filterDelegate {
+    _filterDelegate = filterDelegate;
+    filterDelegate.allSections = [filterDelegate makeSections];
+    
+    if (self.isViewLoaded) {
+        [self _registerCellsForReuse];
+    }
+}
+
+- (void)reloadData {
+    [self reloadData:self.nonemptySections];
+}
+
+- (void)reloadData:(NSArray *)nonemptySections {
+    // Recalculate displayed sections
+    self.filterDelegate.sections = nonemptySections;
+
+    // Refresh table view
+    if (self.isViewLoaded) {
+        [self.tableView reloadData];
+    }
+}
+
+- (void)reloadSections {
+    for (FLEXTableViewSection *section in self.filterDelegate.allSections) {
+        [section reloadData];
+    }
+}
+
+
+#pragma mark - Search
+
+- (void)updateSearchResults:(NSString *)newText {
+    NSArray *(^filter)() = ^NSArray *{
+        self.filterText = newText;
+
+        // Sections will adjust data based on this property
+        for (FLEXTableViewSection *section in self.filterDelegate.allSections) {
+            section.filterText = newText;
+        }
+        
+        return nil;
+    };
+    
+    if (self.filterInBackground) {
+        [self onBackgroundQueue:filter thenOnMainQueue:^(NSArray *unused) {
+            if ([self.searchText isEqualToString:newText]) {
+                [self reloadData];
+            }
+        }];
+    } else {
+        filter();
+        [self reloadData];
+    }
+}
+
+
+#pragma mark Filtering
+
+- (NSArray<FLEXTableViewSection *> *)nonemptySections {
+    return [self.filterDelegate.allSections flex_filtered:^BOOL(FLEXTableViewSection *section, NSUInteger idx) {
+        return section.numberOfRows > 0;
+    }];
+}
+
+- (NSArray<FLEXTableViewSection *> *)makeSections {
+    return @[];
+}
+
+- (void)setAllSections:(NSArray<FLEXTableViewSection *> *)allSections {
+    _allSections = allSections.copy;
+    self.sections = self.nonemptySections;
+}
+
+
+#pragma mark - UITableViewDataSource
+
+- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
+    return self.filterDelegate.sections.count;
+}
+
+- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
+    return self.filterDelegate.sections[section].numberOfRows;
+}
+
+- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
+    return self.filterDelegate.sections[section].title;
+}
+
+- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
+    NSString *reuse = [self.filterDelegate.sections[indexPath.section] reuseIdentifierForRow:indexPath.row];
+    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:reuse forIndexPath:indexPath];
+    [self.filterDelegate.sections[indexPath.section] configureCell:cell forRow:indexPath.row];
+    return cell;
+}
+
+- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
+    return UITableViewAutomaticDimension;
+}
+
+- (NSArray<NSString *> *)sectionIndexTitlesForTableView:(UITableView *)tableView {
+    if (self.wantsSectionIndexTitles) {
+        return [NSArray flex_forEachUpTo:self.filterDelegate.sections.count map:^id(NSUInteger i) {
+            return @"⦁";
+        }];
+    }
+    
+    return nil;
+}
+
+
+#pragma mark - UITableViewDelegate
+
+- (BOOL)tableView:(UITableView *)tableView shouldHighlightRowAtIndexPath:(NSIndexPath *)indexPath {
+    return [self.filterDelegate.sections[indexPath.section] canSelectRow:indexPath.row];
+}
+
+- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
+    FLEXTableViewSection *section = self.filterDelegate.sections[indexPath.section];
+
+    void (^action)(UIViewController *) = [section didSelectRowAction:indexPath.row];
+    UIViewController *details = [section viewControllerToPushForRow:indexPath.row];
+
+    if (action) {
+        action(self);
+        [tableView deselectRowAtIndexPath:indexPath animated:YES];
+    } else if (details) {
+        [self.navigationController pushViewController:details animated:YES];
+    } else {
+        [NSException raise:NSInternalInconsistencyException
+                    format:@"Row is selectable but has no action or view controller"];
+    }
+}
+
+- (void)tableView:(UITableView *)tableView accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)indexPath {
+    [self.filterDelegate.sections[indexPath.section] didPressInfoButtonAction:indexPath.row](self);
+}
+
+#if FLEX_AT_LEAST_IOS13_SDK
+
+- (UIContextMenuConfiguration *)tableView:(UITableView *)tableView contextMenuConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath point:(CGPoint)point __IOS_AVAILABLE(13.0) {
+    FLEXTableViewSection *section = self.filterDelegate.sections[indexPath.section];
+    NSString *title = [section menuTitleForRow:indexPath.row];
+    NSArray<UIMenuElement *> *menuItems = [section menuItemsForRow:indexPath.row sender:self];
+    
+    if (menuItems.count) {
+        return [UIContextMenuConfiguration
+            configurationWithIdentifier:nil
+            previewProvider:nil
+            actionProvider:^UIMenu *(NSArray<UIMenuElement *> *suggestedActions) {
+                return [UIMenu menuWithTitle:title children:menuItems];
+            }
+        ];
+    }
+    
+    return nil;
+}
+
+#endif
+
+@end

+ 17 - 11
Classes/Core/Controllers/FLEXTableViewController.h

@@ -8,7 +8,7 @@
 
 #import <UIKit/UIKit.h>
 #import "FLEXTableView.h"
-@class FLEXScopeCarousel, FLEXWindow;
+@class FLEXScopeCarousel, FLEXWindow, FLEXTableViewSection;
 
 typedef CGFloat FLEXDebounceInterval;
 /// No delay, all events delivered
@@ -21,21 +21,34 @@ extern CGFloat const kFLEXDebounceForAsyncSearch;
 extern CGFloat const kFLEXDebounceForExpensiveIO;
 
 @protocol FLEXSearchResultsUpdating <NSObject>
+/// A method to handle search query update events.
+///
+/// \c searchBarDebounceInterval is used to reduce the frequency at which this
+/// method is called. This method is also called when the search bar becomes
+/// the first responder, and when the selected search bar scope index changes.
 - (void)updateSearchResults:(NSString *)newText;
 @end
 
 @interface FLEXTableViewController : UITableViewController <
-    UISearchResultsUpdating, UISearchControllerDelegate,
-    UISearchBarDelegate, FLEXSearchResultsUpdating
+    UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate
 >
 
 /// A grouped table view. Inset on iOS 13.
 ///
-/// Simply calls into initWithStyle:
+/// Simply calls into \c initWithStyle:
 - (id)init;
 
+/// Subclasses may override to configure the controller before \c viewDidLoad:
+- (id)initWithStyle:(UITableViewStyle)style;
+
 @property (nonatomic) FLEXTableView *tableView;
 
+/// If your subclass conforms to \c FLEXSearchResultsUpdating
+/// then this property is assigned to \c self automatically.
+///
+/// Setting \c filterDelegate will also set this property to that object.
+@property (nonatomic, weak) id<FLEXSearchResultsUpdating> searchDelegate;
+
 /// Defaults to NO.
 ///
 /// Setting this to YES will initialize the carousel and the view.
@@ -98,13 +111,6 @@ extern CGFloat const kFLEXDebounceForExpensiveIO;
 /// self.view.window as a \c FLEXWindow
 @property (nonatomic, readonly) FLEXWindow *window;
 
-/// Subclasses should override to handle search query update events.
-///
-/// searchBarDebounceInterval is used to reduce the frequency at which this method is called.
-/// This method is also called when the search bar becomes the first responder,
-/// and when the selected search bar scope index changes.
-- (void)updateSearchResults:(NSString *)newText;
-
 /// Convenient for doing some async processor-intensive searching
 /// in the background before updating the UI back on the main queue.
 - (void)onBackgroundQueue:(NSArray *(^)(void))backgroundBlock thenOnMainQueue:(void(^)(NSArray *))mainBlock;

+ 9 - 6
Classes/Core/Controllers/FLEXTableViewController.m

@@ -72,6 +72,11 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
         _manuallyDeactivateSearchOnDisappear = ({
             NSProcessInfo.processInfo.operatingSystemVersion.majorVersion < 11;
         });
+        
+        // We will be our own search delegate if we implement this method
+        if ([self respondsToSelector:@selector(updateSearchResults:)]) {
+            self.searchDelegate = (id)self;
+        }
     }
     
     return self;
@@ -125,7 +130,7 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
             FLEXScopeCarousel *carousel = [FLEXScopeCarousel new];
             carousel.selectedIndexChangedAction = ^(NSInteger idx) {
                 __typeof(self) self = weakSelf;
-                [self updateSearchResults:self.searchText];
+                [self.searchDelegate updateSearchResults:self.searchText];
             };
 
             // UITableView won't update the header size unless you reset the header view
@@ -160,7 +165,7 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
         self.carousel.selectedIndex = selectedScope;
     }
 
-    [self updateSearchResults:self.searchText];
+    [self.searchDelegate updateSearchResults:self.searchText];
 }
 
 - (NSString *)searchText {
@@ -187,8 +192,6 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
     _automaticallyShowsSearchBarCancelButton = value;
 }
 
-- (void)updateSearchResults:(NSString *)newText { }
-
 - (void)onBackgroundQueue:(NSArray *(^)(void))backgroundBlock thenOnMainQueue:(void(^)(NSArray *))mainBlock {
     dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
         NSArray *items = backgroundBlock();
@@ -532,7 +535,7 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
         if (self.searchResultsUpdater) {
             [self.searchResultsUpdater updateSearchResults:text];
         } else {
-            [self updateSearchResults:text];
+            [self.searchDelegate updateSearchResults:text];
         }
     };
     
@@ -571,7 +574,7 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
 }
 
 
-#pragma mark Table view
+#pragma mark Table View
 
 /// Not having a title in the first section looks weird with a rounded-corner table view style
 - (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {

+ 2 - 2
Classes/ExplorerInterface/FLEXViewControllersViewController.h

@@ -6,11 +6,11 @@
 //  Copyright © 2020 Flipboard. All rights reserved.
 //
 
-#import "FLEXTableViewController.h"
+#import "FLEXFilteringTableViewController.h"
 
 NS_ASSUME_NONNULL_BEGIN
 
-@interface FLEXViewControllersViewController : FLEXTableViewController
+@interface FLEXViewControllersViewController : FLEXFilteringTableViewController
 
 + (instancetype)controllersForViews:(NSArray<UIView *> *)views;
 

+ 26 - 38
Classes/ExplorerInterface/FLEXViewControllersViewController.m

@@ -8,12 +8,15 @@
 
 #import "FLEXViewControllersViewController.h"
 #import "FLEXObjectExplorerFactory.h"
+#import "FLEXMutableListSection.h"
 
 @interface FLEXViewControllersViewController ()
+@property (nonatomic, readonly) FLEXMutableListSection *section;
 @property (nonatomic, readonly) NSArray<UIViewController *> *controllers;
 @end
 
 @implementation FLEXViewControllersViewController
+@dynamic sections, allSections;
 
 #pragma mark - Initialization
 
@@ -24,7 +27,7 @@
 - (id)initWithViews:(NSArray<UIView *> *)views {
     NSParameterAssert(views.count);
     
-    self = [self init];
+    self = [self initWithStyle:UITableViewStylePlain];
     if (self) {
         _controllers = [views flex_mapped:^id(UIView *view, NSUInteger idx) {
             return [FLEXUtility viewControllerForView:view];
@@ -34,10 +37,6 @@
     return self;
 }
 
-- (id)init {
-    return [self initWithStyle:UITableViewStylePlain];
-}
-
 - (void)viewDidLoad {
     [super viewDidLoad];
     
@@ -45,45 +44,34 @@
     [self disableToolbar];
 }
 
-
-#pragma mark - Private
-
-- (void)dismissAnimated {
-    [self dismissViewControllerAnimated:YES completion:nil];
-}
-
-
-#pragma mark - Table View Data Source
-
-- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
-    return self.controllers.count;
-}
-
-- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
-    return @"View Controllers";
-}
-
-- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
-    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kFLEXDetailCell forIndexPath:indexPath];
-    UIViewController *controller = self.controllers[indexPath.row];
+- (NSArray<FLEXTableViewSection *> *)makeSections {
+    _section = [FLEXMutableListSection list:self.controllers
+        cellConfiguration:^(UITableViewCell *cell, UIViewController *controller, NSInteger row) {
+            cell.textLabel.text = [NSString
+                stringWithFormat:@"%@ — %p", NSStringFromClass(controller.class), controller
+            ];
+            cell.detailTextLabel.text = controller.view.description;
+            cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
+            cell.textLabel.lineBreakMode = NSLineBreakByTruncatingTail;
+    } filterMatcher:^BOOL(NSString *filterText, UIViewController *controller) {
+        return [NSStringFromClass(controller.class) localizedCaseInsensitiveContainsString:filterText];
+    }];
     
-    cell.textLabel.text = [NSString
-        stringWithFormat:@"%@ — %p", NSStringFromClass(controller.class), controller
-    ];
-    cell.detailTextLabel.text = controller.view.description;
-    cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
-    cell.textLabel.lineBreakMode = NSLineBreakByTruncatingTail;
+    self.section.selectionHandler = ^(UIViewController *host, UIViewController *controller) {
+        [host.navigationController pushViewController:
+            [FLEXObjectExplorerFactory explorerViewControllerForObject:controller]
+        animated:YES];
+    };
     
-    return cell;
+    self.section.customTitle = @"View Controllers";
+    return @[self.section];
 }
 
 
-#pragma mark - Table View Delegate
+#pragma mark - Private
 
-- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
-    [self.navigationController pushViewController:
-        [FLEXObjectExplorerFactory explorerViewControllerForObject:self.controllers[indexPath.row]]
-    animated:YES];
+- (void)dismissAnimated {
+    [self dismissViewControllerAnimated:YES completion:nil];
 }
 
 @end

+ 2 - 0
Classes/ExplorerInterface/FLEXWindowManagerController.m

@@ -64,6 +64,8 @@
     } else {
         self.sections = @[@[self.keyWindow], self.windows];
     }
+    
+    [self.tableView reloadData];
 }
 
 - (void)dismissAnimated {

+ 2 - 2
Classes/GlobalStateExplorers/DatabaseBrowser/FLEXTableListViewController.h

@@ -6,9 +6,9 @@
 //  Copyright © 2015年 Peng Tao. All rights reserved.
 //
 
-#import "FLEXTableViewController.h"
+#import "FLEXFilteringTableViewController.h"
 
-@interface FLEXTableListViewController : FLEXTableViewController
+@interface FLEXTableListViewController : FLEXFilteringTableViewController
 
 + (BOOL)supportsExtension:(NSString *)extension;
 - (instancetype)initWithPath:(NSString *)path;

+ 31 - 51
Classes/GlobalStateExplorers/DatabaseBrowser/FLEXTableListViewController.m

@@ -11,6 +11,7 @@
 #import "FLEXSQLiteDatabaseManager.h"
 #import "FLEXRealmDatabaseManager.h"
 #import "FLEXTableContentViewController.h"
+#import "FLEXMutableListSection.h"
 #import "NSArray+Functional.h"
 #import "FLEXAlert.h"
 
@@ -18,8 +19,7 @@
 @property (nonatomic, readonly) id<FLEXDatabaseManager> dbm;
 @property (nonatomic, readonly) NSString *path;
 
-@property (nonatomic) NSArray<NSString *> *tables;
-@property (nonatomic) NSArray<NSString *> *filteredTables;
+@property (nonatomic, readonly) FLEXMutableListSection<NSString *> *tables;
 
 + (NSArray<NSString *> *)supportedSQLiteExtensions;
 + (NSArray<NSString *> *)supportedRealmExtensions;
@@ -42,7 +42,6 @@
     [super viewDidLoad];
 
     self.showsSearchBar = YES;
-    [self getAllTables];
     
     // Compose query button //
 
@@ -58,6 +57,35 @@
     
     [self addToolbarItems:@[composeQuery]];
 }
+
+- (NSArray<FLEXTableViewSection *> *)makeSections {
+    _tables = [FLEXMutableListSection list:[self.dbm queryAllTables]
+        cellConfiguration:^(__kindof UITableViewCell *cell, NSString *tableName, NSInteger row) {
+            cell.textLabel.text = tableName;
+        } filterMatcher:^BOOL(NSString *filterText, NSString *tableName) {
+            return [tableName localizedCaseInsensitiveContainsString:filterText];
+        }
+    ];
+    
+    self.tables.selectionHandler = ^(FLEXTableListViewController *host, NSString *tableName) {
+        NSArray *rows = [host.dbm queryAllDataInTable:tableName];
+        NSArray *columns = [host.dbm queryAllColumnsOfTable:tableName];
+        
+        UIViewController *resultsScreen = [FLEXTableContentViewController columns:columns rows:rows];
+        resultsScreen.title = tableName;
+        [host.navigationController pushViewController:resultsScreen animated:YES];
+    };
+    
+    return @[self.tables];
+}
+
+- (void)reloadData {
+    self.tables.customTitle = [NSString
+        stringWithFormat:@"Tables (%@)", @(self.tables.filteredList.count)
+    ];
+    
+    [super reloadData];
+}
     
 - (void)queryButtonPressed {
     FLEXSQLiteDatabaseManager *database = self.dbm;
@@ -98,54 +126,6 @@
     return nil;
 }
 
-- (void)getAllTables {
-    self.tables = self.filteredTables = [self.dbm queryAllTables];
-}
-
-
-#pragma mark - Search bar
-
-- (void)updateSearchResults:(NSString *)searchText {
-    if (searchText.length) {
-        self.filteredTables = [self.tables flex_filtered:^BOOL(NSString *tableName, NSUInteger idx) {
-            return [tableName containsString:searchText];
-        }];
-    } else {
-        self.filteredTables = self.tables;
-    }
-    
-    [self.tableView reloadData];
-}
-
-
-#pragma mark - Table View Data Source
-
-- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
-    return self.filteredTables.count;
-}
-
-- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
-    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"FLEXTableListViewControllerCell"];
-    if (!cell) {
-        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
-                                      reuseIdentifier:@"FLEXTableListViewControllerCell"];
-    }
-    cell.textLabel.text = self.filteredTables[indexPath.row];
-    return cell;
-}
-
-- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
-    NSArray *rows = [self.dbm queryAllDataInTable:self.filteredTables[indexPath.row]];
-    NSArray *columns = [self.dbm queryAllColumnsOfTable:self.filteredTables[indexPath.row]];
-    
-    UIViewController *resultsScreen = [FLEXTableContentViewController columns:columns rows:rows];
-    resultsScreen.title = self.filteredTables[indexPath.row];
-    [self.navigationController pushViewController:resultsScreen animated:YES];
-}
-
-- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
-    return [NSString stringWithFormat:@"Tables (%lu)", (unsigned long)self.filteredTables.count];
-}
 
 #pragma mark - FLEXTableListViewController
 

+ 2 - 2
Classes/GlobalStateExplorers/FLEXCookiesTableViewController.h

@@ -7,8 +7,8 @@
 //
 
 #import "FLEXGlobalsEntry.h"
-#import "FLEXTableViewController.h"
+#import "FLEXFilteringTableViewController.h"
 
-@interface FLEXCookiesTableViewController : FLEXTableViewController <FLEXGlobalsEntry>
+@interface FLEXCookiesTableViewController : FLEXFilteringTableViewController <FLEXGlobalsEntry>
 
 @end

+ 34 - 51
Classes/GlobalStateExplorers/FLEXCookiesTableViewController.m

@@ -8,77 +8,60 @@
 
 #import "FLEXCookiesTableViewController.h"
 #import "FLEXObjectExplorerFactory.h"
+#import "FLEXMutableListSection.h"
 #import "FLEXUtility.h"
 
 @interface FLEXCookiesTableViewController ()
-@property (nonatomic, readonly) NSArray<NSHTTPCookie *> *cookies;
+@property (nonatomic, readonly) FLEXMutableListSection<NSHTTPCookie *> *cookies;
 @property (nonatomic) NSString *headerTitle;
 @end
 
 @implementation FLEXCookiesTableViewController
 
+#pragma mark - Overrides
+
 - (void)viewDidLoad {
     [super viewDidLoad];
 
+    self.title = @"Cookies";
+}
+
+- (NSArray<FLEXTableViewSection *> *)makeSections {
     NSSortDescriptor *nameSortDescriptor = [[NSSortDescriptor alloc]
         initWithKey:@"name" ascending:YES selector:@selector(caseInsensitiveCompare:)
     ];
-    _cookies = [NSHTTPCookieStorage.sharedHTTPCookieStorage.cookies
-        sortedArrayUsingDescriptors:@[nameSortDescriptor]
+    NSArray *cookies = [NSHTTPCookieStorage.sharedHTTPCookieStorage.cookies
+       sortedArrayUsingDescriptors:@[nameSortDescriptor]
     ];
-
-    self.title = @"Cookies";
-    [self updateHeaderTitle];
-}
-
-- (void)updateHeaderTitle {
-    self.headerTitle = [NSString stringWithFormat:@"%@ cookies", @(self.cookies.count)];
-    // TODO update header title here when we can search cookies
-}
-
-- (NSHTTPCookie *)cookieForRowAtIndexPath:(NSIndexPath *)indexPath {
-    return self.cookies[indexPath.row];
-}
-
-
-#pragma mark - Table View Data Source
-
-- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
-    return self.cookies.count;
-}
-
-- (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.textLabel.font = UIFont.flex_defaultTableCellFont;
-        cell.detailTextLabel.font = UIFont.flex_defaultTableCellFont;
-        cell.detailTextLabel.textColor = UIColor.grayColor;
-        cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
-    }
     
-    NSHTTPCookie *cookie = [self cookieForRowAtIndexPath:indexPath];
-    cell.textLabel.text = [NSString stringWithFormat:@"%@ (%@)", cookie.name, cookie.value];
-    cell.detailTextLabel.text = cookie.domain;
+    _cookies = [FLEXMutableListSection list:cookies
+        cellConfiguration:^(UITableViewCell *cell, NSHTTPCookie *cookie, NSInteger row) {
+            cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
+            cell.textLabel.text = [cookie.name stringByAppendingFormat:@" (%@)", cookie.value];
+            cell.detailTextLabel.text = [cookie.domain stringByAppendingFormat:@" — %@", cookie.path];
+        } filterMatcher:^BOOL(NSString *filterText, NSHTTPCookie *cookie) {
+            return [cookie.name localizedCaseInsensitiveContainsString:filterText] ||
+                [cookie.value localizedCaseInsensitiveContainsString:filterText] ||
+                [cookie.domain localizedCaseInsensitiveContainsString:filterText] ||
+                [cookie.path localizedCaseInsensitiveContainsString:filterText];
+        }
+    ];
     
-    return cell;
-}
-
-- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
-    return self.headerTitle;
-}
-
-
-#pragma mark - Table View Delegate
-
-- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
-    NSHTTPCookie *cookie = [self cookieForRowAtIndexPath:indexPath];
-    UIViewController *cookieViewController = (UIViewController *)[FLEXObjectExplorerFactory explorerViewControllerForObject:cookie];
+    self.cookies.selectionHandler = ^(UIViewController *host, NSHTTPCookie *cookie) {
+        [host.navigationController pushViewController:[
+            FLEXObjectExplorerFactory explorerViewControllerForObject:cookie
+        ] animated:YES];
+    };
     
-    [self.navigationController pushViewController:cookieViewController animated:YES];
+    return @[self.cookies];
 }
 
+- (void)reloadData {
+    self.headerTitle = [NSString stringWithFormat:
+        @"%@ cookies", @(self.cookies.filteredList.count)
+    ];
+    [super reloadData];
+}
 
 #pragma mark - FLEXGlobalsEntry
 

+ 0 - 1
Classes/GlobalStateExplorers/FLEXLiveObjectsTableViewController.m

@@ -33,7 +33,6 @@ static const NSInteger kFLEXLiveObjectsSortBySizeIndex = 2;
 - (void)viewDidLoad {
     [super viewDidLoad];
 
-//    self.title = @"Live Objects";
     self.showsSearchBar = YES;
     self.searchBarDebounceInterval = kFLEXDebounceInstant;
     self.showsCarousel = YES;

+ 2 - 2
Classes/GlobalStateExplorers/FLEXObjectListViewController.h

@@ -6,9 +6,9 @@
 //  Copyright (c) 2014 Flipboard. All rights reserved.
 //
 
-#import "FLEXTableViewController.h"
+#import "FLEXFilteringTableViewController.h"
 
-@interface FLEXObjectListViewController : FLEXTableViewController
+@interface FLEXObjectListViewController : FLEXFilteringTableViewController
 
 + (instancetype)instancesOfClassWithName:(NSString *)className;
 + (instancetype)subclassesOfClassWithName:(NSString *)className;

+ 41 - 95
Classes/GlobalStateExplorers/FLEXObjectListViewController.m

@@ -9,7 +9,7 @@
 #import "FLEXObjectListViewController.h"
 #import "FLEXObjectExplorerFactory.h"
 #import "FLEXObjectExplorerViewController.h"
-#import "FLEXCollectionContentSection.h"
+#import "FLEXMutableListSection.h"
 #import "FLEXRuntimeUtility.h"
 #import "FLEXUtility.h"
 #import "FLEXHeapEnumerator.h"
@@ -21,17 +21,17 @@
 
 
 @interface FLEXObjectListViewController ()
+@property (nonatomic, copy) NSArray<FLEXMutableListSection *> *sections;
+@property (nonatomic, copy) NSArray<FLEXMutableListSection *> *allSections;
 
-@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 *> *references;
+@property (nonatomic, readonly) NSArray<FLEXObjectRef *> *references;
+@property (nonatomic, readonly) NSArray<NSPredicate *> *predicates;
+@property (nonatomic, readonly) NSArray<NSString *> *sectionTitles;
 
 @end
 
 @implementation FLEXObjectListViewController
+@dynamic sections, allSections;
 
 #pragma mark - Reference Grouping
 
@@ -103,13 +103,9 @@
 
     self = [super initWithStyle:UITableViewStylePlain];
     if (self) {
-        self.references = references;
-
-        if (predicates.count) {
-            [self buildSections:sectionTitles predicates:predicates];
-        } else {
-            _sections = _allSections = @[[self makeSection:references title:nil]];
-        }
+        _references = references;
+        _predicates = predicates;
+        _sectionTitles = sectionTitles;
     }
 
     return self;
@@ -200,7 +196,7 @@
 }
 
 
-#pragma mark - Lifecycle
+#pragma mark - Overrides
 
 - (void)viewDidLoad {
     [super viewDidLoad];
@@ -208,100 +204,50 @@
     self.showsSearchBar = YES;
 }
 
+- (NSArray<FLEXMutableListSection *> *)makeSections {
+    if (self.predicates.count) {
+        return [self buildSections:self.sectionTitles predicates:self.predicates];
+    } else {
+        return @[[self makeSection:self.references title:nil]];
+    }
+}
+
 
 #pragma mark - Private
 
-- (void)buildSections:(NSArray<NSString *> *)titles predicates:(NSArray<NSPredicate *> *)predicates {
+- (NSArray *)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) {
+    return [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;
+- (FLEXMutableListSection *)makeSection:(NSArray *)rows title:(NSString *)title {
+    FLEXMutableListSection *section = [FLEXMutableListSection list:rows
+        cellConfiguration:^(FLEXTableViewCell *cell, FLEXObjectRef *ref, NSInteger row) {
+            cell.textLabel.text = ref.reference;
+            cell.detailTextLabel.text = ref.summary;
+            cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
+        } filterMatcher:^BOOL(NSString *filterText, FLEXObjectRef *ref) {
+            if (ref.summary && [ref.summary localizedCaseInsensitiveContainsString:filterText]) {
+                return YES;
+            }
+            
+            return [ref.reference localizedCaseInsensitiveContainsString:filterText];
         }
-        
-        return [ref.reference localizedCaseInsensitiveContainsString:filterText];
-    };
-    
-    // Use custom title, or hide title entirely
-    if (title) {
-        section.customTitle = title;
-    } else {
-        section.hideSectionTitle = YES;
-    }
-    
-    return section;
-}
-
-- (NSArray *)nonemptySections {
-    return [self.allSections flex_filtered:^BOOL(FLEXTableViewSection *section, NSUInteger idx) {
-        return section.numberOfRows > 0;
-    }];
-}
-
-- (FLEXObjectRef *)referenceForIndexPath:(NSIndexPath *)ip {
-    return [self.sections[ip.section] objectForRow:ip.row];
-}
-
-
-#pragma mark - Search
-
-- (void)updateSearchResults:(NSString *)newText; {
-    // Sections will adjust data based on this property
-    for (FLEXTableViewSection *section in self.allSections) {
-        section.filterText = newText;
-    }
-
-    // Recalculate empty sections
-    self.sections = [self nonemptySections];
-
-    // Refresh table view
-    if (self.isViewLoaded) {
-        [self.tableView reloadData];
-    }
-}
-
-
-#pragma mark - Table View Data Source
-
-- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
-    return self.sections.count;
-}
-
-- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
-    return self.sections[section].numberOfRows;
-}
-
-- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
-    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 {
-    return self.sections[section].title;
-}
-
-
-#pragma mark - Table View Delegate
+    section.selectionHandler = ^(__kindof UIViewController *host, id element) {
+        [self.navigationController pushViewController:[
+            FLEXObjectExplorerFactory explorerViewControllerForObject:element
+        ] animated:YES];
+    };
 
-- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
-    [self.navigationController pushViewController:[FLEXObjectExplorerFactory
-        explorerViewControllerForObject:[self referenceForIndexPath:indexPath].object
-    ] animated:YES];
+    section.customTitle = title;    
+    return section;
 }
 
 @end

+ 1 - 1
Classes/GlobalStateExplorers/Globals/FLEXGlobalsEntry.h

@@ -3,7 +3,7 @@
 //  FLEX
 //
 //  Created by Javier Soto on 7/26/14.
-//  Copyright (c) 2014 f. All rights reserved.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
 //
 
 #import <UIKit/UIKit.h>

+ 13 - 1
Classes/GlobalStateExplorers/Globals/FLEXGlobalsEntry.m

@@ -3,7 +3,7 @@
 //  FLEX
 //
 //  Created by Javier Soto on 7/26/14.
-//  Copyright (c) 2014 f. All rights reserved.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
 //
 
 #import "FLEXGlobalsEntry.h"
@@ -59,6 +59,18 @@
 
 @end
 
+@interface FLEXGlobalsEntry (Debugging)
+@property (nonatomic, readonly) NSString *name;
+@end
+
+@implementation FLEXGlobalsEntry (Debugging)
+
+- (NSString *)name {
+    return self.entryNameFuture();
+}
+
+@end
+
 #pragma mark - flex_concreteGlobalsEntry
 
 @implementation NSObject (FLEXGlobalsEntry)

+ 3 - 0
Classes/GlobalStateExplorers/Globals/FLEXGlobalsSection.m

@@ -8,6 +8,7 @@
 
 #import "FLEXGlobalsSection.h"
 #import "NSArray+Functional.h"
+#import "UIFont+FLEX.h"
 
 @interface FLEXGlobalsSection ()
 /// Filtered rows
@@ -68,6 +69,8 @@
 }
 
 - (void)configureCell:(__kindof UITableViewCell *)cell forRow:(NSInteger)row {
+    cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
+    cell.textLabel.font = UIFont.flex_defaultTableCellFont;
     cell.textLabel.text = self.rows[row].entryNameFuture();
 }
 

+ 2 - 2
Classes/GlobalStateExplorers/Globals/FLEXGlobalsViewController.h

@@ -6,7 +6,7 @@
 //  Copyright (c) 2014 Flipboard. All rights reserved.
 //
 
-#import "FLEXTableViewController.h"
+#import "FLEXFilteringTableViewController.h"
 @protocol FLEXGlobalsTableViewControllerDelegate;
 
 typedef NS_ENUM(NSUInteger, FLEXGlobalsSectionKind) {
@@ -23,6 +23,6 @@ typedef NS_ENUM(NSUInteger, FLEXGlobalsSectionKind) {
     FLEXGlobalsSectionCount
 };
 
-@interface FLEXGlobalsViewController : FLEXTableViewController
+@interface FLEXGlobalsViewController : FLEXFilteringTableViewController
 
 @end

+ 17 - 87
Classes/GlobalStateExplorers/Globals/FLEXGlobalsViewController.m

@@ -22,16 +22,18 @@
 #import "FLEXNetworkMITMViewController.h"
 #import "FLEXAddressExplorerCoordinator.h"
 #import "FLEXGlobalsSection.h"
+#import "UIBarButtonItem+FLEX.h"
 
 @interface FLEXGlobalsViewController ()
 /// Only displayed sections of the table view; empty sections are purged from this array.
-@property (nonatomic, copy) NSArray<FLEXGlobalsSection *> *sections;
+@property (nonatomic) NSArray<FLEXGlobalsSection *> *sections;
 /// Every section in the table view, regardless of whether or not a section is empty.
 @property (nonatomic, readonly) NSArray<FLEXGlobalsSection *> *allSections;
 @property (nonatomic, readonly) BOOL manuallyDeselectOnAppear;
 @end
 
 @implementation FLEXGlobalsViewController
+@dynamic sections, allSections;
 
 #pragma mark - Initialization
 
@@ -154,7 +156,7 @@
 }
 
 
-#pragma mark - UIViewController
+#pragma mark - Overrides
 
 - (void)viewDidLoad {
     [super viewDidLoad];
@@ -162,19 +164,7 @@
     self.title = @"💪  FLEX";
     self.showsSearchBar = YES;
     self.searchBarDebounceInterval = kFLEXDebounceInstant;
-
-    // Table view data
-    _allSections = [[self class] defaultGlobalSections];
-    if ([FLEXManager sharedManager].userGlobalEntries.count) {
-        // Make custom section
-        NSString *title = [[self class] globalsTitleForSection:FLEXGlobalsSectionCustom];
-        FLEXGlobalsSection *custom = [FLEXGlobalsSection
-            title:title
-            rows:FLEXManager.sharedManager.userGlobalEntries
-        ];
-        _allSections = [_allSections arrayByAddingObject:custom];
-    }
-    self.sections = self.allSections;
+    self.navigationItem.backBarButtonItem = [UIBarButtonItem backItemWithTitle:@"Back"];
     
     _manuallyDeselectOnAppear = NSProcessInfo.processInfo.operatingSystemVersion.majorVersion < 10;
 }
@@ -189,80 +179,20 @@
     }
 }
 
-
-#pragma mark - Search Bar
-
-- (void)updateSearchResults:(NSString *)newText {
-    // Sections will adjust data based on this property
-    for (FLEXTableViewSection *section in self.allSections) {
-        section.filterText = newText;
-    }
-
-    // Recalculate empty sections
-    self.sections = [self nonemptySections];
-
-    // Refresh table view
-    if (self.isViewLoaded) {
-        [self.tableView reloadData];
-    }
-}
-
-
-#pragma mark - Private
-
-- (NSArray<FLEXGlobalsSection *> *)nonemptySections {
-    return [self.allSections flex_filtered:^BOOL(FLEXTableViewSection *section, NSUInteger idx) {
-        return section.numberOfRows > 0;
-    }];
-}
-
-
-#pragma mark - Table View Data Source
-
-- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
-    return self.sections.count;
-}
-
-- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
-    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:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
-        cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
-        cell.textLabel.font = [UIFont systemFontOfSize:14.0];
-    }
-    
-    [self.sections[indexPath.section] configureCell:cell forRow:indexPath.row];
+- (NSArray<FLEXGlobalsSection *> *)makeSections {
+    NSArray *sections = [self.class defaultGlobalSections];
     
-    return cell;
-}
-
-- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
-    return self.sections[section].title;
-}
-
-#pragma mark - Table View Delegate
-
-
-- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
-    FLEXTableViewSection *section = self.sections[indexPath.section];
-
-    void (^action)(UIViewController *) = [section didSelectRowAction:indexPath.row];
-    UIViewController *details = [section viewControllerToPushForRow:indexPath.row];
-
-    if (action) {
-        action(self);
-        [tableView deselectRowAtIndexPath:indexPath animated:YES];
-    } else if (details) {
-        [self.navigationController pushViewController:details animated:YES];
-    } else {
-        [NSException raise:NSInternalInconsistencyException
-                    format:@"Row is selectable but has no action or view controller"];
+    // Do we have custom sections to add?
+    if (FLEXManager.sharedManager.userGlobalEntries.count) {
+        NSString *title = [[self class] globalsTitleForSection:FLEXGlobalsSectionCustom];
+        FLEXGlobalsSection *custom = [FLEXGlobalsSection
+            title:title
+            rows:FLEXManager.sharedManager.userGlobalEntries
+        ];
+        sections = [sections arrayByAddingObject:custom];
     }
+    
+    return sections;
 }
 
 @end

+ 1 - 1
Classes/GlobalStateExplorers/Keychain/FLEXKeychain.m

@@ -86,7 +86,7 @@ static CFTypeRef FLEXKeychainAccessibilityType = NULL;
 }
 
 + (NSArray *)allAccounts {
-    return [self allAccounts:nil];
+    return [self allAccounts:nil] ?: @[];
 }
 
 + (NSArray *)allAccounts:(NSError *__autoreleasing *)error {

+ 1 - 1
Classes/GlobalStateExplorers/Keychain/FLEXKeychainQuery.m

@@ -116,7 +116,7 @@
         return nil;
     }
     
-    return (__bridge_transfer NSArray *)result;
+    return (__bridge_transfer NSArray *)result ?: @[];
 }
 
 

+ 2 - 2
Classes/GlobalStateExplorers/Keychain/FLEXKeychainTableViewController.h

@@ -7,8 +7,8 @@
 //
 
 #import "FLEXGlobalsEntry.h"
-#import "FLEXTableViewController.h"
+#import "FLEXFilteringTableViewController.h"
 
-@interface FLEXKeychainTableViewController : FLEXTableViewController <FLEXGlobalsEntry>
+@interface FLEXKeychainTableViewController : FLEXFilteringTableViewController <FLEXGlobalsEntry>
 
 @end

+ 89 - 51
Classes/GlobalStateExplorers/Keychain/FLEXKeychainTableViewController.m

@@ -9,18 +9,23 @@
 #import "FLEXKeychain.h"
 #import "FLEXKeychainQuery.h"
 #import "FLEXKeychainTableViewController.h"
+#import "FLEXTableViewCell.h"
+#import "FLEXMutableListSection.h"
 #import "FLEXUtility.h"
 #import "UIPasteboard+FLEX.h"
 
 @interface FLEXKeychainTableViewController ()
-
-@property (nonatomic) NSMutableArray<NSDictionary *> *keychainItems;
-@property (nonatomic) NSString *headerTitle;
-
+@property (nonatomic, readonly) FLEXMutableListSection<NSDictionary *> *section;
 @end
 
 @implementation FLEXKeychainTableViewController
 
+- (id)init {
+    return [self initWithStyle:UITableViewStyleGrouped];
+}
+
+#pragma mark - Overrides
+
 - (void)viewDidLoad {
     [super viewDidLoad];
     
@@ -33,20 +38,67 @@
         ],
     ];
 
-    [self refreshkeychainItems];
-    [self updateHeaderTitle];
+    [self reloadData];
 }
 
-- (void)refreshkeychainItems {
-    self.keychainItems = [FLEXKeychain allAccounts].mutableCopy;
+- (NSArray<FLEXTableViewSection *> *)makeSections {
+    _section = [FLEXMutableListSection list:[FLEXKeychain allAccounts].mutableCopy
+        cellConfiguration:^(__kindof FLEXTableViewCell *cell, NSDictionary *item, NSInteger row) {
+            cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
+        
+            id account = item[kFLEXKeychainAccountKey];
+            if ([account isKindOfClass:[NSString class]]) {
+                cell.textLabel.text = account;
+            } else {
+                cell.textLabel.text = [NSString stringWithFormat:
+                    @"[%@]\n\n%@",
+                    NSStringFromClass([account class]),
+                    [account description]
+                ];
+            }
+        } filterMatcher:^BOOL(NSString *filterText, NSDictionary *item) {
+            // Loop over contents of the keychain item looking for a match
+            for (NSString *field in item.allValues) {
+                if ([field isKindOfClass:[NSString class]]) {
+                    if ([field localizedCaseInsensitiveContainsString:filterText]) {
+                        return YES;
+                    }
+                }
+            }
+            
+            return NO;
+        }
+    ];
+    
+    return @[self.section];
+}
+
+/// We always want to show this section
+- (NSArray<FLEXTableViewSection *> *)nonemptySections {
+    return @[self.section];
+}
+
+- (void)reloadSections {
+    self.section.list = [FLEXKeychain allAccounts].mutableCopy;
+}
+
+- (void)refreshSectionTitle {
+    self.section.customTitle = FLEXPluralString(
+        self.section.filteredList.count, @"items", @"item"
+    );
 }
 
-- (void)updateHeaderTitle {
-    self.headerTitle = [NSString stringWithFormat:@"%@ items", @(self.keychainItems.count)];
+- (void)reloadData {
+    [self reloadSections];
+    [self refreshSectionTitle];
+    [super reloadData];
 }
 
+
+#pragma mark - Private
+
 - (FLEXKeychainQuery *)queryForItemAtIndex:(NSInteger)idx {
-    NSDictionary *item = self.keychainItems[idx];
+    NSDictionary *item = self.section.filteredList[idx];
 
     FLEXKeychainQuery *query = [FLEXKeychainQuery new];
     query.service = item[kFLEXKeychainWhereKey];
@@ -81,12 +133,11 @@
         make.message(@"This will remove all keychain items for this app.\n");
         make.message(@"This action cannot be undone. Are you sure?");
         make.button(@"Yes, clear the keychain").destructiveStyle().handler(^(NSArray *strings) {
-            for (id account in self.keychainItems) {
+            for (id account in self.section.list) {
                 [self deleteItem:account];
             }
 
-            [self refreshkeychainItems];
-            [self.tableView reloadData];
+            [self reloadData];
         });
         make.button(@"Cancel").cancelStyle();
     } showFrom:self];
@@ -96,7 +147,7 @@
     [FLEXAlert makeAlert:^(FLEXAlert *make) {
         make.title(@"Add Keychain Item");
         make.textField(@"Service name, i.e. Instagram");
-        make.textField(@"Account, i.e. username@example.com");
+        make.textField(@"Account");
         make.textField(@"Password");
         make.button(@"Cancel").cancelStyle();
         make.button(@"Save").handler(^(NSArray<NSString *> *strings) {
@@ -106,8 +157,7 @@
                 [FLEXAlert showAlert:@"Error" message:error.localizedDescription from:self];
             }
 
-            [self refreshkeychainItems];
-            [self.tableView reloadData];
+            [self reloadData];
         });
     } showFrom:self];
 }
@@ -129,49 +179,37 @@
 
 #pragma mark - Table View Data Source
 
-- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
-    return self.keychainItems.count;
-}
-
-- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
-    static NSString *CellIdentifier = @"Cell";
-    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
-    if (!cell) {
-        cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
-        cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
-        cell.textLabel.font = UIFont.flex_defaultTableCellFont;
-    }
-    
-    NSDictionary *item = self.keychainItems[indexPath.row];
-    id account = item[kFLEXKeychainAccountKey];
-    if ([account isKindOfClass:[NSString class]]) {
-        cell.textLabel.text = account;
-    } else {
-        cell.textLabel.text = [NSString stringWithFormat:
-            @"[%@]\n\n%@",
-            NSStringFromClass([account class]),
-            [account description]
-        ];
-    }
-    
-    return cell;
-}
-
-- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
-    return self.headerTitle;
-}
-
 - (void)tableView:(UITableView *)tv commitEditingStyle:(UITableViewCellEditingStyle)style forRowAtIndexPath:(NSIndexPath *)ip {
     if (style == UITableViewCellEditingStyleDelete) {
-        [self deleteItem:self.keychainItems[ip.row]];
-        [self.keychainItems removeObjectAtIndex:ip.row];
+        // Update the model
+        NSDictionary *toRemove = self.section.filteredList[ip.row];
+        [self deleteItem:toRemove];
+        [self.section mutate:^(NSMutableArray *list) {
+            [list removeObject:toRemove];
+        }];
+    
+        // Delete the row
         [tv deleteRowsAtIndexPaths:@[ip] withRowAnimation:UITableViewRowAnimationAutomatic];
+        
+        // Update the title by refreshing the section without disturbing the delete animation
+        //
+        // This is an ugly hack, but literally nothing else works, save for manually getting
+        // the header and setting its title, which I personally think is worse since it
+        // would need to make assumptions about the default style of the header (CAPS)
+        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
+            [self refreshSectionTitle];
+            [tv reloadSections:[NSIndexSet indexSetWithIndex:0] withRowAnimation:UITableViewRowAnimationNone];
+        });
     }
 }
 
 
 #pragma mark - Table View Delegate
 
+- (BOOL)tableView:(UITableView *)tableView shouldHighlightRowAtIndexPath:(NSIndexPath *)indexPath {
+    return YES;
+}
+
 - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
     FLEXKeychainQuery *query = [self queryForItemAtIndex:indexPath.row];
     

+ 3 - 3
Classes/GlobalStateExplorers/RuntimeBrowser/FLEXObjcRuntimeViewController.m

@@ -30,8 +30,6 @@
 
 - (void)viewDidLoad {
     [super viewDidLoad];
-
-    self.title = @"📚  Runtime Browser";
     
     // Search bar stuff, must be first because this creates self.searchController
     self.showsSearchBar = YES;
@@ -107,7 +105,9 @@
 }
 
 + (UIViewController *)globalsEntryViewController:(FLEXGlobalsRow)row {
-    return [self new];
+    UIViewController *controller = [self new];
+    controller.title = [self globalsEntryTitle:row];
+    return controller;
 }
 
 @end

+ 2 - 2
Classes/GlobalStateExplorers/SystemLog/FLEXSystemLogCell.h

@@ -6,13 +6,13 @@
 //  Copyright (c) 2015 f. All rights reserved.
 //
 
-#import <UIKit/UIKit.h>
+#import "FLEXTableViewCell.h"
 
 @class FLEXSystemLogMessage;
 
 extern NSString *const kFLEXSystemLogCellIdentifier;
 
-@interface FLEXSystemLogCell : UITableViewCell
+@interface FLEXSystemLogCell : FLEXTableViewCell
 
 @property (nonatomic) FLEXSystemLogMessage *logMessage;
 @property (nonatomic, copy) NSString *highlightedText;

+ 8 - 10
Classes/GlobalStateExplorers/SystemLog/FLEXSystemLogCell.m

@@ -21,16 +21,14 @@ NSString *const kFLEXSystemLogCellIdentifier = @"FLEXSystemLogCellIdentifier";
 
 @implementation FLEXSystemLogCell
 
-- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
-    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
-    if (self) {
-        self.logMessageLabel = [UILabel new];
-        self.logMessageLabel.numberOfLines = 0;
-        self.separatorInset = UIEdgeInsetsZero;
-        self.selectionStyle = UITableViewCellSelectionStyleNone;
-        [self.contentView addSubview:self.logMessageLabel];
-    }
-    return self;
+- (void)postInit {
+    [super postInit];
+    
+    self.logMessageLabel = [UILabel new];
+    self.logMessageLabel.numberOfLines = 0;
+    self.separatorInset = UIEdgeInsetsZero;
+    self.selectionStyle = UITableViewCellSelectionStyleNone;
+    [self.contentView addSubview:self.logMessageLabel];
 }
 
 - (void)setLogMessage:(FLEXSystemLogMessage *)logMessage {

+ 2 - 2
Classes/GlobalStateExplorers/SystemLog/FLEXSystemLogViewController.h

@@ -6,9 +6,9 @@
 //  Copyright (c) 2015 f. All rights reserved.
 //
 
-#import "FLEXTableViewController.h"
+#import "FLEXFilteringTableViewController.h"
 #import "FLEXGlobalsEntry.h"
 
-@interface FLEXSystemLogViewController : FLEXTableViewController <FLEXGlobalsEntry>
+@interface FLEXSystemLogViewController : FLEXFilteringTableViewController <FLEXGlobalsEntry>
 
 @end

+ 51 - 53
Classes/GlobalStateExplorers/SystemLog/FLEXSystemLogViewController.m

@@ -10,6 +10,7 @@
 #import "FLEXASLLogController.h"
 #import "FLEXOSLogController.h"
 #import "FLEXSystemLogCell.h"
+#import "FLEXMutableListSection.h"
 #import "FLEXUtility.h"
 #import "FLEXColor.h"
 #import "FLEXResources.h"
@@ -19,9 +20,8 @@
 
 @interface FLEXSystemLogViewController ()
 
+@property (nonatomic, readonly) FLEXMutableListSection<FLEXSystemLogMessage *> *logMessages;
 @property (nonatomic, readonly) id<FLEXLogController> logController;
-@property (nonatomic, readonly) NSMutableArray<FLEXSystemLogMessage *> *logMessages;
-@property (nonatomic, copy) NSArray<FLEXSystemLogMessage *> *filteredLogMessages;
 
 @end
 
@@ -38,6 +38,8 @@ BOOL my_os_log_shim_enabled() {
 
 @implementation FLEXSystemLogViewController
 
+#pragma mark - Initialization
+
 + (void)load {
     // Thanks to @Ram4096 on GitHub for telling me that
     // os_log is conditionally enabled by the SDK version
@@ -83,6 +85,9 @@ BOOL my_os_log_shim_enabled() {
     return [super initWithStyle:UITableViewStylePlain];
 }
 
+
+#pragma mark - Overrides
+
 - (void)viewDidLoad {
     [super viewDidLoad];
     
@@ -93,8 +98,7 @@ BOOL my_os_log_shim_enabled() {
         __strong __typeof(weakSelf) self = weakSelf;
         [self handleUpdateWithNewMessages:newMessages];
     };
-
-    _logMessages = [NSMutableArray array];
+    
     if (FLEXOSLogAvailable() && !FLEXNSLogHookWorks) {
         _logController = [FLEXOSLogController withUpdateHandler:logHandler];
     } else {
@@ -125,10 +129,48 @@ BOOL my_os_log_shim_enabled() {
     }
 }
 
+- (void)viewWillAppear:(BOOL)animated {
+    [super viewWillAppear:animated];
+
+    [self.logController startMonitoring];
+}
+
+- (NSArray<FLEXTableViewSection *> *)makeSections {
+    _logMessages = [FLEXMutableListSection list:@[]
+        cellConfiguration:^(FLEXSystemLogCell *cell, FLEXSystemLogMessage *message, NSInteger row) {
+            cell.logMessage = message;
+            cell.highlightedText = self.filterText;
+            
+            if (row % 2 == 0) {
+                cell.backgroundColor = FLEXColor.primaryBackgroundColor;
+            } else {
+                cell.backgroundColor = FLEXColor.secondaryBackgroundColor;
+            }
+        } filterMatcher:^BOOL(NSString *filterText, FLEXSystemLogMessage *message) {
+            NSString *displayedText = [FLEXSystemLogCell displayedTextForLogMessage:message];
+            return [displayedText localizedCaseInsensitiveContainsString:filterText];
+        }
+    ];
+    
+    self.logMessages.cellRegistrationMapping = @{
+        kFLEXSystemLogCellIdentifier : [FLEXSystemLogCell class]
+    };
+    
+    return @[self.logMessages];
+}
+
+- (NSArray<FLEXTableViewSection *> *)nonemptySections {
+    return @[self.logMessages];
+}
+
+
+#pragma mark - Private
 - (void)handleUpdateWithNewMessages:(NSArray<FLEXSystemLogMessage *> *)newMessages {
     self.title = @"System Log";
 
-    [self.logMessages addObjectsFromArray:newMessages];
+    [self.logMessages mutate:^(NSMutableArray *list) {
+        [list addObjectsFromArray:newMessages];
+    }];
 
     // "Follow" the log as new messages stream in if we were previously near the bottom.
     BOOL wasNearBottom = self.tableView.contentOffset.y >= self.tableView.contentSize.height - self.tableView.frame.size.height - 100.0;
@@ -138,11 +180,6 @@ BOOL my_os_log_shim_enabled() {
     }
 }
 
-- (void)viewWillAppear:(BOOL)animated {
-    [super viewWillAppear:animated];
-
-    [self.logController startMonitoring];
-}
 
 - (void)scrollToLastRow {
     NSInteger numberOfRows = [self.tableView numberOfRowsInSection:0];
@@ -163,9 +200,9 @@ BOOL my_os_log_shim_enabled() {
 
     [FLEXAlert makeAlert:^(FLEXAlert *make) {
         make.title(title).message(body).button(toggle).handler(^(NSArray<NSString *> *strings) {
-            [[NSUserDefaults standardUserDefaults] setBool:!persistent forKey:kFLEXiOSPersistentOSLogKey];
+            [NSUserDefaults.standardUserDefaults setBool:!persistent forKey:kFLEXiOSPersistentOSLogKey];
             logController.persistent = !persistent;
-            [logController.messages addObjectsFromArray:self.logMessages];
+            [logController.messages addObjectsFromArray:self.logMessages.list];
         });
         make.button(@"Dismiss").cancelStyle();
     } showFrom:self];
@@ -185,26 +222,8 @@ BOOL my_os_log_shim_enabled() {
 
 #pragma mark - Table view data source
 
-- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
-    return self.searchController.isActive ? self.filteredLogMessages.count : self.logMessages.count;
-}
-
-- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath  {
-    FLEXSystemLogCell *cell = [tableView dequeueReusableCellWithIdentifier:kFLEXSystemLogCellIdentifier forIndexPath:indexPath];
-    cell.logMessage = [self logMessageAtIndexPath:indexPath];
-    cell.highlightedText = self.searchText;
-    
-    if (indexPath.row % 2 == 0) {
-        cell.backgroundColor = [FLEXColor primaryBackgroundColor];
-    } else {
-        cell.backgroundColor = [FLEXColor secondaryBackgroundColor];
-    }
-    
-    return cell;
-}
-
 - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
-    FLEXSystemLogMessage *logMessage = [self logMessageAtIndexPath:indexPath];
+    FLEXSystemLogMessage *logMessage = self.logMessages.filteredList[indexPath.row];
     return [FLEXSystemLogCell preferredHeightForLogMessage:logMessage inWidth:self.tableView.bounds.size.width];
 }
 
@@ -222,29 +241,8 @@ BOOL my_os_log_shim_enabled() {
 - (void)tableView:(UITableView *)tableView performAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender {
     if (action == @selector(copy:)) {
         // We usually only want to copy the log message itself, not any metadata associated with it.
-        UIPasteboard.generalPasteboard.string = [self logMessageAtIndexPath:indexPath].messageText;
+        UIPasteboard.generalPasteboard.string = self.logMessages.filteredList[indexPath.row].messageText;
     }
 }
 
-- (FLEXSystemLogMessage *)logMessageAtIndexPath:(NSIndexPath *)indexPath {
-    return self.searchController.isActive ? self.filteredLogMessages[indexPath.row] : self.logMessages[indexPath.row];
-}
-
-
-#pragma mark - Search bar
-
-- (void)updateSearchResults:(NSString *)searchString {
-    [self onBackgroundQueue:^NSArray *{
-        return [self.logMessages filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(FLEXSystemLogMessage *logMessage, NSDictionary<NSString *, id> *bindings) {
-            NSString *displayedText = [FLEXSystemLogCell displayedTextForLogMessage:logMessage];
-            return [displayedText rangeOfString:searchString options:NSCaseInsensitiveSearch].length > 0;
-        }]];
-    } thenOnMainQueue:^(NSArray *filteredLogMessages) {
-        if ([self.searchText isEqual:searchString]) {
-            self.filteredLogMessages = filteredLogMessages;
-            [self.tableView reloadData];
-        }
-    }];
-}
-
 @end

+ 2 - 2
Classes/ObjectExplorers/FLEXObjectExplorerViewController.h

@@ -10,7 +10,7 @@
 #define _FLEXObjectExplorerViewController_h
 #endif
 
-#import "FLEXTableViewController.h"
+#import "FLEXFilteringTableViewController.h"
 #import "FLEXObjectExplorer.h"
 @class FLEXTableViewSection;
 
@@ -23,7 +23,7 @@ NS_ASSUME_NONNULL_BEGIN
 /// Below the description and before properties, some shortcuts will be displayed
 /// for certain classes like UIViews. At very bottom, there is an option to view
 /// a list of other objects found to be referencing the object being explored.
-@interface FLEXObjectExplorerViewController : FLEXTableViewController
+@interface FLEXObjectExplorerViewController : FLEXFilteringTableViewController
 
 /// Uses the default \c FLEXShortcutsSection for this object as a custom section.
 + (instancetype)exploringObject:(id)objectOrClass;

+ 28 - 181
Classes/ObjectExplorers/FLEXObjectExplorerViewController.m

@@ -27,12 +27,6 @@
 
 #pragma mark - Private properties
 @interface FLEXObjectExplorerViewController () <UIGestureRecognizerDelegate>
-
-@property (nonatomic, copy) NSString *filterText;
-/// Every section in the table view, regardless of whether or not a section is empty.
-@property (nonatomic, readonly) NSArray<FLEXTableViewSection *> *allSections;
-/// Only displayed sections of the table view; empty sections are purged from this array.
-@property (nonatomic) NSArray<FLEXTableViewSection *> *sections;
 @property (nonatomic, readonly) FLEXSingleRowSection *descriptionSection;
 @property (nonatomic, readonly) FLEXTableViewSection *customSection;
 @property (nonatomic) NSIndexSet *customSectionVisibleIndexes;
@@ -65,7 +59,6 @@
         _object = target;
         _explorer = explorer;
         _customSection = customSection;
-        _allSections = [self makeSections];
     }
 
     return self;
@@ -73,21 +66,11 @@
 
 #pragma mark - View controller lifecycle
 
-- (void)loadView {
-    [super loadView];
-
-    // Register cell classes
-    for (FLEXTableViewSection *section in self.allSections) {
-        if (section.cellRegistrationMapping) {
-            [self.tableView registerCells:section.cellRegistrationMapping];
-        }
-    }
-}
-
 - (void)viewDidLoad {
     [super viewDidLoad];
 
     self.showsShareToolbarItem = YES;
+    self.wantsSectionIndexTitles = YES;
 
     // Use [object class] here rather than object_getClass
     // to avoid the KVO prefix for observed objects
@@ -120,7 +103,18 @@
 }
 
 
-#pragma mark - Private
+#pragma mark - Overrides
+
+/// Override to hide the description section when searching
+- (NSArray<FLEXTableViewSection *> *)nonemptySections {
+    if (self.shouldShowDescription) {
+        return super.nonemptySections;
+    }
+    
+    return [super.nonemptySections flex_filtered:^BOOL(FLEXTableViewSection *section, NSUInteger idx) {
+        return section != self.descriptionSection;
+    }];
+}
 
 - (NSArray<FLEXTableViewSection *> *)makeSections {
     FLEXObjectExplorer *explorer = self.explorer;
@@ -174,19 +168,18 @@
     return sections.copy;
 }
 
-- (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;
-    }];
+- (void)reloadData {
+    // Check to see if class scope changed, update accordingly
+    if (self.explorer.classScope != self.selectedScope) {
+        self.explorer.classScope = self.selectedScope;
+        [self reloadSections];
+    }
+    
+    [super reloadData];
 }
 
-- (BOOL)sectionHasActions:(NSInteger)section {
-    return self.sections[section] == self.descriptionSection;
-}
+
+#pragma mark - Private
 
 - (void)handleSwipeGesture:(UISwipeGestureRecognizer *)gesture {
     if (gesture.state == UIGestureRecognizerStateEnded) {
@@ -240,12 +233,13 @@
             UIPasteboard.generalPasteboard.string = self.explorer.objectDescription;
         });
         make.button(@"Copy Address").handler(^(NSArray<NSString *> *strings) {
-            [self copyObjectAddress:nil];
+            UIPasteboard.generalPasteboard.string = [FLEXUtility addressOfObject:self.object];
         });
         make.button(@"Cancel").cancelStyle();
     } showFrom:self];
 }
 
+
 #pragma mark - Description
 
 - (BOOL)shouldShowDescription {
@@ -259,71 +253,10 @@
     return YES;
 }
 
-
-#pragma mark - Search
-
-- (void)updateSearchResults:(NSString *)newText; {
-    self.filterText = newText;
-
-    // Sections will adjust data based on this property
-    for (FLEXTableViewSection *section in self.allSections) {
-        section.filterText = newText;
-    }
-
-    // Check to see if class scope changed, update accordingly
-    if (self.explorer.classScope != self.selectedScope) {
-        self.explorer.classScope = self.selectedScope;
-        [self reloadSections];
-    }
-
-    [self reloadData];
-}
-
-
-#pragma mark - Reloading
-
-- (void)reloadData {
-    // Recalculate displayed sections
-    self.sections = [self nonemptySections];
-
-    // Refresh table view
-    if (self.isViewLoaded) {
-        [self.tableView reloadData];
-    }
-}
-
-- (void)reloadSections {
-    for (FLEXTableViewSection *section in self.allSections) {
-        [section reloadData];
-    }
-}
-
-
-#pragma mark - UITableViewDataSource
-
-- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
-    return self.sections.count;
-}
-
-- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
-    return self.sections[section].numberOfRows;
-}
-
-- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
-    return self.sections[section].title;
-}
-
-- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
-    NSString *reuse = [self.sections[indexPath.section] reuseIdentifierForRow:indexPath.row];
-    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:reuse forIndexPath:indexPath];
-    [self.sections[indexPath.section] configureCell:cell forRow:indexPath.row];
-    return cell;
-}
-
 - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
     // For the description section, we want that nice slim/snug looking row.
     // Other rows use the automatic size.
-    FLEXTableViewSection *section = self.sections[indexPath.section];
+    FLEXTableViewSection *section = self.filterDelegate.sections[indexPath.section];
     
     if (section == self.descriptionSection) {
         NSAttributedString *attributedText = [[NSAttributedString alloc]
@@ -342,43 +275,13 @@
     return UITableViewAutomaticDimension;
 }
 
-- (NSArray<NSString *> *)sectionIndexTitlesForTableView:(UITableView *)tableView {
-    return [self.sections flex_mapped:^id(FLEXTableViewSection *obj, NSUInteger idx) {
-        return @"⦁";
-    }];
-}
-
-
-#pragma mark - UITableViewDelegate
-
-- (BOOL)tableView:(UITableView *)tableView shouldHighlightRowAtIndexPath:(NSIndexPath *)indexPath {
-    return [self.sections[indexPath.section] canSelectRow:indexPath.row];
-}
-
-- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
-    FLEXTableViewSection *section = self.sections[indexPath.section];
-
-    void (^action)(UIViewController *) = [section didSelectRowAction:indexPath.row];
-    UIViewController *details = [section viewControllerToPushForRow:indexPath.row];
-
-    if (action) {
-        action(self);
-        [tableView deselectRowAtIndexPath:indexPath animated:YES];
-    } else if (details) {
-        [self.navigationController pushViewController:details animated:YES];
-    } else {
-        [NSException raise:NSInternalInconsistencyException
-                    format:@"Row is selectable but has no action or view controller"];
-    }
-}
-
 - (BOOL)tableView:(UITableView *)tableView shouldShowMenuForRowAtIndexPath:(NSIndexPath *)indexPath {
-    return [self sectionHasActions:indexPath.section];
+    return self.filterDelegate.sections[indexPath.section] == self.descriptionSection;
 }
 
 - (BOOL)tableView:(UITableView *)tableView canPerformAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender {
     // Only the description section has "actions"
-    if (self.sections[indexPath.section] == self.descriptionSection) {
+    if (self.filterDelegate.sections[indexPath.section] == self.descriptionSection) {
         return action == @selector(copy:);
     }
 
@@ -391,60 +294,4 @@
     }
 }
 
-- (void)tableView:(UITableView *)tableView accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)indexPath {
-    [self.sections[indexPath.section] didPressInfoButtonAction:indexPath.row](self);
-}
-
-#if FLEX_AT_LEAST_IOS13_SDK
-
-- (UIContextMenuConfiguration *)tableView:(UITableView *)tableView contextMenuConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath point:(CGPoint)point __IOS_AVAILABLE(13.0) {
-    FLEXTableViewSection *section = self.sections[indexPath.section];
-    NSString *title = [section menuTitleForRow:indexPath.row];
-    NSArray<UIMenuElement *> *menuItems = [section menuItemsForRow:indexPath.row sender:self];
-    
-    if (menuItems.count) {
-        return [UIContextMenuConfiguration
-            configurationWithIdentifier:nil
-            previewProvider:nil
-            actionProvider:^UIMenu *(NSArray<UIMenuElement *> *suggestedActions) {
-                return [UIMenu menuWithTitle:title children:menuItems];
-            }
-        ];
-    }
-    
-    return nil;
-}
-
-#endif
-
-
-#pragma mark - UIMenuController
-
-/// Prevent the search bar from trying to use us as a responder
-///
-/// Our table cells will use the UITableViewDelegate methods
-/// to make sure we can perform the actions we want to
-- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
-    return NO;
-}
-
-- (void)copy:(NSIndexPath *)indexPath {
-    FLEXTableViewSection *section = self.sections[indexPath.section];
-    UIPasteboard.generalPasteboard.string = ({
-        NSString *copy = [section titleForRow:indexPath.row];
-        NSString *subtitle = [section subtitleForRow:indexPath.row];
-
-        if (subtitle.length) {
-            copy = [NSString stringWithFormat:@"%@\n\n%@", copy, subtitle];
-        }
-
-        // If no string was provided, don't overwrite the pasteboard
-        copy.length > 2 ? copy : UIPasteboard.generalPasteboard.string;
-    });
-}
-
-- (void)copyObjectAddress:(NSIndexPath *)indexPath {
-    UIPasteboard.generalPasteboard.string = [FLEXUtility addressOfObject:self.object];
-}
-
 @end

+ 11 - 3
Classes/ObjectExplorers/Sections/FLEXCollectionContentSection.h

@@ -20,8 +20,8 @@ typedef id<FLEXCollection>(^FLEXCollectionContentFuture)(__kindof FLEXCollection
 
 @property (nonatomic, readonly) NSUInteger count;
 
-- (id<FLEXCollection>)copy;
-- (id<FLEXMutableCollection>)mutableCopy;
+- (id)copy;
+- (id)mutableCopy;
 
 @optional
 
@@ -58,7 +58,15 @@ typedef id<FLEXCollection>(^FLEXCollectionContentFuture)(__kindof FLEXCollection
 /// A custom section for viewing collection elements.
 ///
 /// Tapping on a row pushes an object explorer for that element.
-@interface FLEXCollectionContentSection<__covariant ObjectType> : FLEXTableViewSection <FLEXObjectInfoSection>
+@interface FLEXCollectionContentSection<__covariant ObjectType> : FLEXTableViewSection <FLEXObjectInfoSection> {
+    @protected
+    /// Unused if initialized with a future
+    id<FLEXCollection> _collection;
+    /// Unused if initialized with a collection
+    FLEXCollectionContentFuture _collectionFuture;
+    /// The filtered collection from \c _collection or \c _collectionFuture
+    id<FLEXCollection> _cachedCollection;
+}
 
 + (instancetype)forCollection:(id<FLEXCollection>)collection;
 /// The future given should be safe to call more than once.

+ 2 - 1
Classes/ObjectExplorers/Sections/FLEXCollectionContentSection.m

@@ -132,6 +132,7 @@ typedef NS_ENUM(NSUInteger, FLEXCollectionType) {
     }
 }
 
+
 #pragma mark - Overrides
 
 - (NSString *)title {
@@ -166,7 +167,7 @@ typedef NS_ENUM(NSUInteger, FLEXCollectionType) {
         [tmp filterUsingPredicate:filter];
         self.cachedCollection = tmp;
     } else {
-        self.cachedCollection = self.collection;
+        self.cachedCollection = self.collection ?: self.collectionFuture(self);
     }
 }
 

+ 58 - 0
Classes/ObjectExplorers/Sections/FLEXMutableListSection.h

@@ -0,0 +1,58 @@
+//
+//  FLEXMutableListSection.h
+//  FLEX
+//
+//  Created by Tanner on 3/9/20.
+//  Copyright © 2020 Flipboard. All rights reserved.
+//
+
+#import "FLEXCollectionContentSection.h"
+
+typedef void (^FLEXMutableListCellForElement)(__kindof UITableViewCell *cell, id element, NSInteger row);
+
+/// A section aimed at meeting the needs of table views with one section
+/// (or, a section that shouldn't warrant the code duplication that comes
+/// with creating a new section just for some specific table view)
+///
+/// Use this section if you want to display a growing list of rows,
+/// or even if you want to display a static list of rows.
+///
+/// To support editing or inserting, implement the appropriate
+/// table view delegate methods in your table view delegate class
+/// and call \c mutate: (or \c setList: ) before updating the table view.
+///
+/// By default, no section title is shown. Assign one to \c customTitle
+///
+/// By default, \c kFLEXDetailCell is the reuse identifier used. If you need
+/// to support multiple reuse identifiers in a single section, implement the
+/// \c cellForRowAtIndexPath: method, dequeue the cell yourself and call
+/// \c -configureCell: on the appropriate section object, passing in the cell
+@interface FLEXMutableListSection<__covariant ObjectType> : FLEXCollectionContentSection
+
+/// Initializes a section with an empty list.
++ (instancetype)list:(NSArray<ObjectType> *)list
+   cellConfiguration:(FLEXMutableListCellForElement)configurationBlock
+       filterMatcher:(BOOL(^)(NSString *filterText, id element))filterBlock;
+
+/// By default, rows are not selectable. If you want rows
+/// to be selectable, provide a selection handler here.
+@property (nonatomic) void (^selectionHandler)(__kindof UIViewController * host, id element);
+
+/// The objects representing all possible rows in the section.
+@property (nonatomic) NSArray<ObjectType> *list;
+/// The objects representing the currently unfiltered rows in the section.
+@property (nonatomic, readonly) NSArray<ObjectType> *filteredList;
+
+/// A readwrite version of the same property in \c FLEXTableViewSection.h
+///
+/// This property expects one entry. An exception is thrown if more than one
+/// entry is supplied. If you need more than one reuse identifier within a single
+/// section, your view probably has more complexity than this class can handle.
+@property (nonatomic, readwrite) NSDictionary<NSString *, Class> *cellRegistrationMapping;
+
+/// Call this method to mutate the full, unfiltered list.
+/// This ensures that \c filteredList is updated after any mutations.
+- (void)mutate:(void(^)(NSMutableArray *list))block;
+
+@end
+

+ 107 - 0
Classes/ObjectExplorers/Sections/FLEXMutableListSection.m

@@ -0,0 +1,107 @@
+//
+//  FLEXMutableListSection.m
+//  FLEX
+//
+//  Created by Tanner on 3/9/20.
+//  Copyright © 2020 Flipboard. All rights reserved.
+//
+
+#import "FLEXMutableListSection.h"
+
+@interface FLEXMutableListSection ()
+@property (nonatomic, readonly) FLEXMutableListCellForElement configureCell;
+@end
+
+@implementation FLEXMutableListSection
+@synthesize cellRegistrationMapping = _cellRegistrationMapping;
+
+#pragma mark - Initialization
+
++ (instancetype)list:(NSArray *)list
+   cellConfiguration:(FLEXMutableListCellForElement)cellConfig
+       filterMatcher:(BOOL(^)(NSString *, id))filterBlock {
+    return [[self alloc] initWithList:list configurationBlock:cellConfig filterMatcher:filterBlock];
+}
+
+- (id)initWithList:(NSArray *)list
+configurationBlock:(FLEXMutableListCellForElement)cellConfig
+     filterMatcher:(BOOL(^)(NSString *, id))filterBlock {
+    self = [super init];
+    if (self) {
+        _configureCell = cellConfig;
+        
+        self.list = list.mutableCopy;
+        self.customFilter = filterBlock;
+        self.hideSectionTitle = YES;
+    }
+    
+    return self;
+}
+
+
+#pragma mark - Public
+
+- (NSArray *)list {
+    return (id)_collection;
+}
+
+- (void)setList:(NSMutableArray *)list {
+    NSParameterAssert(list);
+    _collection = list;
+    
+    [self reloadData];
+}
+
+- (NSArray *)filteredList {
+    return (id)_cachedCollection;
+}
+
+- (void)mutate:(void (^)(NSMutableArray *))block {
+    block((NSMutableArray *)_collection);
+    [self reloadData];
+}
+
+
+#pragma mark - Overrides
+
+- (void)setCustomTitle:(NSString *)customTitle {
+    super.customTitle = customTitle;
+    self.hideSectionTitle = customTitle == nil;
+}
+
+- (BOOL)canSelectRow:(NSInteger)row {
+    return self.selectionHandler != nil;
+}
+
+- (UIViewController *)viewControllerToPushForRow:(NSInteger)row {
+    return nil;
+}
+
+- (void (^)(__kindof UIViewController *))didSelectRowAction:(NSInteger)row {
+    if (self.selectionHandler) {
+        return ^(UIViewController *host) {
+            self.selectionHandler(host, self.filteredList[row]);
+        };
+    }
+    
+    return nil;
+}
+
+- (void)configureCell:(__kindof UITableViewCell *)cell forRow:(NSInteger)row {
+    self.configureCell(cell, self.filteredList[row], row);
+}
+
+- (NSString *)reuseIdentifierForRow:(NSInteger)row {
+    if (self.cellRegistrationMapping.count) {
+        return self.cellRegistrationMapping.allKeys.firstObject;
+    }
+    
+    return [super reuseIdentifierForRow:row];
+}
+
+- (void)setCellRegistrationMapping:(NSDictionary<NSString *,Class> *)cellRegistrationMapping {
+    NSParameterAssert(cellRegistrationMapping.count <= 1);
+    _cellRegistrationMapping = cellRegistrationMapping;
+}
+
+@end

+ 1 - 0
Classes/Utility/Categories/UIBarButtonItem+FLEX.h

@@ -17,6 +17,7 @@
 @property (nonatomic, readonly, class) UIBarButtonItem *flex_fixedSpace;
 
 + (instancetype)itemWithCustomView:(UIView *)customView;
++ (instancetype)backItemWithTitle:(NSString *)title;
 
 + (instancetype)systemItem:(UIBarButtonSystemItem)item target:(id)target action:(SEL)action;
 

+ 4 - 0
Classes/Utility/Categories/UIBarButtonItem+FLEX.m

@@ -30,6 +30,10 @@
     return [[self alloc] initWithCustomView:customView];
 }
 
++ (instancetype)backItemWithTitle:(NSString *)title {
+    return [self itemWithTitle:title target:nil action:nil];
+}
+
 + (instancetype)itemWithTitle:(NSString *)title target:(id)target action:(SEL)action {
     return [[self alloc] initWithTitle:title style:UIBarButtonItemStylePlain target:target action:action];
 }

+ 18 - 2
FLEX.xcodeproj/project.pbxproj

@@ -301,6 +301,10 @@
 		C3DB9F642107FC9600B46809 /* FLEXObjectRef.h in Headers */ = {isa = PBXBuildFile; fileRef = C3DB9F622107FC9600B46809 /* FLEXObjectRef.h */; };
 		C3DB9F652107FC9600B46809 /* FLEXObjectRef.m in Sources */ = {isa = PBXBuildFile; fileRef = C3DB9F632107FC9600B46809 /* FLEXObjectRef.m */; };
 		C3DC287C223ED5F200F48AA6 /* FLEXOSLogController.m in Sources */ = {isa = PBXBuildFile; fileRef = C34EE30721CB23CC00BD3A7C /* FLEXOSLogController.m */; };
+		C3DFCD942416BC6500BB7084 /* FLEXFilteringTableViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = C3DFCD922416BC6500BB7084 /* FLEXFilteringTableViewController.h */; };
+		C3DFCD952416BC6500BB7084 /* FLEXFilteringTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = C3DFCD932416BC6500BB7084 /* FLEXFilteringTableViewController.m */; };
+		C3DFCD982416E7DD00BB7084 /* FLEXMutableListSection.h in Headers */ = {isa = PBXBuildFile; fileRef = C3DFCD962416E7DD00BB7084 /* FLEXMutableListSection.h */; };
+		C3DFCD992416E7DD00BB7084 /* FLEXMutableListSection.m in Sources */ = {isa = PBXBuildFile; fileRef = C3DFCD972416E7DD00BB7084 /* FLEXMutableListSection.m */; };
 		C3E5D9FD2316E83700E655DB /* FLEXRuntime+Compare.h in Headers */ = {isa = PBXBuildFile; fileRef = C3E5D9FB2316E83700E655DB /* FLEXRuntime+Compare.h */; };
 		C3E5D9FE2316E83700E655DB /* FLEXRuntime+Compare.m in Sources */ = {isa = PBXBuildFile; fileRef = C3E5D9FC2316E83700E655DB /* FLEXRuntime+Compare.m */; };
 		C3E5DA02231700EE00E655DB /* FLEXObjectInfoSection.h in Headers */ = {isa = PBXBuildFile; fileRef = C3E5DA00231700EE00E655DB /* FLEXObjectInfoSection.h */; };
@@ -642,6 +646,10 @@
 		C3BFD06F233C23ED0015FB82 /* NSArray+Functional.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSArray+Functional.m"; sourceTree = "<group>"; };
 		C3DB9F622107FC9600B46809 /* FLEXObjectRef.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FLEXObjectRef.h; sourceTree = "<group>"; };
 		C3DB9F632107FC9600B46809 /* FLEXObjectRef.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLEXObjectRef.m; sourceTree = "<group>"; };
+		C3DFCD922416BC6500BB7084 /* FLEXFilteringTableViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FLEXFilteringTableViewController.h; sourceTree = "<group>"; };
+		C3DFCD932416BC6500BB7084 /* FLEXFilteringTableViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLEXFilteringTableViewController.m; sourceTree = "<group>"; };
+		C3DFCD962416E7DD00BB7084 /* FLEXMutableListSection.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FLEXMutableListSection.h; sourceTree = "<group>"; };
+		C3DFCD972416E7DD00BB7084 /* FLEXMutableListSection.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLEXMutableListSection.m; sourceTree = "<group>"; };
 		C3E5D9FB2316E83700E655DB /* FLEXRuntime+Compare.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "FLEXRuntime+Compare.h"; sourceTree = "<group>"; };
 		C3E5D9FC2316E83700E655DB /* FLEXRuntime+Compare.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "FLEXRuntime+Compare.m"; sourceTree = "<group>"; };
 		C3E5DA00231700EE00E655DB /* FLEXObjectInfoSection.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FLEXObjectInfoSection.h; sourceTree = "<group>"; };
@@ -1109,10 +1117,12 @@
 		C36B096823E1E26C008F5D47 /* Controllers */ = {
 			isa = PBXGroup;
 			children = (
-				C38DF0E822CFE4370077B4AD /* FLEXTableViewController.h */,
-				C38DF0E922CFE4370077B4AD /* FLEXTableViewController.m */,
 				C3531BA323E88A2100A184AD /* FLEXNavigationController.h */,
 				C3531BA423E88A2100A184AD /* FLEXNavigationController.m */,
+				C38DF0E822CFE4370077B4AD /* FLEXTableViewController.h */,
+				C38DF0E922CFE4370077B4AD /* FLEXTableViewController.m */,
+				C3DFCD922416BC6500BB7084 /* FLEXFilteringTableViewController.h */,
+				C3DFCD932416BC6500BB7084 /* FLEXFilteringTableViewController.m */,
 			);
 			path = Controllers;
 			sourceTree = "<group>";
@@ -1325,6 +1335,8 @@
 				C31C4A6823342A2200C35F12 /* FLEXMetadataSection.m */,
 				C32A195C231732E800EB02AC /* FLEXCollectionContentSection.h */,
 				C32A195D231732E800EB02AC /* FLEXCollectionContentSection.m */,
+				C3DFCD962416E7DD00BB7084 /* FLEXMutableListSection.h */,
+				C3DFCD972416E7DD00BB7084 /* FLEXMutableListSection.m */,
 				C32A19602317378C00EB02AC /* FLEXDefaultsContentSection.h */,
 				C32A19612317378C00EB02AC /* FLEXDefaultsContentSection.m */,
 				C34D4EB223A2AF2A00C1F903 /* FLEXColorPreviewSection.h */,
@@ -1388,6 +1400,7 @@
 				C398626B23AD71C1007E6793 /* TBRuntime.h in Headers */,
 				3A4C95321B5B21410088C3F2 /* FLEXSystemLogCell.h in Headers */,
 				C3F977852311B38F0032776D /* NSDictionary+ObjcRuntime.h in Headers */,
+				C3DFCD982416E7DD00BB7084 /* FLEXMutableListSection.h in Headers */,
 				C398625123AD6C67007E6793 /* TBKeyPath.h in Headers */,
 				3A4C94F91B5B21410088C3F2 /* FLEXArgumentInputNumberView.h in Headers */,
 				C3F646F223A045DB00D4A011 /* FLEXClassShortcuts.h in Headers */,
@@ -1420,6 +1433,7 @@
 				3A4C950F1B5B21410088C3F2 /* FLEXMethodCallingViewController.h in Headers */,
 				3A4C94F51B5B21410088C3F2 /* FLEXArgumentInputObjectView.h in Headers */,
 				C3A9424923C78878006871A3 /* FLEXHierarchyViewController.h in Headers */,
+				C3DFCD942416BC6500BB7084 /* FLEXFilteringTableViewController.h in Headers */,
 				3A4C94F11B5B21410088C3F2 /* FLEXArgumentInputFontsPickerView.h in Headers */,
 				C3BFD070233C23ED0015FB82 /* NSArray+Functional.h in Headers */,
 				C398682923AC370100E9E391 /* FLEXViewShortcuts.h in Headers */,
@@ -1699,6 +1713,7 @@
 				C3F977842311B38F0032776D /* NSDictionary+ObjcRuntime.m in Sources */,
 				C312A13523ECBE5800E38049 /* FLEXBookmarksViewController.m in Sources */,
 				3A4C95411B5B21410088C3F2 /* FLEXNetworkTransactionTableViewCell.m in Sources */,
+				C3DFCD952416BC6500BB7084 /* FLEXFilteringTableViewController.m in Sources */,
 				C3F527BE2318603F009CBA07 /* FLEXShortcutsSection.m in Sources */,
 				3A4C94D61B5B21410088C3F2 /* FLEXObjectExplorerViewController.m in Sources */,
 				C383C3BD23B6B398007A321B /* UITextField+Range.m in Sources */,
@@ -1764,6 +1779,7 @@
 				C3490E20233BDD73002AE200 /* FLEXSingleRowSection.m in Sources */,
 				C34D4EB923A2B17900C1F903 /* FLEXBundleShortcuts.m in Sources */,
 				C3531BAB23E88FAC00A184AD /* FLEXTabList.m in Sources */,
+				C3DFCD992416E7DD00BB7084 /* FLEXMutableListSection.m in Sources */,
 				3A4C952D1B5B21410088C3F2 /* FLEXLiveObjectsTableViewController.m in Sources */,
 				C33E46B0223B02CD004BD0E6 /* FLEXASLLogController.m in Sources */,
 				C398625023AD6C67007E6793 /* FLEXObjcRuntimeViewController.m in Sources */,