Pārlūkot izejas kodu

Add basic support for tabs

Other changes:
- Editor/caller view controllers use a toolbar for the call/set button now
- FLEXNavigationController adds the Done button to it's root view controller instead of FLEXExplorerViewController
- FLEXExplorerViewController now overrides presentViewController: and dismissViewControllerAnimated: to toggle its window's key status instead of using new methods to do it
- Adds a 't' simulator shortcut to quickly present an explorer screen for testing
Tanner Bennett 6 gadi atpakaļ
vecāks
revīzija
2300d68321

+ 40 - 1
Classes/Core/Controllers/FLEXNavigationController.m

@@ -7,6 +7,8 @@
 //
 //
 
 
 #import "FLEXNavigationController.h"
 #import "FLEXNavigationController.h"
+#import "FLEXExplorerViewController.h"
+#import "FLEXTabList.h"
 
 
 @interface UINavigationController (Private) <UIGestureRecognizerDelegate>
 @interface UINavigationController (Private) <UIGestureRecognizerDelegate>
 - (void)_gestureRecognizedInteractiveHide:(UIGestureRecognizer *)sender;
 - (void)_gestureRecognizedInteractiveHide:(UIGestureRecognizer *)sender;
@@ -23,7 +25,24 @@
 @implementation FLEXNavigationController
 @implementation FLEXNavigationController
 
 
 + (instancetype)withRootViewController:(UIViewController *)rootVC {
 + (instancetype)withRootViewController:(UIViewController *)rootVC {
-    return [[self alloc] initWithRootViewController:rootVC];
+    FLEXNavigationController *instance =  [[self alloc] initWithRootViewController:rootVC];
+    
+    // Give root view controllers a Done button
+    UIBarButtonItem *done = [[UIBarButtonItem alloc]
+        initWithBarButtonSystemItem:UIBarButtonSystemItemDone
+        target:instance
+        action:@selector(dismissAnimated)
+    ];
+    
+    // Prepend the button if other buttons exist already
+    NSArray *existingItems = rootVC.navigationItem.rightBarButtonItems;
+    if (existingItems.count) {
+        rootVC.navigationItem.rightBarButtonItems = [@[done] arrayByAddingObjectsFromArray:existingItems];
+    } else {
+        rootVC.navigationItem.rightBarButtonItem = done;
+    }
+    
+    return instance;
 }
 }
 
 
 - (void)viewDidLoad {
 - (void)viewDidLoad {
@@ -32,6 +51,26 @@
     self.waitingToAddTab = YES;
     self.waitingToAddTab = YES;
 }
 }
 
 
+- (void)viewDidAppear:(BOOL)animated {
+    [super viewDidAppear:animated];
+    
+    if (self.waitingToAddTab) {
+        // Only add new tab if we're presented properly
+        if ([self.presentingViewController isKindOfClass:[FLEXExplorerViewController class]]) {
+            // New navigation controllers always add themselves as new tabs,
+            // tabs are closed by FLEXExplorerViewController
+            [FLEXTabList.sharedList addTab:self];
+            self.waitingToAddTab = NO;
+        }
+    }
+}
+
+- (void)dismissAnimated {
+    // TODO tabs not closed on swipe down gesture
+    [FLEXTabList.sharedList closeTab:self];
+    [self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
+}
+
 - (void)_gestureRecognizedInteractiveHide:(UIPanGestureRecognizer *)sender {
 - (void)_gestureRecognizedInteractiveHide:(UIPanGestureRecognizer *)sender {
     if (sender.state == UIGestureRecognizerStateRecognized) {
     if (sender.state == UIGestureRecognizerStateRecognized) {
         BOOL show = self.topViewController.toolbarItems.count;
         BOOL show = self.topViewController.toolbarItems.count;

+ 6 - 0
Classes/Core/Controllers/FLEXTableViewController.h

@@ -106,4 +106,10 @@ extern CGFloat const kFLEXDebounceForExpensiveIO;
 /// in the background before updating the UI back on the main queue.
 /// in the background before updating the UI back on the main queue.
 - (void)onBackgroundQueue:(NSArray *(^)(void))backgroundBlock thenOnMainQueue:(void(^)(NSArray *))mainBlock;
 - (void)onBackgroundQueue:(NSArray *(^)(void))backgroundBlock thenOnMainQueue:(void(^)(NSArray *))mainBlock;
 
 
+/// Whether or not to display the "share" icon in the middle of the toolbar. NO by default.
+@property (nonatomic) BOOL showsShareToolbarItem;
+/// Called when the share button is pressed.
+/// Default implementation does nothign. Subclasses may override.
+- (void)shareButtonPressed;
+
 @end
 @end

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

@@ -7,9 +7,12 @@
 //
 //
 
 
 #import "FLEXTableViewController.h"
 #import "FLEXTableViewController.h"
+#import "FLEXExplorerViewController.h"
+#import "FLEXTabsViewController.h"
 #import "FLEXScopeCarousel.h"
 #import "FLEXScopeCarousel.h"
 #import "FLEXTableView.h"
 #import "FLEXTableView.h"
 #import "FLEXUtility.h"
 #import "FLEXUtility.h"
+#import "UIBarButtonItem+FLEX.h"
 #import <objc/runtime.h>
 #import <objc/runtime.h>
 
 
 @interface Block : NSObject
 @interface Block : NSObject
@@ -33,7 +36,7 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
 @synthesize tableHeaderViewContainer = _tableHeaderViewContainer;
 @synthesize tableHeaderViewContainer = _tableHeaderViewContainer;
 @synthesize automaticallyShowsSearchBarCancelButton = _automaticallyShowsSearchBarCancelButton;
 @synthesize automaticallyShowsSearchBarCancelButton = _automaticallyShowsSearchBarCancelButton;
 
 
-#pragma mark - Public
+#pragma mark - Initialization
 
 
 - (id)init {
 - (id)init {
 #if FLEX_AT_LEAST_IOS13_SDK
 #if FLEX_AT_LEAST_IOS13_SDK
@@ -60,6 +63,9 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
     return self;
     return self;
 }
 }
 
 
+
+#pragma mark - Public
+
 - (FLEXWindow *)window {
 - (FLEXWindow *)window {
     return (id)self.view.window;
     return (id)self.view.window;
 }
 }
@@ -178,6 +184,14 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
     });
     });
 }
 }
 
 
+- (void)setsShowsShareToolbarItem:(BOOL)showsShareToolbarItem {
+    _showsShareToolbarItem = showsShareToolbarItem;
+    if (self.isViewLoaded) {
+        [self setupToolbarItems];
+    }
+}
+
+
 #pragma mark - View Controller Lifecycle
 #pragma mark - View Controller Lifecycle
 
 
 - (void)loadView {
 - (void)loadView {
@@ -190,6 +204,10 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
     [super viewDidLoad];
     [super viewDidLoad];
     
     
     self.tableView.keyboardDismissMode = UIScrollViewKeyboardDismissModeOnDrag;
     self.tableView.keyboardDismissMode = UIScrollViewKeyboardDismissModeOnDrag;
+    
+    // Toolbar
+    self.navigationController.toolbarHidden = NO;
+    self.navigationController.hidesBarsOnSwipe = YES;
 
 
     // On iOS 13, the root view controller shows it's search bar no matter what.
     // On iOS 13, the root view controller shows it's search bar no matter what.
     // Turning this off avoids some weird flash the navigation bar does when we
     // Turning this off avoids some weird flash the navigation bar does when we
@@ -203,11 +221,6 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
     }
     }
 }
 }
 
 
-- (void)viewWillLayoutSubviews {
-    [super viewWillLayoutSubviews];
-    
-}
-
 - (void)viewWillAppear:(BOOL)animated {
 - (void)viewWillAppear:(BOOL)animated {
     [super viewWillAppear:animated];
     [super viewWillAppear:animated];
     
     
@@ -217,6 +230,8 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
             self.navigationItem.hidesSearchBarWhenScrolling = NO;
             self.navigationItem.hidesSearchBarWhenScrolling = NO;
         }
         }
     }
     }
+
+    [self setupToolbarItems];
 }
 }
 
 
 - (void)viewDidAppear:(BOOL)animated {
 - (void)viewDidAppear:(BOOL)animated {
@@ -248,8 +263,36 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
     self.didInitiallyRevealSearchBar = NO;
     self.didInitiallyRevealSearchBar = NO;
 }
 }
 
 
+
 #pragma mark - Private
 #pragma mark - Private
 
 
+- (void)setupToolbarItems {
+    UIBarButtonItem *emptySpaceOrShare = UIBarButtonItem.flex_fixedSpace;
+    if (self.showsShareToolbarItem) {
+        emptySpaceOrShare = FLEXBarButtonItemSystem(Action, self, @selector(shareButtonPressed));
+    }
+    
+    self.toolbarItems = @[
+        UIBarButtonItem.flex_fixedSpace,
+        UIBarButtonItem.flex_flexibleSpace,
+        UIBarButtonItem.flex_fixedSpace,
+        UIBarButtonItem.flex_flexibleSpace,
+        UIBarButtonItem.flex_fixedSpace,
+        UIBarButtonItem.flex_flexibleSpace,
+        emptySpaceOrShare,
+        UIBarButtonItem.flex_flexibleSpace,
+        FLEXBarButtonItemSystem(Bookmarks, self, @selector(showBookmarks)),
+        UIBarButtonItem.flex_flexibleSpace,
+        FLEXBarButtonItemSystem(Organize, self, @selector(showTabSwitcher)),
+    ];
+    
+    // Disable tabs entirely when not presented by FLEXExplorerViewController
+    UIViewController *presenter = self.navigationController.presentingViewController;
+    if (![presenter isKindOfClass:[FLEXExplorerViewController class]]) {
+        self.toolbarItems.lastObject.enabled = NO;
+    }
+}
+
 - (void)debounce:(void(^)(void))block {
 - (void)debounce:(void(^)(void))block {
     [self.debounceTimer invalidate];
     [self.debounceTimer invalidate];
     
     
@@ -366,6 +409,22 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
     return _tableHeaderViewContainer;
     return _tableHeaderViewContainer;
 }
 }
 
 
+- (void)showBookmarks {
+    // TODO
+}
+
+- (void)showTabSwitcher {
+    UINavigationController *nav = [[UINavigationController alloc]
+        initWithRootViewController:[FLEXTabsViewController new]
+    ];
+    [self presentViewController:nav animated:YES completion:nil];
+}
+
+- (void)shareButtonPressed {
+
+}
+
+
 #pragma mark - Search Bar
 #pragma mark - Search Bar
 
 
 #pragma mark UISearchResultsUpdating
 #pragma mark UISearchResultsUpdating
@@ -391,6 +450,7 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
     }
     }
 }
 }
 
 
+
 #pragma mark UISearchControllerDelegate
 #pragma mark UISearchControllerDelegate
 
 
 - (void)willPresentSearchController:(UISearchController *)searchController {
 - (void)willPresentSearchController:(UISearchController *)searchController {
@@ -407,6 +467,7 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
     }
     }
 }
 }
 
 
+
 #pragma mark UISearchBarDelegate
 #pragma mark UISearchBarDelegate
 
 
 /// Not necessary in iOS 13; remove this when iOS 13 is the deployment target
 /// Not necessary in iOS 13; remove this when iOS 13 is the deployment target
@@ -414,6 +475,7 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
     [self updateSearchResultsForSearchController:self.searchController];
     [self updateSearchResultsForSearchController:self.searchController];
 }
 }
 
 
+
 #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
 /// Not having a title in the first section looks weird with a rounded-corner table view style

+ 10 - 2
Classes/Editing/FLEXFieldEditorViewController.m

@@ -12,6 +12,7 @@
 #import "FLEXPropertyAttributes.h"
 #import "FLEXPropertyAttributes.h"
 #import "FLEXUtility.h"
 #import "FLEXUtility.h"
 #import "FLEXColor.h"
 #import "FLEXColor.h"
+#import "UIBarButtonItem+FLEX.h"
 
 
 @interface FLEXFieldEditorViewController () <FLEXArgumentInputViewDelegate>
 @interface FLEXFieldEditorViewController () <FLEXArgumentInputViewDelegate>
 
 
@@ -61,7 +62,9 @@
         target:self
         target:self
         action:@selector(getterButtonPressed:)
         action:@selector(getterButtonPressed:)
     ];
     ];
-    self.navigationItem.rightBarButtonItems = @[self.setterButton, self.getterButton];
+    self.toolbarItems = @[
+        UIBarButtonItem.flex_flexibleSpace, self.getterButton, self.setterButton
+    ];
 
 
     // Configure input view
     // Configure input view
     self.fieldEditorView.fieldDescription = self.fieldDescription;
     self.fieldEditorView.fieldDescription = self.fieldDescription;
@@ -72,7 +75,12 @@
 
 
     // Don't show a "set" button for switches; we mutate when the switch is flipped
     // Don't show a "set" button for switches; we mutate when the switch is flipped
     if ([inputView isKindOfClass:[FLEXArgumentInputSwitchView class]]) {
     if ([inputView isKindOfClass:[FLEXArgumentInputSwitchView class]]) {
-        self.navigationItem.rightBarButtonItem = nil;
+        self.setterButton.enabled = NO;
+        self.setterButton.title = @"Flip the switch to call the setter";
+        // Put getter button before setter button 
+        self.toolbarItems = @[
+            UIBarButtonItem.flex_flexibleSpace, self.setterButton, self.getterButton
+        ];
     }
     }
 }
 }
 
 

+ 1 - 1
Classes/Editing/FLEXVariableEditorViewController.h

@@ -23,7 +23,7 @@
 // For subclass use only.
 // For subclass use only.
 @property (nonatomic, readonly) id target;
 @property (nonatomic, readonly) id target;
 @property (nonatomic, readonly) FLEXFieldEditorView *fieldEditorView;
 @property (nonatomic, readonly) FLEXFieldEditorView *fieldEditorView;
-/// Subclasses can change the button title via the \c title property
+/// Subclasses can change the button title via the button's \c title property
 @property (nonatomic, readonly) UIBarButtonItem *setterButton;
 @property (nonatomic, readonly) UIBarButtonItem *setterButton;
 
 
 - (void)actionButtonPressed:(id)sender;
 - (void)actionButtonPressed:(id)sender;

+ 4 - 1
Classes/Editing/FLEXVariableEditorViewController.m

@@ -15,6 +15,7 @@
 #import "FLEXArgumentInputView.h"
 #import "FLEXArgumentInputView.h"
 #import "FLEXArgumentInputViewFactory.h"
 #import "FLEXArgumentInputViewFactory.h"
 #import "FLEXObjectExplorerViewController.h"
 #import "FLEXObjectExplorerViewController.h"
+#import "UIBarButtonItem+FLEX.h"
 
 
 @interface FLEXVariableEditorViewController () <UIScrollViewDelegate>
 @interface FLEXVariableEditorViewController () <UIScrollViewDelegate>
 @property (nonatomic) UIScrollView *scrollView;
 @property (nonatomic) UIScrollView *scrollView;
@@ -98,7 +99,9 @@
         target:self
         target:self
         action:@selector(actionButtonPressed:)
         action:@selector(actionButtonPressed:)
     ];
     ];
-    self.navigationItem.rightBarButtonItem = self.setterButton;
+    
+    self.navigationController.toolbarHidden = NO;
+    self.toolbarItems = @[UIBarButtonItem.flex_flexibleSpace, self.setterButton];
 }
 }
 
 
 - (void)viewWillLayoutSubviews {
 - (void)viewWillLayoutSubviews {

+ 30 - 43
Classes/ExplorerInterface/FLEXExplorerViewController.m

@@ -11,12 +11,14 @@
 #import "FLEXToolbarItem.h"
 #import "FLEXToolbarItem.h"
 #import "FLEXUtility.h"
 #import "FLEXUtility.h"
 #import "FLEXWindow.h"
 #import "FLEXWindow.h"
+#import "FLEXTabList.h"
 #import "FLEXNavigationController.h"
 #import "FLEXNavigationController.h"
 #import "FLEXHierarchyViewController.h"
 #import "FLEXHierarchyViewController.h"
 #import "FLEXGlobalsViewController.h"
 #import "FLEXGlobalsViewController.h"
 #import "FLEXObjectExplorerViewController.h"
 #import "FLEXObjectExplorerViewController.h"
 #import "FLEXObjectExplorerFactory.h"
 #import "FLEXObjectExplorerFactory.h"
 #import "FLEXNetworkHistoryTableViewController.h"
 #import "FLEXNetworkHistoryTableViewController.h"
+#import "FLEXTabsViewController.h"
 
 
 static NSString *const kFLEXToolbarTopMarginDefaultsKey = @"com.flex.FLEXToolbar.topMargin";
 static NSString *const kFLEXToolbarTopMarginDefaultsKey = @"com.flex.FLEXToolbar.topMargin";
 
 
@@ -416,23 +418,28 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
 #pragma mark - Toolbar Dragging
 #pragma mark - Toolbar Dragging
 
 
 - (void)setupToolbarGestures {
 - (void)setupToolbarGestures {
+    FLEXExplorerToolbar *toolbar = self.explorerToolbar;
+    
     // Pan gesture for dragging.
     // Pan gesture for dragging.
-    UIPanGestureRecognizer *panGR = [[UIPanGestureRecognizer alloc]
+    [toolbar.dragHandle addGestureRecognizer:[[UIPanGestureRecognizer alloc]
         initWithTarget:self action:@selector(handleToolbarPanGesture:)
         initWithTarget:self action:@selector(handleToolbarPanGesture:)
-    ];
-    [self.explorerToolbar.dragHandle addGestureRecognizer:panGR];
+    ]];
     
     
     // Tap gesture for hinting.
     // Tap gesture for hinting.
-    UITapGestureRecognizer *hintTapGR = [[UITapGestureRecognizer alloc]
+    [toolbar.dragHandle addGestureRecognizer:[[UITapGestureRecognizer alloc]
         initWithTarget:self action:@selector(handleToolbarHintTapGesture:)
         initWithTarget:self action:@selector(handleToolbarHintTapGesture:)
-    ];
-    [self.explorerToolbar.dragHandle addGestureRecognizer:hintTapGR];
+    ]];
     
     
     // Tap gesture for showing additional details
     // Tap gesture for showing additional details
     self.detailsTapGR = [[UITapGestureRecognizer alloc]
     self.detailsTapGR = [[UITapGestureRecognizer alloc]
         initWithTarget:self action:@selector(handleToolbarDetailsTapGesture:)
         initWithTarget:self action:@selector(handleToolbarDetailsTapGesture:)
     ];
     ];
-    [self.explorerToolbar.selectedViewDescriptionContainer addGestureRecognizer:self.detailsTapGR];
+    [toolbar.selectedViewDescriptionContainer addGestureRecognizer:self.detailsTapGR];
+    
+    // Long press gesture to present tabs manager
+    [toolbar.globalsItem addGestureRecognizer:[[UILongPressGestureRecognizer alloc]
+        initWithTarget:self action:@selector(handleToolbarShowTabsGesture:)
+    ]];
 }
 }
 
 
 - (void)handleToolbarPanGesture:(UIPanGestureRecognizer *)panGR {
 - (void)handleToolbarPanGesture:(UIPanGestureRecognizer *)panGR {
@@ -501,12 +508,21 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
 - (void)handleToolbarDetailsTapGesture:(UITapGestureRecognizer *)tapGR {
 - (void)handleToolbarDetailsTapGesture:(UITapGestureRecognizer *)tapGR {
     if (tapGR.state == UIGestureRecognizerStateRecognized && self.selectedView) {
     if (tapGR.state == UIGestureRecognizerStateRecognized && self.selectedView) {
         UIViewController *topStackVC = [FLEXObjectExplorerFactory explorerViewControllerForObject:self.selectedView];
         UIViewController *topStackVC = [FLEXObjectExplorerFactory explorerViewControllerForObject:self.selectedView];
-        [self makeKeyAndPresentViewController:
+        [self presentViewController:
             [FLEXNavigationController withRootViewController:topStackVC]
             [FLEXNavigationController withRootViewController:topStackVC]
         animated:YES completion:nil];
         animated:YES completion:nil];
     }
     }
 }
 }
 
 
+- (void)handleToolbarShowTabsGesture:(UILongPressGestureRecognizer *)sender {
+    // Back up the UIMenuController items since dismissViewController: will attempt to replace them
+    self.appMenuItems = UIMenuController.sharedMenuController.menuItems;
+    
+    [super presentViewController:[[UINavigationController alloc]
+        initWithRootViewController:[FLEXTabsViewController new]
+    ] animated:YES completion:nil];
+}
+
 
 
 #pragma mark - View Selection
 #pragma mark - View Selection
 
 
@@ -737,20 +753,9 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
 }
 }
 
 
 
 
-#pragma mark - Modal Dismissal
-
-- (void)presentationControllerDidDismiss:(UIPresentationController *)presentationController {
-    [self presentedViewControllerDidDismiss];
-}
-
-- (void)presentedViewControllerDidDismiss {
-    [self resignKeyAndDismissViewControllerAnimated:YES completion:nil];
-}
-
-
 #pragma mark - Modal Presentation and Window Management
 #pragma mark - Modal Presentation and Window Management
 
 
-- (void)makeKeyAndPresentViewController:(UINavigationController *)toPresent
+- (void)presentViewController:(UIViewController *)toPresent
                                animated:(BOOL)animated
                                animated:(BOOL)animated
                              completion:(void (^)(void))completion {
                              completion:(void (^)(void))completion {
     // Make our window key to correctly handle input.
     // Make our window key to correctly handle input.
@@ -771,29 +776,11 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
     UIMenuController.sharedMenuController.menuItems = @[copyObjectAddress];
     UIMenuController.sharedMenuController.menuItems = @[copyObjectAddress];
     [UIMenuController.sharedMenuController update];
     [UIMenuController.sharedMenuController update];
     
     
-    // Add the "Done" button to the navigation controller's view controller if it doesn't have one
-    // (the hierarchy screen adds it's own done button in order to pass data between us)
-    if (!toPresent.topViewController.navigationItem.rightBarButtonItem) {
-        toPresent.topViewController.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc]
-            initWithBarButtonSystemItem:UIBarButtonSystemItemDone
-            target:self
-            action:@selector(presentedViewControllerDidDismiss)
-        ];
-    }
-    
-    // Make myself the delegate for sheets presented modally so we can do the
-    // proper cleanup when sheets are dragged to dismiss without the done button
-    if (@available(iOS 13, *)) {
-        toPresent.presentationController.delegate = self;
-    }
-    
     // Show the view controller.
     // Show the view controller.
-    [self presentViewController:toPresent animated:animated completion:completion];
+    [super presentViewController:toPresent animated:animated completion:completion];
 }
 }
 
 
-- (void)resignKeyAndDismissViewControllerAnimated:(BOOL)animated
-                                       completion:(void (^)(void))completion
-{
+- (void)dismissViewControllerAnimated:(BOOL)animated completion:(void (^)(void))completion {    
     UIWindow *appWindow = self.window.previousKeyWindow;
     UIWindow *appWindow = self.window.previousKeyWindow;
     [appWindow makeKeyWindow];
     [appWindow makeKeyWindow];
     [appWindow.rootViewController setNeedsStatusBarAppearanceUpdate];
     [appWindow.rootViewController setNeedsStatusBarAppearanceUpdate];
@@ -809,7 +796,7 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
     // scroll to top, but below FLEX otherwise for exploration.
     // scroll to top, but below FLEX otherwise for exploration.
     [self statusWindow].windowLevel = UIWindowLevelStatusBar;
     [self statusWindow].windowLevel = UIWindowLevelStatusBar;
     
     
-    [self dismissViewControllerAnimated:animated completion:completion];
+    [super dismissViewControllerAnimated:animated completion:completion];
 }
 }
 
 
 - (BOOL)wantsWindowToBecomeKey
 - (BOOL)wantsWindowToBecomeKey
@@ -820,9 +807,9 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
 - (void)toggleToolWithViewControllerProvider:(UINavigationController *(^)(void))future
 - (void)toggleToolWithViewControllerProvider:(UINavigationController *(^)(void))future
                                   completion:(void(^)(void))completion {
                                   completion:(void(^)(void))completion {
     if (self.presentedViewController) {
     if (self.presentedViewController) {
-        [self resignKeyAndDismissViewControllerAnimated:YES completion:completion];
+        [self dismissViewControllerAnimated:YES completion:completion];
     } else if (future) {
     } else if (future) {
-        [self makeKeyAndPresentViewController:future() animated:YES completion:completion];
+        [self presentViewController:future() animated:YES completion:completion];
     }
     }
 }
 }
 
 

+ 45 - 0
Classes/ExplorerInterface/Tabs/FLEXTabList.h

@@ -0,0 +1,45 @@
+//
+//  FLEXTabList.h
+//  FLEX
+//
+//  Created by Tanner on 2/1/20.
+//  Copyright © 2020 Flipboard. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface FLEXTabList : NSObject
+
+@property (nonatomic, readonly, class) FLEXTabList *sharedList;
+
+@property (nonatomic, readonly, nullable) UINavigationController *activeTab;
+@property (nonatomic, readonly) NSArray<UINavigationController *> *openTabs;
+/// Snapshots of each tab when they were last active.
+@property (nonatomic, readonly) NSArray<UIImage *> *openTabSnapshots;
+/// \c NSNotFound if no tabs are present.
+/// Setting this property changes the active tab to one of the already open tabs.
+@property (nonatomic) NSInteger activeTabIndex;
+
+/// Adds a new tab and sets the new tab as the active tab.
+- (void)addTab:(UINavigationController *)newTab;
+/// Closes the given tab. If this tab was the active tab,
+/// the most recent tab before that becomes the active tab.
+- (void)closeTab:(UINavigationController *)tab;
+/// Closes a tab at the given index. If this tab was the active tab,
+/// the most recent tab before that becomes the active tab.
+- (void)closeTabAtIndex:(NSInteger)idx;
+/// Closes all of the tabs at the given indexes. If the active tab
+/// is included, the most recent still-open tab becomes the active tab.
+- (void)closeTabsAtIndexes:(NSIndexSet *)indexes;
+/// A shortcut to close the active tab.
+- (void)closeActiveTab;
+/// A shortcut to close \e every tab.
+- (void)closeAllTabs;
+
+- (void)updateSnapshotForActiveTab;
+
+@end
+
+NS_ASSUME_NONNULL_END

+ 133 - 0
Classes/ExplorerInterface/Tabs/FLEXTabList.m

@@ -0,0 +1,133 @@
+//
+//  FLEXTabList.m
+//  FLEX
+//
+//  Created by Tanner on 2/1/20.
+//  Copyright © 2020 Flipboard. All rights reserved.
+//
+
+#import "FLEXTabList.h"
+#import "FLEXUtility.h"
+
+@interface FLEXTabList () {
+    NSMutableArray *_openTabs;
+    NSMutableArray *_openTabSnapshots;
+}
+@end
+#pragma mark -
+@implementation FLEXTabList
+
+#pragma mark Initialization
+
++ (FLEXTabList *)sharedList {
+    static FLEXTabList *sharedList = nil;
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        sharedList = [self new];
+    });
+    
+    return sharedList;
+}
+
+- (id)init {
+    self = [super init];
+    if (self) {
+        _openTabs = [NSMutableArray new];
+        _openTabSnapshots = [NSMutableArray new];
+        _activeTabIndex = NSNotFound;
+    }
+    
+    return self;
+}
+
+
+#pragma mark Private
+
+- (void)chooseNewActiveTab {
+    if (self.openTabs.count) {
+        self.activeTabIndex = self.openTabs.count - 1;
+    } else {
+        self.activeTabIndex = NSNotFound;
+    }
+}
+
+
+#pragma mark Public
+
+- (void)setActiveTabIndex:(NSInteger)idx {
+    NSParameterAssert(idx < self.openTabs.count || idx == NSNotFound);
+    if (_activeTabIndex == idx) return;
+    
+    _activeTabIndex = idx;
+    _activeTab = (idx == NSNotFound) ? nil : self.openTabs[idx];
+}
+
+- (void)addTab:(UINavigationController *)newTab {
+    NSParameterAssert(newTab);
+    
+    // Update snapshot of the last active tab
+    if (self.activeTab) {
+        [self updateSnapshotForActiveTab];
+    }
+    
+    // Add new tab and snapshot,
+    // update active tab and index
+    [_openTabs addObject:newTab];
+    [_openTabSnapshots addObject:[FLEXUtility previewImageForView:newTab.view]];
+    _activeTab = newTab;
+    _activeTabIndex = self.openTabs.count - 1;
+}
+
+- (void)closeTab:(UINavigationController *)tab {
+    NSParameterAssert(tab);
+    NSParameterAssert([self.openTabs containsObject:tab]);
+    NSInteger idx = [self.openTabs indexOfObject:tab];
+    
+    [self closeTabAtIndex:idx];
+}
+
+- (void)closeTabAtIndex:(NSInteger)idx {
+    NSParameterAssert(idx < self.openTabs.count);
+    
+    // Remove old tab and snapshot
+    [_openTabs removeObjectAtIndex:idx];
+    [_openTabSnapshots removeObjectAtIndex:idx];
+    
+    // Update active tab and index if needed
+    if (self.activeTabIndex == idx) {
+        [self chooseNewActiveTab];
+    }
+}
+
+- (void)closeTabsAtIndexes:(NSIndexSet *)indexes {
+    // Remove old tabs and snapshot
+    [_openTabs removeObjectsAtIndexes:indexes];
+    [_openTabSnapshots removeObjectsAtIndexes:indexes];
+    
+    // Update active tab and index if needed
+    if ([indexes containsIndex:self.activeTabIndex]) {
+        [self chooseNewActiveTab];
+    }
+}
+
+- (void)closeActiveTab {
+    [self closeTab:self.activeTab];
+}
+
+- (void)closeAllTabs {
+    // Remove tabs and snapshots
+    [_openTabs removeAllObjects];
+    [_openTabSnapshots removeAllObjects];
+    
+    // Update active tab index
+    self.activeTabIndex = NSNotFound;
+}
+
+- (void)updateSnapshotForActiveTab {
+    if (self.activeTabIndex != NSNotFound) {
+        UIImage *newSnapshot = [FLEXUtility previewImageForView:self.activeTab.view];
+        [_openTabSnapshots replaceObjectAtIndex:self.activeTabIndex withObject:newSnapshot];
+    }
+}
+
+@end

+ 13 - 0
Classes/ExplorerInterface/Tabs/FLEXTabsViewController.h

@@ -0,0 +1,13 @@
+//
+//  FLEXTabsViewController.h
+//  FLEX
+//
+//  Created by Tanner on 2/4/20.
+//  Copyright © 2020 Flipboard. All rights reserved.
+//
+
+#import "FLEXTableViewController.h"
+
+@interface FLEXTabsViewController : FLEXTableViewController
+
+@end

+ 295 - 0
Classes/ExplorerInterface/Tabs/FLEXTabsViewController.m

@@ -0,0 +1,295 @@
+//
+//  FLEXTabsViewController.m
+//  FLEX
+//
+//  Created by Tanner on 2/4/20.
+//  Copyright © 2020 Flipboard. All rights reserved.
+//
+
+#import "FLEXTabsViewController.h"
+#import "FLEXNavigationController.h"
+#import "FLEXTabList.h"
+#import "FLEXTableView.h"
+#import "FLEXUtility.h"
+#import "FLEXColor.h"
+#import "UIBarButtonItem+FLEX.h"
+#import "FLEXExplorerViewController.h"
+#import "FLEXGlobalsViewController.h"
+
+@interface FLEXTabsViewController ()
+@property (nonatomic, copy) NSArray<UINavigationController *> *openTabs;
+@property (nonatomic, copy) NSArray<UIImage *> *tabSnapshots;
+@property (nonatomic) NSInteger activeIndex;
+@property (nonatomic) BOOL presentNewActiveTabOnDismiss;
+
+@property (nonatomic, readonly) FLEXExplorerViewController *corePresenter;
+@end
+
+@implementation FLEXTabsViewController
+
+#pragma mark - Initialization
+
+- (id)init {
+    return [self initWithStyle:UITableViewStylePlain];
+}
+
+- (void)viewDidLoad {
+    [super viewDidLoad];
+    
+    self.title = @"Open Tabs";
+    self.navigationController.hidesBarsOnSwipe = NO;
+    self.tableView.allowsMultipleSelectionDuringEditing = YES;
+    
+    [FLEXTabList.sharedList updateSnapshotForActiveTab];
+    [self reloadData:NO];
+}
+
+- (void)viewWillAppear:(BOOL)animated {
+    [super viewWillAppear:animated];
+    [self setupDefaultBarItems];
+}
+
+#pragma mark - Private
+
+/// @param trackActiveTabDelta whether to check if the active
+/// tab changed and needs to be presented upon "Done" dismissal.
+/// @return whether the active tab changed or not (if there are any tabs left)
+- (BOOL)reloadData:(BOOL)trackActiveTabDelta {
+    BOOL activeTabDidChange = NO;
+    FLEXTabList *list = FLEXTabList.sharedList;
+    
+    // Flag to enable check to determine whether
+    if (trackActiveTabDelta) {
+        NSInteger oldActiveIndex = self.activeIndex;
+        if (oldActiveIndex != list.activeTabIndex && list.activeTabIndex != NSNotFound) {
+            self.presentNewActiveTabOnDismiss = YES;
+            activeTabDidChange = YES;
+        } else if (self.presentNewActiveTabOnDismiss) {
+            // If we had something to present before, now we don't
+            // (i.e. activeTabIndex == NSNotFound)
+            self.presentNewActiveTabOnDismiss = NO;
+        }
+    }
+    
+    // We assume the tabs aren't going to change out from under us, since
+    // presenting any other tool via keyboard shortcuts should dismiss us first
+    self.openTabs = list.openTabs;
+    self.tabSnapshots = list.openTabSnapshots;
+    self.activeIndex = list.activeTabIndex;
+    
+    return activeTabDidChange;
+}
+
+- (void)reloadActiveTabRowIfChanged:(BOOL)activeTabChanged {
+    // Refresh the newly active tab row if needed
+    if (activeTabChanged) {
+        NSIndexPath *active = [NSIndexPath
+           indexPathForRow:self.activeIndex inSection:0
+        ];
+        [self.tableView reloadRowsAtIndexPaths:@[active] withRowAnimation:UITableViewRowAnimationNone];
+    }
+}
+
+- (void)setupDefaultBarItems {
+    self.navigationItem.rightBarButtonItem = FLEXBarButtonItemSystem(Done, self, @selector(dismissAnimated));
+    self.toolbarItems = @[
+        UIBarButtonItem.flex_fixedSpace,
+        UIBarButtonItem.flex_flexibleSpace,
+        FLEXBarButtonItemSystem(Add, self, @selector(addTabButtonPressed)),
+        UIBarButtonItem.flex_flexibleSpace,
+        FLEXBarButtonItemSystem(Edit, self, @selector(toggleEditing)),
+    ];
+    
+    // Disable editing if no tabs available
+    self.toolbarItems.lastObject.enabled = self.openTabs.count > 0;
+}
+
+- (void)setupEditingBarItems {
+    self.navigationItem.rightBarButtonItem = nil;
+    self.toolbarItems = @[
+        [UIBarButtonItem itemWithTitle:@"Close All" target:self action:@selector(closeAllButtonPressed)],
+        UIBarButtonItem.flex_flexibleSpace,
+        [UIBarButtonItem disabledSystemItem:UIBarButtonSystemItemAdd],
+        UIBarButtonItem.flex_flexibleSpace,
+        // We use a non-system done item because we change its title dynamically
+        [UIBarButtonItem doneStyleitemWithTitle:@"Done" target:self action:@selector(toggleEditing)]
+    ];
+    
+    self.toolbarItems.firstObject.tintColor = FLEXColor.destructiveColor;
+}
+
+- (FLEXExplorerViewController *)corePresenter {
+    // We must be presented by a FLEXExplorerViewController, or presented
+    // by another view controller that was presented by FLEXExplorerViewController
+    FLEXExplorerViewController *presenter = (id)self.presentingViewController;
+    presenter = (id)presenter.presentingViewController ?: presenter;
+    NSAssert(
+        [presenter isKindOfClass:[FLEXExplorerViewController class]],
+        @"The tabs view controller expects to be presented by the explorer controller"
+    );
+    return presenter;
+}
+
+#pragma mark Button Actions
+
+- (void)dismissAnimated {
+    if (self.presentNewActiveTabOnDismiss) {
+        // The active tab was closed so we need to present the new one
+        UIViewController *activeTab = FLEXTabList.sharedList.activeTab;
+        FLEXExplorerViewController *presenter = self.corePresenter;
+        [presenter dismissViewControllerAnimated:YES completion:^{
+            [presenter presentViewController:activeTab animated:YES completion:nil];
+        }];
+    } else if (self.activeIndex == NSNotFound) {
+        // The only tab was closed
+        [self.corePresenter dismissViewControllerAnimated:YES completion:nil];
+    } else {
+        // Simple dismiss with the same active tab
+        [self dismissViewControllerAnimated:YES completion:nil];
+    }
+}
+
+- (void)toggleEditing {
+    NSArray<NSIndexPath *> *selected = self.tableView.indexPathsForSelectedRows;
+    self.editing = !self.editing;
+    
+    if (self.isEditing) {
+        [self setupEditingBarItems];
+    } else {
+        [self setupDefaultBarItems];
+        
+        // Get index set of tabs to close
+        NSMutableIndexSet *indexes = [NSMutableIndexSet new];
+        for (NSIndexPath *ip in selected) {
+            [indexes addIndex:ip.row];
+        }
+        
+        if (selected.count) {
+            // Close tabs and update data source
+            [FLEXTabList.sharedList closeTabsAtIndexes:indexes];
+            BOOL activeTabChanged = [self reloadData:YES];
+            
+            // Remove deleted rows
+            [self.tableView deleteRowsAtIndexPaths:selected withRowAnimation:UITableViewRowAnimationAutomatic];
+            
+            // Refresh the newly active tab row if needed
+            [self reloadActiveTabRowIfChanged:activeTabChanged];
+        }
+    }
+}
+
+- (void)addTabButtonPressed {
+    FLEXExplorerViewController *presenter = self.corePresenter;
+    [presenter dismissViewControllerAnimated:YES completion:^{
+        [presenter presentViewController:[FLEXNavigationController
+            withRootViewController:[FLEXGlobalsViewController new]
+        ] animated:YES completion:nil];
+    }];
+}
+
+- (void)closeAllButtonPressed {
+    [FLEXAlert makeSheet:^(FLEXAlert *make) {
+        NSInteger count = self.openTabs.count;
+        NSString *title = FLEXPluralFormatString(count, @"Close %@ tabs", @"Close %@ tab");
+        make.button(title).destructiveStyle().handler(^(NSArray<NSString *> *strings) {
+            [self closeAllTabs];
+            [self toggleEditing];
+        });
+        make.button(@"Cancel").cancelStyle();
+    } showFrom:self];
+}
+
+- (void)closeAllTabs {
+    NSInteger rowCount = self.openTabs.count;
+    
+    // Close tabs and update data source
+    [FLEXTabList.sharedList closeAllTabs];
+    [self reloadData:YES];
+    
+    // Delete rows from table view
+    NSArray<NSIndexPath *> *allRows = [NSArray flex_forEachUpTo:rowCount map:^id(NSUInteger row) {
+        return [NSIndexPath indexPathForRow:row inSection:0];
+    }];
+    [self.tableView deleteRowsAtIndexPaths:allRows withRowAnimation:UITableViewRowAnimationAutomatic];
+}
+
+#pragma mark - Table View Data Source
+
+- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
+    return self.openTabs.count;
+}
+
+- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
+    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kFLEXDetailCell forIndexPath:indexPath];
+    
+    UINavigationController *tab = self.openTabs[indexPath.row];
+    cell.imageView.image = self.tabSnapshots[indexPath.row];
+    cell.textLabel.text = tab.topViewController.title;
+    cell.detailTextLabel.text = FLEXPluralString(tab.viewControllers.count, @"pages", @"page");
+    
+    if (!cell.tag) {
+        cell.textLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline];
+        cell.detailTextLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleSubheadline];
+        cell.tag = 1;
+    }
+    
+    if (indexPath.row == self.activeIndex) {
+        cell.backgroundColor = FLEXColor.secondaryBackgroundColor;
+    } else {
+        cell.backgroundColor = FLEXColor.primaryBackgroundColor;
+    }
+    
+    return cell;
+}
+
+#pragma mark - Table View Delegate
+
+- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
+    if (self.editing) {
+        // Case: editing with multi-select
+        self.toolbarItems.lastObject.title = @"Close Selected";
+        self.toolbarItems.lastObject.tintColor = FLEXColor.destructiveColor;
+    } else {
+        if (self.activeIndex == indexPath.row && self.corePresenter != self.presentingViewController) {
+            // Case: selected the already active tab
+            [self dismissAnimated];
+        } else {
+            // Case: selected a different tab,
+            // or selected a tab when presented from the FLEX toolbar
+            FLEXTabList.sharedList.activeTabIndex = indexPath.row;
+            self.presentNewActiveTabOnDismiss = YES;
+            [self dismissAnimated];
+        }
+    }
+}
+
+- (void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath {
+    NSParameterAssert(self.editing);
+    
+    if (tableView.indexPathsForSelectedRows.count == 0) {
+        self.toolbarItems.lastObject.title = @"Done";
+        self.toolbarItems.lastObject.tintColor = self.view.tintColor;
+    }
+}
+
+- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
+    return YES;
+}
+
+- (void)tableView:(UITableView *)table
+commitEditingStyle:(UITableViewCellEditingStyle)edit
+forRowAtIndexPath:(NSIndexPath *)indexPath {
+    NSParameterAssert(edit == UITableViewCellEditingStyleDelete);
+    
+    // Close tab and update data source
+    [FLEXTabList.sharedList closeTab:self.openTabs[indexPath.row]];
+    BOOL activeTabChanged = [self reloadData:YES];
+    
+    // Delete row from table view
+    [table deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
+    
+    // Refresh the newly active tab row if needed
+    [self reloadActiveTabRowIfChanged:activeTabChanged];
+}
+
+@end

+ 13 - 0
Classes/GlobalStateExplorers/Globals/FLEXGlobalsViewController.m

@@ -32,6 +32,8 @@
 
 
 @implementation FLEXGlobalsViewController
 @implementation FLEXGlobalsViewController
 
 
+#pragma mark - Initialization
+
 + (NSString *)globalsTitleForSection:(FLEXGlobalsSectionKind)section {
 + (NSString *)globalsTitleForSection:(FLEXGlobalsSectionKind)section {
     switch (section) {
     switch (section) {
         case FLEXGlobalsSectionProcessAndEvents:
         case FLEXGlobalsSectionProcessAndEvents:
@@ -128,6 +130,7 @@
     return sections;
     return sections;
 }
 }
 
 
+
 #pragma mark - UIViewController
 #pragma mark - UIViewController
 
 
 - (void)viewDidLoad {
 - (void)viewDidLoad {
@@ -151,6 +154,13 @@
     self.sections = self.allSections;
     self.sections = self.allSections;
 }
 }
 
 
+- (void)viewWillAppear:(BOOL)animated {
+    [super viewWillAppear:animated];
+    
+    self.navigationController.toolbarHidden = YES;
+}
+
+
 #pragma mark - Search Bar
 #pragma mark - Search Bar
 
 
 - (void)updateSearchResults:(NSString *)newText {
 - (void)updateSearchResults:(NSString *)newText {
@@ -168,6 +178,7 @@
     }
     }
 }
 }
 
 
+
 #pragma mark - Private
 #pragma mark - Private
 
 
 - (NSArray<FLEXGlobalsSection *> *)nonemptySections {
 - (NSArray<FLEXGlobalsSection *> *)nonemptySections {
@@ -176,6 +187,7 @@
     }];
     }];
 }
 }
 
 
+
 #pragma mark - Table View Data Source
 #pragma mark - Table View Data Source
 
 
 - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
 - (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
@@ -206,6 +218,7 @@
 
 
 #pragma mark - Table View Delegate
 #pragma mark - Table View Delegate
 
 
+
 - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
 - (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
     FLEXTableViewSection *section = self.sections[indexPath.section];
     FLEXTableViewSection *section = self.sections[indexPath.section];
 
 

+ 12 - 1
Classes/Manager/FLEXManager+Extensibility.m

@@ -109,6 +109,17 @@
         [self toggleTopViewControllerOfClass:[FLEXNetworkHistoryTableViewController class]];
         [self toggleTopViewControllerOfClass:[FLEXNetworkHistoryTableViewController class]];
     } description:@"Toggle network history view"];
     } description:@"Toggle network history view"];
     
     
+    // 't' is for testing: quickly present an object explorer for debugging
+    [self registerSimulatorShortcutWithKey:@"t" modifiers:0 action:^{
+        [self showExplorerIfNeeded];
+        
+        [self.explorerViewController toggleToolWithViewControllerProvider:^UINavigationController *{
+            return [FLEXNavigationController withRootViewController:[FLEXObjectExplorerFactory
+                explorerViewControllerForObject:NSBundle.mainBundle
+            ]];
+        } completion:nil];
+    } description:@"Present an object explorer for debugging"];
+    
     [self registerSimulatorShortcutWithKey:UIKeyInputDownArrow modifiers:0 action:^{
     [self registerSimulatorShortcutWithKey:UIKeyInputDownArrow modifiers:0 action:^{
         if (self.isHidden || ![self.explorerViewController handleDownArrowKeyPressed]) {
         if (self.isHidden || ![self.explorerViewController handleDownArrowKeyPressed]) {
             [self tryScrollDown];
             [self tryScrollDown];
@@ -234,7 +245,7 @@
         }
         }
     } else {
     } else {
         // Present it in an entirely new navigation controller
         // Present it in an entirely new navigation controller
-        [topViewController presentViewController:
+        [self.explorerViewController presentViewController:
             [FLEXNavigationController withRootViewController:[class new]]
             [FLEXNavigationController withRootViewController:[class new]]
         animated:YES completion:nil];
         animated:YES completion:nil];
     }
     }

+ 23 - 2
Classes/ObjectExplorers/FLEXObjectExplorerViewController.m

@@ -9,11 +9,13 @@
 #import "FLEXObjectExplorerViewController.h"
 #import "FLEXObjectExplorerViewController.h"
 #import "FLEXUtility.h"
 #import "FLEXUtility.h"
 #import "FLEXRuntimeUtility.h"
 #import "FLEXRuntimeUtility.h"
+#import "UIBarButtonItem+FLEX.h"
 #import "FLEXMultilineTableViewCell.h"
 #import "FLEXMultilineTableViewCell.h"
 #import "FLEXObjectExplorerFactory.h"
 #import "FLEXObjectExplorerFactory.h"
 #import "FLEXFieldEditorViewController.h"
 #import "FLEXFieldEditorViewController.h"
 #import "FLEXMethodCallingViewController.h"
 #import "FLEXMethodCallingViewController.h"
 #import "FLEXInstancesViewController.h"
 #import "FLEXInstancesViewController.h"
+#import "FLEXTabsViewController.h"
 #import "FLEXTableView.h"
 #import "FLEXTableView.h"
 #import "FLEXTableViewCell.h"
 #import "FLEXTableViewCell.h"
 #import "FLEXScopeCarousel.h"
 #import "FLEXScopeCarousel.h"
@@ -71,7 +73,6 @@
 #pragma mark - View controller lifecycle
 #pragma mark - View controller lifecycle
 
 
 - (void)loadView {
 - (void)loadView {
-    // TODO: grouped with rounded corners or not?
     FLEXTableView *tableView = [[FLEXTableView alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];
     FLEXTableView *tableView = [[FLEXTableView alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];
     self.tableView = tableView;
     self.tableView = tableView;
 
 
@@ -86,6 +87,8 @@
 - (void)viewDidLoad {
 - (void)viewDidLoad {
     [super viewDidLoad];
     [super viewDidLoad];
 
 
+    self.showsShareToolbarItem = YES;
+
     // Use [object class] here rather than object_getClass
     // Use [object class] here rather than object_getClass
     // to avoid the KVO prefix for observed objects
     // to avoid the KVO prefix for observed objects
     self.title = [[self.object class] description];
     self.title = [[self.object class] description];
@@ -223,7 +226,22 @@
 }
 }
 
 
 - (BOOL)gestureRecognizer:(UIGestureRecognizer *)g1 shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)g2 {
 - (BOOL)gestureRecognizer:(UIGestureRecognizer *)g1 shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)g2 {
-    return [g2 isKindOfClass:[UIPanGestureRecognizer class]];
+    return [g2 isKindOfClass:[UIPanGestureRecognizer class]] && g2 != self.navigationController.interactivePopGestureRecognizer;
+}
+
+- (void)shareButtonPressed {
+    [FLEXAlert makeSheet:^(FLEXAlert *make) {
+        make.button(@"Add to Bookmarks").handler(^(NSArray<NSString *> *strings) {
+            // TODO
+        });
+        make.button(@"Copy Description").handler(^(NSArray<NSString *> *strings) {
+            UIPasteboard.generalPasteboard.string = self.explorer.objectDescription;
+        });
+        make.button(@"Copy Address").handler(^(NSArray<NSString *> *strings) {
+            [self copyObjectAddress:nil];
+        });
+        make.button(@"Cancel").cancelStyle();
+    } showFrom:self];
 }
 }
 
 
 #pragma mark - Description
 #pragma mark - Description
@@ -239,6 +257,7 @@
     return YES;
     return YES;
 }
 }
 
 
+
 #pragma mark - Search
 #pragma mark - Search
 
 
 - (void)updateSearchResults:(NSString *)newText; {
 - (void)updateSearchResults:(NSString *)newText; {
@@ -266,6 +285,7 @@
     }
     }
 }
 }
 
 
+
 #pragma mark - Reloading
 #pragma mark - Reloading
 
 
 - (void)reloadData {
 - (void)reloadData {
@@ -406,6 +426,7 @@
 
 
 #endif
 #endif
 
 
+
 #pragma mark - UIMenuController
 #pragma mark - UIMenuController
 
 
 /// Prevent the search bar from trying to use us as a responder
 /// Prevent the search bar from trying to use us as a responder

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

@@ -0,0 +1,35 @@
+//
+//  UIBarButtonItem+FLEX.h
+//  FLEX
+//
+//  Created by Tanner on 2/4/20.
+//  Copyright © 2020 Flipboard. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+
+#define FLEXBarButtonItemSystem(item, tgt, sel) \
+    [UIBarButtonItem systemItem:UIBarButtonSystemItem##item target:tgt action:sel]
+
+@interface UIBarButtonItem (FLEX)
+
+@property (nonatomic, readonly, class) UIBarButtonItem *flex_flexibleSpace;
+@property (nonatomic, readonly, class) UIBarButtonItem *flex_fixedSpace;
+
++ (instancetype)itemWithCustomView:(UIView *)customView;
+
++ (instancetype)systemItem:(UIBarButtonSystemItem)item target:(id)target action:(SEL)action;
+
++ (instancetype)itemWithTitle:(NSString *)title target:(id)target action:(SEL)action;
++ (instancetype)doneStyleitemWithTitle:(NSString *)title target:(id)target action:(SEL)action;
+
++ (instancetype)itemWithImage:(UIImage *)image
+                        style:(UIBarButtonItemStyle)style
+                       target:(id)target
+                       action:(SEL)action;
+
++ (instancetype)disabledSystemItem:(UIBarButtonSystemItem)item;
++ (instancetype)disabledItemWithTitle:(NSString *)title style:(UIBarButtonItemStyle)style;
++ (instancetype)disabledItemWithImage:(UIImage *)image style:(UIBarButtonItemStyle)style;
+
+@end

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

@@ -0,0 +1,62 @@
+//
+//  UIBarButtonItem+FLEX.m
+//  FLEX
+//
+//  Created by Tanner on 2/4/20.
+//  Copyright © 2020 Flipboard. All rights reserved.
+//
+
+#import "UIBarButtonItem+FLEX.h"
+
+@implementation UIBarButtonItem (FLEX)
+
++ (UIBarButtonItem *)flex_flexibleSpace {
+    return [self systemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil];
+}
+
++ (UIBarButtonItem *)flex_fixedSpace {
+    return [self systemItem:UIBarButtonSystemItemFixedSpace target:nil action:nil];
+}
+
++ (instancetype)systemItem:(UIBarButtonSystemItem)item target:(id)target action:(SEL)action {
+    return [[self alloc] initWithBarButtonSystemItem:item target:target action:action];
+}
+
++ (instancetype)itemWithCustomView:(UIView *)customView {
+    return [[self alloc] initWithCustomView:customView];
+}
+
++ (instancetype)itemWithTitle:(NSString *)title target:(id)target action:(SEL)action {
+    return [[self alloc] initWithTitle:title style:UIBarButtonItemStylePlain target:target action:action];
+}
+
++ (instancetype)doneStyleitemWithTitle:(NSString *)title target:(id)target action:(SEL)action {
+    return [[self alloc] initWithTitle:title style:UIBarButtonItemStyleDone target:target action:action];
+}
+
++ (instancetype)itemWithImage:(UIImage *)image
+                        style:(UIBarButtonItemStyle)style
+                       target:(id)target
+                       action:(SEL)action {
+    return [[self alloc] initWithImage:image style:style target:target action:action];
+}
+
++ (instancetype)disabledSystemItem:(UIBarButtonSystemItem)system {
+    UIBarButtonItem *item = [self systemItem:system target:nil action:nil];
+    item.enabled = NO;
+    return item;
+}
+
++ (instancetype)disabledItemWithTitle:(NSString *)title style:(UIBarButtonItemStyle)style {
+    UIBarButtonItem *item = [self itemWithTitle:title target:nil action:nil];
+    item.enabled = NO;
+    return item;
+}
+
++ (instancetype)disabledItemWithImage:(UIImage *)image style:(UIBarButtonItemStyle)style {
+    UIBarButtonItem *item = [self itemWithImage:image style:style target:nil action:nil];
+    item.enabled = NO;
+    return item;
+}
+
+@end

+ 1 - 0
Classes/Utility/FLEXColor.h

@@ -40,6 +40,7 @@ NS_ASSUME_NONNULL_BEGIN
 @property (readonly, class) UIColor *toolbarItemHighlightedColor;
 @property (readonly, class) UIColor *toolbarItemHighlightedColor;
 @property (readonly, class) UIColor *toolbarItemSelectedColor;
 @property (readonly, class) UIColor *toolbarItemSelectedColor;
 @property (readonly, class) UIColor *hairlineColor;
 @property (readonly, class) UIColor *hairlineColor;
+@property (readonly, class) UIColor *destructiveColor;
 
 
 @end
 @end
 
 

+ 4 - 0
Classes/Utility/FLEXColor.m

@@ -133,4 +133,8 @@
     return FLEXDynamicColor(systemGrayColor, grayColor);
     return FLEXDynamicColor(systemGrayColor, grayColor);
 }
 }
 
 
++ (UIColor *)destructiveColor {
+    return FLEXDynamicColor(systemRedColor, redColor);
+}
+
 @end
 @end

+ 4 - 0
Classes/Utility/FLEXUtility.h

@@ -66,6 +66,10 @@ NS_INLINE CGRect FLEXRectSetHeight(CGRect r, CGFloat height) {
     stringWithFormat:@"%@ %@", @(count), (count == 1 ? singular : plural) \
     stringWithFormat:@"%@ %@", @(count), (count == 1 ? singular : plural) \
 ]
 ]
 
 
+#define FLEXPluralFormatString(count, pluralFormat, singularFormat) [NSString \
+    stringWithFormat:(count == 1 ? singularFormat : pluralFormat), @(count)  \
+]
+
 #if !FLEX_AT_LEAST_IOS13_SDK
 #if !FLEX_AT_LEAST_IOS13_SDK
 @class UIWindowScene;
 @class UIWindowScene;
 #endif
 #endif

+ 1 - 1
Example/UICatalog/AAPLAppDelegate.m

@@ -71,7 +71,7 @@
     [[FLEXManager sharedManager] showExplorer];
     [[FLEXManager sharedManager] showExplorer];
     [[FLEXManager sharedManager] setNetworkDebuggingEnabled:YES];
     [[FLEXManager sharedManager] setNetworkDebuggingEnabled:YES];
     [self sendExampleNetworkRequests];
     [self sendExampleNetworkRequests];
-    self.repeatingLogExampleTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(sendExampleLogMessage) userInfo:nil repeats:YES];
+//    self.repeatingLogExampleTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(sendExampleLogMessage) userInfo:nil repeats:YES];
 
 
     [[NSUserDefaults standardUserDefaults] setObject:@"foo" forKey:@"FLEXExamplePrefFoo"];
     [[NSUserDefaults standardUserDefaults] setObject:@"foo" forKey:@"FLEXExamplePrefFoo"];
     
     

+ 33 - 1
FLEX.xcodeproj/project.pbxproj

@@ -74,7 +74,7 @@
 		3A4C95231B5B21410088C3F2 /* FLEXFileBrowserSearchOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = 3A4C94A01B5B21410088C3F2 /* FLEXFileBrowserSearchOperation.m */; };
 		3A4C95231B5B21410088C3F2 /* FLEXFileBrowserSearchOperation.m in Sources */ = {isa = PBXBuildFile; fileRef = 3A4C94A01B5B21410088C3F2 /* FLEXFileBrowserSearchOperation.m */; };
 		3A4C95241B5B21410088C3F2 /* FLEXFileBrowserTableViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 3A4C94A11B5B21410088C3F2 /* FLEXFileBrowserTableViewController.h */; settings = {ATTRIBUTES = (Private, ); }; };
 		3A4C95241B5B21410088C3F2 /* FLEXFileBrowserTableViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 3A4C94A11B5B21410088C3F2 /* FLEXFileBrowserTableViewController.h */; settings = {ATTRIBUTES = (Private, ); }; };
 		3A4C95251B5B21410088C3F2 /* FLEXFileBrowserTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3A4C94A21B5B21410088C3F2 /* FLEXFileBrowserTableViewController.m */; };
 		3A4C95251B5B21410088C3F2 /* FLEXFileBrowserTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3A4C94A21B5B21410088C3F2 /* FLEXFileBrowserTableViewController.m */; };
-		3A4C95261B5B21410088C3F2 /* FLEXGlobalsViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 3A4C94A31B5B21410088C3F2 /* FLEXGlobalsViewController.h */; settings = {ATTRIBUTES = (Private, ); }; };
+		3A4C95261B5B21410088C3F2 /* FLEXGlobalsViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = 3A4C94A31B5B21410088C3F2 /* FLEXGlobalsViewController.h */; };
 		3A4C95271B5B21410088C3F2 /* FLEXGlobalsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3A4C94A41B5B21410088C3F2 /* FLEXGlobalsViewController.m */; };
 		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, ); }; };
 		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 */; };
 		3A4C95291B5B21410088C3F2 /* FLEXInstancesViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 3A4C94A61B5B21410088C3F2 /* FLEXInstancesViewController.m */; };
@@ -178,8 +178,14 @@
 		C3531BA223E796BD00A184AD /* FLEXManager+Extensibility.m in Sources */ = {isa = PBXBuildFile; fileRef = C3531BA023E796BD00A184AD /* FLEXManager+Extensibility.m */; };
 		C3531BA223E796BD00A184AD /* FLEXManager+Extensibility.m in Sources */ = {isa = PBXBuildFile; fileRef = C3531BA023E796BD00A184AD /* FLEXManager+Extensibility.m */; };
 		C3531BA523E88A2100A184AD /* FLEXNavigationController.h in Headers */ = {isa = PBXBuildFile; fileRef = C3531BA323E88A2100A184AD /* FLEXNavigationController.h */; };
 		C3531BA523E88A2100A184AD /* FLEXNavigationController.h in Headers */ = {isa = PBXBuildFile; fileRef = C3531BA323E88A2100A184AD /* FLEXNavigationController.h */; };
 		C3531BA623E88A2100A184AD /* FLEXNavigationController.m in Sources */ = {isa = PBXBuildFile; fileRef = C3531BA423E88A2100A184AD /* FLEXNavigationController.m */; };
 		C3531BA623E88A2100A184AD /* FLEXNavigationController.m in Sources */ = {isa = PBXBuildFile; fileRef = C3531BA423E88A2100A184AD /* FLEXNavigationController.m */; };
+		C3531BAA23E88FAC00A184AD /* FLEXTabList.h in Headers */ = {isa = PBXBuildFile; fileRef = C3531BA823E88FAC00A184AD /* FLEXTabList.h */; };
+		C3531BAB23E88FAC00A184AD /* FLEXTabList.m in Sources */ = {isa = PBXBuildFile; fileRef = C3531BA923E88FAC00A184AD /* FLEXTabList.m */; };
 		C362AE8123C7E9D1005A86AE /* NSMapTable+FLEX_Subscripting.h in Headers */ = {isa = PBXBuildFile; fileRef = C362AE7F23C7E9D1005A86AE /* NSMapTable+FLEX_Subscripting.h */; };
 		C362AE8123C7E9D1005A86AE /* NSMapTable+FLEX_Subscripting.h in Headers */ = {isa = PBXBuildFile; fileRef = C362AE7F23C7E9D1005A86AE /* NSMapTable+FLEX_Subscripting.h */; };
 		C362AE8223C7E9D1005A86AE /* NSMapTable+FLEX_Subscripting.m in Sources */ = {isa = PBXBuildFile; fileRef = C362AE8023C7E9D1005A86AE /* NSMapTable+FLEX_Subscripting.m */; };
 		C362AE8223C7E9D1005A86AE /* NSMapTable+FLEX_Subscripting.m in Sources */ = {isa = PBXBuildFile; fileRef = C362AE8023C7E9D1005A86AE /* NSMapTable+FLEX_Subscripting.m */; };
+		C3694DBA23EA1096006625D7 /* FLEXTabsViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = C3694DB823EA1096006625D7 /* FLEXTabsViewController.h */; };
+		C3694DBB23EA1096006625D7 /* FLEXTabsViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = C3694DB923EA1096006625D7 /* FLEXTabsViewController.m */; };
+		C3694DC223EA147F006625D7 /* UIBarButtonItem+FLEX.h in Headers */ = {isa = PBXBuildFile; fileRef = C3694DC023EA147F006625D7 /* UIBarButtonItem+FLEX.h */; };
+		C3694DC323EA147F006625D7 /* UIBarButtonItem+FLEX.m in Sources */ = {isa = PBXBuildFile; fileRef = C3694DC123EA147F006625D7 /* UIBarButtonItem+FLEX.m */; };
 		C36B096523E0D4A1008F5D47 /* UIMenu+FLEX.h in Headers */ = {isa = PBXBuildFile; fileRef = C36B096323E0D4A1008F5D47 /* UIMenu+FLEX.h */; };
 		C36B096523E0D4A1008F5D47 /* UIMenu+FLEX.h in Headers */ = {isa = PBXBuildFile; fileRef = C36B096323E0D4A1008F5D47 /* UIMenu+FLEX.h */; };
 		C36B096623E0D4A1008F5D47 /* UIMenu+FLEX.m in Sources */ = {isa = PBXBuildFile; fileRef = C36B096423E0D4A1008F5D47 /* UIMenu+FLEX.m */; };
 		C36B096623E0D4A1008F5D47 /* UIMenu+FLEX.m in Sources */ = {isa = PBXBuildFile; fileRef = C36B096423E0D4A1008F5D47 /* UIMenu+FLEX.m */; };
 		C36B097023E1EDCD008F5D47 /* FLEXTableViewSection.h in Headers */ = {isa = PBXBuildFile; fileRef = C36B096E23E1EDCD008F5D47 /* FLEXTableViewSection.h */; };
 		C36B097023E1EDCD008F5D47 /* FLEXTableViewSection.h in Headers */ = {isa = PBXBuildFile; fileRef = C36B096E23E1EDCD008F5D47 /* FLEXTableViewSection.h */; };
@@ -501,8 +507,14 @@
 		C3531BA023E796BD00A184AD /* FLEXManager+Extensibility.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "FLEXManager+Extensibility.m"; sourceTree = "<group>"; };
 		C3531BA023E796BD00A184AD /* FLEXManager+Extensibility.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "FLEXManager+Extensibility.m"; sourceTree = "<group>"; };
 		C3531BA323E88A2100A184AD /* FLEXNavigationController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXNavigationController.h; sourceTree = "<group>"; };
 		C3531BA323E88A2100A184AD /* FLEXNavigationController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXNavigationController.h; sourceTree = "<group>"; };
 		C3531BA423E88A2100A184AD /* FLEXNavigationController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXNavigationController.m; sourceTree = "<group>"; };
 		C3531BA423E88A2100A184AD /* FLEXNavigationController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXNavigationController.m; sourceTree = "<group>"; };
+		C3531BA823E88FAC00A184AD /* FLEXTabList.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXTabList.h; sourceTree = "<group>"; };
+		C3531BA923E88FAC00A184AD /* FLEXTabList.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXTabList.m; sourceTree = "<group>"; };
 		C362AE7F23C7E9D1005A86AE /* NSMapTable+FLEX_Subscripting.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSMapTable+FLEX_Subscripting.h"; sourceTree = "<group>"; };
 		C362AE7F23C7E9D1005A86AE /* NSMapTable+FLEX_Subscripting.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSMapTable+FLEX_Subscripting.h"; sourceTree = "<group>"; };
 		C362AE8023C7E9D1005A86AE /* NSMapTable+FLEX_Subscripting.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSMapTable+FLEX_Subscripting.m"; sourceTree = "<group>"; };
 		C362AE8023C7E9D1005A86AE /* NSMapTable+FLEX_Subscripting.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSMapTable+FLEX_Subscripting.m"; sourceTree = "<group>"; };
+		C3694DB823EA1096006625D7 /* FLEXTabsViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FLEXTabsViewController.h; sourceTree = "<group>"; };
+		C3694DB923EA1096006625D7 /* FLEXTabsViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FLEXTabsViewController.m; sourceTree = "<group>"; };
+		C3694DC023EA147F006625D7 /* UIBarButtonItem+FLEX.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIBarButtonItem+FLEX.h"; sourceTree = "<group>"; };
+		C3694DC123EA147F006625D7 /* UIBarButtonItem+FLEX.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIBarButtonItem+FLEX.m"; sourceTree = "<group>"; };
 		C36B096323E0D4A1008F5D47 /* UIMenu+FLEX.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIMenu+FLEX.h"; sourceTree = "<group>"; };
 		C36B096323E0D4A1008F5D47 /* UIMenu+FLEX.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "UIMenu+FLEX.h"; sourceTree = "<group>"; };
 		C36B096423E0D4A1008F5D47 /* UIMenu+FLEX.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIMenu+FLEX.m"; sourceTree = "<group>"; };
 		C36B096423E0D4A1008F5D47 /* UIMenu+FLEX.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIMenu+FLEX.m"; sourceTree = "<group>"; };
 		C36B096E23E1EDCD008F5D47 /* FLEXTableViewSection.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FLEXTableViewSection.h; sourceTree = "<group>"; };
 		C36B096E23E1EDCD008F5D47 /* FLEXTableViewSection.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FLEXTableViewSection.h; sourceTree = "<group>"; };
@@ -963,6 +975,7 @@
 		94A515121C4C9E7B0063292F /* ExplorerInterface */ = {
 		94A515121C4C9E7B0063292F /* ExplorerInterface */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
+				C3531BA723E88FAC00A184AD /* Tabs */,
 				94A515191C4CA1F10063292F /* FLEXExplorerViewController.h */,
 				94A515191C4CA1F10063292F /* FLEXExplorerViewController.h */,
 				94A5151A1C4CA1F10063292F /* FLEXExplorerViewController.m */,
 				94A5151A1C4CA1F10063292F /* FLEXExplorerViewController.m */,
 				94A5151B1C4CA1F10063292F /* FLEXWindow.h */,
 				94A5151B1C4CA1F10063292F /* FLEXWindow.h */,
@@ -995,6 +1008,17 @@
 			path = Globals;
 			path = Globals;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
 		};
 		};
+		C3531BA723E88FAC00A184AD /* Tabs */ = {
+			isa = PBXGroup;
+			children = (
+				C3531BA823E88FAC00A184AD /* FLEXTabList.h */,
+				C3531BA923E88FAC00A184AD /* FLEXTabList.m */,
+				C3694DB823EA1096006625D7 /* FLEXTabsViewController.h */,
+				C3694DB923EA1096006625D7 /* FLEXTabsViewController.m */,
+			);
+			path = Tabs;
+			sourceTree = "<group>";
+		};
 		C36B096723E1E25F008F5D47 /* Carousel */ = {
 		C36B096723E1E25F008F5D47 /* Carousel */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
@@ -1098,6 +1122,8 @@
 				C362AE8023C7E9D1005A86AE /* NSMapTable+FLEX_Subscripting.m */,
 				C362AE8023C7E9D1005A86AE /* NSMapTable+FLEX_Subscripting.m */,
 				C36B096323E0D4A1008F5D47 /* UIMenu+FLEX.h */,
 				C36B096323E0D4A1008F5D47 /* UIMenu+FLEX.h */,
 				C36B096423E0D4A1008F5D47 /* UIMenu+FLEX.m */,
 				C36B096423E0D4A1008F5D47 /* UIMenu+FLEX.m */,
+				C3694DC023EA147F006625D7 /* UIBarButtonItem+FLEX.h */,
+				C3694DC123EA147F006625D7 /* UIBarButtonItem+FLEX.m */,
 			);
 			);
 			path = Categories;
 			path = Categories;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
@@ -1258,6 +1284,7 @@
 				C3A9424523C641C1006871A3 /* SceneKit+Snapshot.h in Headers */,
 				C3A9424523C641C1006871A3 /* SceneKit+Snapshot.h in Headers */,
 				3A4C94EB1B5B21410088C3F2 /* FLEXImagePreviewViewController.h in Headers */,
 				3A4C94EB1B5B21410088C3F2 /* FLEXImagePreviewViewController.h in Headers */,
 				C34D4EB823A2B17900C1F903 /* FLEXBundleShortcuts.h in Headers */,
 				C34D4EB823A2B17900C1F903 /* FLEXBundleShortcuts.h in Headers */,
+				C3694DC223EA147F006625D7 /* UIBarButtonItem+FLEX.h in Headers */,
 				C38F3F31230C958F004E3731 /* FLEXAlert.h in Headers */,
 				C38F3F31230C958F004E3731 /* FLEXAlert.h in Headers */,
 				C398624D23AD6C67007E6793 /* TBKeyPathSearchController.h in Headers */,
 				C398624D23AD6C67007E6793 /* TBKeyPathSearchController.h in Headers */,
 				3A4C95381B5B21410088C3F2 /* FLEXNetworkRecorder.h in Headers */,
 				3A4C95381B5B21410088C3F2 /* FLEXNetworkRecorder.h in Headers */,
@@ -1313,6 +1340,7 @@
 				C3490E1F233BDD73002AE200 /* FLEXSingleRowSection.h in Headers */,
 				C3490E1F233BDD73002AE200 /* FLEXSingleRowSection.h in Headers */,
 				3A4C95281B5B21410088C3F2 /* FLEXInstancesViewController.h in Headers */,
 				3A4C95281B5B21410088C3F2 /* FLEXInstancesViewController.h in Headers */,
 				C36B097023E1EDCD008F5D47 /* FLEXTableViewSection.h in Headers */,
 				C36B097023E1EDCD008F5D47 /* FLEXTableViewSection.h in Headers */,
+				C3531BAA23E88FAC00A184AD /* FLEXTabList.h in Headers */,
 				3A4C950F1B5B21410088C3F2 /* FLEXMethodCallingViewController.h in Headers */,
 				3A4C950F1B5B21410088C3F2 /* FLEXMethodCallingViewController.h in Headers */,
 				3A4C94F51B5B21410088C3F2 /* FLEXArgumentInputObjectView.h in Headers */,
 				3A4C94F51B5B21410088C3F2 /* FLEXArgumentInputObjectView.h in Headers */,
 				C3A9424923C78878006871A3 /* FLEXHierarchyViewController.h in Headers */,
 				C3A9424923C78878006871A3 /* FLEXHierarchyViewController.h in Headers */,
@@ -1337,6 +1365,7 @@
 				C3F31D432267D883003C991A /* FLEXTableView.h in Headers */,
 				C3F31D432267D883003C991A /* FLEXTableView.h in Headers */,
 				3A4C95071B5B21410088C3F2 /* FLEXDefaultEditorViewController.h in Headers */,
 				3A4C95071B5B21410088C3F2 /* FLEXDefaultEditorViewController.h in Headers */,
 				C309B82F223ED64400B228EC /* FLEXLogController.h in Headers */,
 				C309B82F223ED64400B228EC /* FLEXLogController.h in Headers */,
+				C3694DBA23EA1096006625D7 /* FLEXTabsViewController.h in Headers */,
 				C31C4A6923342A2200C35F12 /* FLEXMetadataSection.h in Headers */,
 				C31C4A6923342A2200C35F12 /* FLEXMetadataSection.h in Headers */,
 				C3F977832311B38F0032776D /* NSString+ObjcRuntime.h in Headers */,
 				C3F977832311B38F0032776D /* NSString+ObjcRuntime.h in Headers */,
 				94A5151F1C4CA1F10063292F /* FLEXWindow.h in Headers */,
 				94A5151F1C4CA1F10063292F /* FLEXWindow.h in Headers */,
@@ -1543,6 +1572,7 @@
 				C3878DBA23A749960038FDBE /* FLEXVariableEditorViewController.m in Sources */,
 				C3878DBA23A749960038FDBE /* FLEXVariableEditorViewController.m in Sources */,
 				94AAF0391BAF2E1F00DE8760 /* FLEXKeyboardHelpViewController.m in Sources */,
 				94AAF0391BAF2E1F00DE8760 /* FLEXKeyboardHelpViewController.m in Sources */,
 				C398624E23AD6C67007E6793 /* TBKeyPath.m in Sources */,
 				C398624E23AD6C67007E6793 /* TBKeyPath.m in Sources */,
+				C3694DC323EA147F006625D7 /* UIBarButtonItem+FLEX.m in Sources */,
 				C36FBFD4230F3B98008D95D5 /* FLEXProtocol.m in Sources */,
 				C36FBFD4230F3B98008D95D5 /* FLEXProtocol.m in Sources */,
 				C34D4EB123A2ABD900C1F903 /* FLEXLayerShortcuts.m in Sources */,
 				C34D4EB123A2ABD900C1F903 /* FLEXLayerShortcuts.m in Sources */,
 				C38F3F32230C958F004E3731 /* FLEXAlert.m in Sources */,
 				C38F3F32230C958F004E3731 /* FLEXAlert.m in Sources */,
@@ -1595,6 +1625,7 @@
 				3A4C95311B5B21410088C3F2 /* FLEXSystemLogMessage.m in Sources */,
 				3A4C95311B5B21410088C3F2 /* FLEXSystemLogMessage.m in Sources */,
 				C3F31D422267D883003C991A /* FLEXTableViewCell.m in Sources */,
 				C3F31D422267D883003C991A /* FLEXTableViewCell.m in Sources */,
 				3A4C94F41B5B21410088C3F2 /* FLEXArgumentInputFontView.m in Sources */,
 				3A4C94F41B5B21410088C3F2 /* FLEXArgumentInputFontView.m in Sources */,
+				C3694DBB23EA1096006625D7 /* FLEXTabsViewController.m in Sources */,
 				C3A9424623C641C1006871A3 /* SceneKit+Snapshot.m in Sources */,
 				C3A9424623C641C1006871A3 /* SceneKit+Snapshot.m in Sources */,
 				3A4C953B1B5B21410088C3F2 /* FLEXNetworkSettingsTableViewController.m in Sources */,
 				3A4C953B1B5B21410088C3F2 /* FLEXNetworkSettingsTableViewController.m in Sources */,
 				3A4C94FC1B5B21410088C3F2 /* FLEXArgumentInputStringView.m in Sources */,
 				3A4C94FC1B5B21410088C3F2 /* FLEXArgumentInputStringView.m in Sources */,
@@ -1642,6 +1673,7 @@
 				3A4C95231B5B21410088C3F2 /* FLEXFileBrowserSearchOperation.m in Sources */,
 				3A4C95231B5B21410088C3F2 /* FLEXFileBrowserSearchOperation.m in Sources */,
 				C3490E20233BDD73002AE200 /* FLEXSingleRowSection.m in Sources */,
 				C3490E20233BDD73002AE200 /* FLEXSingleRowSection.m in Sources */,
 				C34D4EB923A2B17900C1F903 /* FLEXBundleShortcuts.m in Sources */,
 				C34D4EB923A2B17900C1F903 /* FLEXBundleShortcuts.m in Sources */,
+				C3531BAB23E88FAC00A184AD /* FLEXTabList.m in Sources */,
 				3A4C952D1B5B21410088C3F2 /* FLEXLiveObjectsTableViewController.m in Sources */,
 				3A4C952D1B5B21410088C3F2 /* FLEXLiveObjectsTableViewController.m in Sources */,
 				C33E46B0223B02CD004BD0E6 /* FLEXASLLogController.m in Sources */,
 				C33E46B0223B02CD004BD0E6 /* FLEXASLLogController.m in Sources */,
 				C398625023AD6C67007E6793 /* FLEXObjcRuntimeViewController.m in Sources */,
 				C398625023AD6C67007E6793 /* FLEXObjcRuntimeViewController.m in Sources */,