Explorar el Código

Initial import of files.

Ryan Olson hace 12 años
padre
commit
1744de4140
Se han modificado 74 ficheros con 5947 adiciones y 0 borrados
  1. 19 0
      Classes/Editing/FLEXArgumentInputView.h
  2. 145 0
      Classes/Editing/FLEXArgumentInputView.m
  3. 17 0
      Classes/Editing/FLEXDefaultEditorViewController.h
  4. 72 0
      Classes/Editing/FLEXDefaultEditorViewController.m
  5. 18 0
      Classes/Editing/FLEXFieldEditorView.h
  6. 178 0
      Classes/Editing/FLEXFieldEditorView.m
  7. 31 0
      Classes/Editing/FLEXFieldEditorViewController.h
  8. 194 0
      Classes/Editing/FLEXFieldEditorViewController.m
  9. 18 0
      Classes/Editing/FLEXIvarEditorViewController.h
  10. 73 0
      Classes/Editing/FLEXIvarEditorViewController.m
  11. 16 0
      Classes/Editing/FLEXMethodCallingViewController.h
  12. 118 0
      Classes/Editing/FLEXMethodCallingViewController.m
  13. 18 0
      Classes/Editing/FLEXPropertyEditorViewController.h
  14. 79 0
      Classes/Editing/FLEXPropertyEditorViewController.m
  15. 49 0
      Classes/Explorer Toolbar/FLEXExplorerToolbar.h
  16. 220 0
      Classes/Explorer Toolbar/FLEXExplorerToolbar.m
  17. 25 0
      Classes/Explorer Toolbar/FLEXExplorerViewController.h
  18. 765 0
      Classes/Explorer Toolbar/FLEXExplorerViewController.m
  19. 20 0
      Classes/Explorer Toolbar/FLEXManager.h
  20. 79 0
      Classes/Explorer Toolbar/FLEXManager.m
  21. 17 0
      Classes/Explorer Toolbar/FLEXToolbarItem.h
  22. 132 0
      Classes/Explorer Toolbar/FLEXToolbarItem.m
  23. 23 0
      Classes/Explorer Toolbar/FLEXWindow.h
  24. 34 0
      Classes/Explorer Toolbar/FLEXWindow.m
  25. 15 0
      Classes/Global State Explorers/FLEXClassesTableViewController.h
  26. 140 0
      Classes/Global State Explorers/FLEXClassesTableViewController.m
  27. 27 0
      Classes/Global State Explorers/FLEXGlobalsTableViewController.h
  28. 226 0
      Classes/Global State Explorers/FLEXGlobalsTableViewController.m
  29. 15 0
      Classes/Global State Explorers/FLEXInstancesTableViewController.h
  30. 69 0
      Classes/Global State Explorers/FLEXInstancesTableViewController.m
  31. 13 0
      Classes/Global State Explorers/FLEXLibrariesTableViewController.h
  32. 116 0
      Classes/Global State Explorers/FLEXLibrariesTableViewController.m
  33. 13 0
      Classes/Global State Explorers/FLEXLiveObjectsTableViewController.h
  34. 220 0
      Classes/Global State Explorers/FLEXLiveObjectsTableViewController.m
  35. 13 0
      Classes/Object Explorers/FLEXArrayExplorerViewController.h
  36. 78 0
      Classes/Object Explorers/FLEXArrayExplorerViewController.m
  37. 13 0
      Classes/Object Explorers/FLEXDefaultsExplorerViewController.h
  38. 73 0
      Classes/Object Explorers/FLEXDefaultsExplorerViewController.m
  39. 15 0
      Classes/Object Explorers/FLEXDescriptionTableViewCell.h
  40. 82 0
      Classes/Object Explorers/FLEXDescriptionTableViewCell.m
  41. 13 0
      Classes/Object Explorers/FLEXDictionaryExplorerViewController.h
  42. 64 0
      Classes/Object Explorers/FLEXDictionaryExplorerViewController.m
  43. 17 0
      Classes/Object Explorers/FLEXObjectExplorerFactory.h
  44. 49 0
      Classes/Object Explorers/FLEXObjectExplorerFactory.m
  45. 29 0
      Classes/Object Explorers/FLEXObjectExplorerViewController.h
  46. 851 0
      Classes/Object Explorers/FLEXObjectExplorerViewController.m
  47. 13 0
      Classes/Object Explorers/FLEXSetExplorerViewController.h
  48. 64 0
      Classes/Object Explorers/FLEXSetExplorerViewController.m
  49. 17 0
      Classes/Utility/FLEXHeapEnumerator.h
  50. 94 0
      Classes/Utility/FLEXHeapEnumerator.m
  51. 22 0
      Classes/Utility/FLEXResources.h
  52. 94 0
      Classes/Utility/FLEXResources.m
  53. 39 0
      Classes/Utility/FLEXRuntimeUtility.h
  54. 589 0
      Classes/Utility/FLEXRuntimeUtility.m
  55. 26 0
      Classes/Utility/FLEXUtility.h
  56. 117 0
      Classes/Utility/FLEXUtility.m
  57. 18 0
      Classes/View Hierarchy/FLEXHierarchyTableViewCell.h
  58. 112 0
      Classes/View Hierarchy/FLEXHierarchyTableViewCell.m
  59. 25 0
      Classes/View Hierarchy/FLEXHierarchyTableViewController.h
  60. 206 0
      Classes/View Hierarchy/FLEXHierarchyTableViewController.m
  61. BIN
      Image Originals/close-icon.png
  62. BIN
      Image Originals/close-icon@2x.png
  63. BIN
      Image Originals/drag-handle.png
  64. BIN
      Image Originals/drag-handle@2x.png
  65. BIN
      Image Originals/globe-icon.png
  66. BIN
      Image Originals/globe-icon@2x.png
  67. BIN
      Image Originals/hierarchy-indent-pattern.png
  68. BIN
      Image Originals/hierarchy-indent-pattern@2x.png
  69. BIN
      Image Originals/list-icon.png
  70. BIN
      Image Originals/list-icon@2x.png
  71. BIN
      Image Originals/move-icon.png
  72. BIN
      Image Originals/move-icon@2x.png
  73. BIN
      Image Originals/select-icon.png
  74. BIN
      Image Originals/select-icon@2x.png

+ 19 - 0
Classes/Editing/FLEXArgumentInputView.h

@@ -0,0 +1,19 @@
+//
+//  FLEXArgumentInputView.h
+//  Flipboard
+//
+//  Created by Ryan Olson on 5/30/14.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+
+@interface FLEXArgumentInputView : UIView
+
+@property (nonatomic, copy) NSString *title;
+@property (nonatomic, copy) NSString *inputText;
+@property (nonatomic, assign) UIKeyboardType keyboardType;
+@property (nonatomic, readonly) BOOL inputViewIsFirstResponder;
+@property (nonatomic, assign) NSUInteger numberOfInputLines;
+
+@end

+ 145 - 0
Classes/Editing/FLEXArgumentInputView.m

@@ -0,0 +1,145 @@
+//
+//  FLEXArgumentInputView.m
+//  Flipboard
+//
+//  Created by Ryan Olson on 5/30/14.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import "FLEXArgumentInputView.h"
+#import "FLEXUtility.h"
+
+@interface FLEXArgumentInputView ()
+
+@property (nonatomic, strong) UILabel *titleLabel;
+@property (nonatomic, strong) UITextView *inputTextView;
+
+@end
+
+@implementation FLEXArgumentInputView
+
+- (id)initWithFrame:(CGRect)frame
+{
+    self = [super initWithFrame:frame];
+    if (self) {
+        // Default to two lines in the text view. Users of the class can customize.
+        self.numberOfInputLines = 2;
+        
+        self.inputTextView = [[UITextView alloc] init];
+        self.inputTextView.font = [[self class] inputFont];
+        self.inputTextView.backgroundColor = [UIColor whiteColor];
+        self.inputTextView.layer.borderColor = [[UIColor blackColor] CGColor];
+        self.inputTextView.layer.borderWidth = 1.0;
+        self.inputTextView.autocapitalizationType = UITextAutocapitalizationTypeNone;
+        self.inputTextView.autocorrectionType = UITextAutocorrectionTypeNo;
+        [self addSubview:self.inputTextView];
+    }
+    return self;
+}
+
+- (void)layoutSubviews
+{
+    CGFloat originY = 0;
+    CGFloat contentWidth = self.bounds.size.width;
+    if ([self.title length] > 0) {
+        CGSize constrainSize = CGSizeMake(contentWidth, CGFLOAT_MAX);
+        CGSize labelSize = [self.titleLabel sizeThatFits:constrainSize];
+        self.titleLabel.frame = CGRectMake(0, originY, labelSize.width, labelSize.height);
+        originY = CGRectGetMaxY(self.titleLabel.frame) + [[self class] titleBottomPadding];
+    }
+    
+    self.inputTextView.frame = CGRectMake(0, originY, contentWidth, [self inputTextViewHeight]);
+}
+
+- (void)setTitle:(NSString *)title
+{
+    if (![_title isEqual:title]) {
+        _title = title;
+        self.titleLabel.text = title;
+        [self setNeedsLayout];
+    }
+}
+
+- (UILabel *)titleLabel
+{
+    if (!_titleLabel) {
+        _titleLabel = [[UILabel alloc] init];
+        _titleLabel.font = [[self class] titleFont];
+        _titleLabel.backgroundColor = self.backgroundColor;
+        _titleLabel.textColor = [UIColor colorWithWhite:0.3 alpha:1.0];
+        _titleLabel.numberOfLines = 0;
+        [self addSubview:_titleLabel];
+    }
+    return _titleLabel;
+}
+
+
+#pragma mark - Text View Passthroughs
+
+- (void)setKeyboardType:(UIKeyboardType)keyboardType
+{
+    self.inputTextView.keyboardType = keyboardType;
+}
+
+- (UIKeyboardType)keyboardType
+{
+    return self.inputTextView.keyboardType;
+}
+
+- (void)setInputText:(NSString *)inputText
+{
+    self.inputTextView.text = inputText;
+}
+
+- (NSString *)inputText
+{
+    return self.inputTextView.text;
+}
+
+- (BOOL)inputViewIsFirstResponder
+{
+    return self.inputTextView.isFirstResponder;
+}
+
+
+#pragma mark - Class Helpers
+
++ (UIFont *)inputFont
+{
+    return [FLEXUtility defaultFontOfSize:14.0];
+}
+
++ (UIFont *)titleFont
+{
+    return [FLEXUtility defaultFontOfSize:12.0];
+}
+
++ (CGFloat)titleBottomPadding
+{
+    return 4.0;
+}
+
+
+#pragma mark - Sizing
+
+- (CGFloat)inputTextViewHeight
+{
+    return ceil([[self class] inputFont].lineHeight * self.numberOfInputLines) + 20.0;
+}
+
+- (CGSize)sizeThatFits:(CGSize)size
+{
+    CGFloat height = 0;
+    
+    if ([self.title length] > 0) {
+        CGSize constrainSize = CGSizeMake(size.width, CGFLOAT_MAX);
+        height += ceil([self.title sizeWithFont:[[self class] titleFont] constrainedToSize:constrainSize].height);
+        height += [[self class] titleBottomPadding];
+    }
+    
+    height += [self inputTextViewHeight];
+    
+    return CGSizeMake(size.width, height);
+}
+
+@end

+ 17 - 0
Classes/Editing/FLEXDefaultEditorViewController.h

@@ -0,0 +1,17 @@
+//
+//  FLEXDefaultEditorViewController.h
+//  Flipboard
+//
+//  Created by Ryan Olson on 5/23/14.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import "FLEXFieldEditorViewController.h"
+
+@interface FLEXDefaultEditorViewController : FLEXFieldEditorViewController
+
+- (id)initWithDefaults:(NSUserDefaults *)defaults key:(NSString *)key;
+
++ (BOOL)canEditDefaultWithValue:(id)currentValue;
+
+@end

+ 72 - 0
Classes/Editing/FLEXDefaultEditorViewController.m

@@ -0,0 +1,72 @@
+//
+//  FLEXDefaultEditorViewController.m
+//  Flipboard
+//
+//  Created by Ryan Olson on 5/23/14.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import "FLEXDefaultEditorViewController.h"
+#import "FLEXFieldEditorView.h"
+#import "FLEXRuntimeUtility.h"
+#import "FLEXArgumentInputView.h"
+
+@interface FLEXDefaultEditorViewController ()
+
+@property (nonatomic, readonly) NSUserDefaults *defaults;
+@property (nonatomic, strong) NSString *key;
+
+@end
+
+@implementation FLEXDefaultEditorViewController
+
+- (id)initWithDefaults:(NSUserDefaults *)defaults key:(NSString *)key
+{
+    self = [super initWithTarget:defaults];
+    if (self) {
+        self.key = key;
+        self.title = @"Edit Default";
+    }
+    return self;
+}
+
+- (NSUserDefaults *)defaults
+{
+    return [self.target isKindOfClass:[NSUserDefaults class]] ? self.target : nil;
+}
+
+- (void)viewDidLoad
+{
+    [super viewDidLoad];
+    
+    self.fieldEditorView.fieldDescription = self.key;
+    
+    [self updateTextFieldString];
+}
+
+- (void)actionButtonPressed:(id)sender
+{
+    [super actionButtonPressed:sender];
+    
+    id value = [FLEXRuntimeUtility objectValueFromEditableString:self.firstInputView.inputText];
+    if (value) {
+        [self.defaults setObject:value forKey:self.key];
+    } else {
+        [self.defaults removeObjectForKey:self.key];
+    }
+    [self.defaults synchronize];
+    [self updateTextFieldString];
+}
+
+- (void)updateTextFieldString
+{
+    id defaultsValue = [self.defaults objectForKey:self.key];
+    self.firstInputView.inputText = [FLEXRuntimeUtility editiableDescriptionForObject:defaultsValue];
+}
+
++ (BOOL)canEditDefaultWithValue:(id)currentValue
+{
+    return !currentValue || [FLEXRuntimeUtility editiableDescriptionForObject:currentValue] != nil;
+}
+
+@end

+ 18 - 0
Classes/Editing/FLEXFieldEditorView.h

@@ -0,0 +1,18 @@
+//
+//  FLEXFieldEditorView.h
+//  Flipboard
+//
+//  Created by Ryan Olson on 5/16/14.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+
+@interface FLEXFieldEditorView : UIView
+
+@property (nonatomic, copy) NSString *targetDescription;
+@property (nonatomic, copy) NSString *fieldDescription;
+
+@property (nonatomic, strong) NSArray *argumentInputViews;
+
+@end

+ 178 - 0
Classes/Editing/FLEXFieldEditorView.m

@@ -0,0 +1,178 @@
+//
+//  FLEXFieldEditorView.m
+//  Flipboard
+//
+//  Created by Ryan Olson on 5/16/14.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import "FLEXFieldEditorView.h"
+#import "FLEXArgumentInputView.h"
+#import "FLEXUtility.h"
+
+@interface FLEXFieldEditorView ()
+
+@property (nonatomic, strong) UILabel *targetDescriptionLabel;
+@property (nonatomic, strong) UIView *targetDescriptionDivider;
+@property (nonatomic, strong) UILabel *fieldDescriptionLabel;
+@property (nonatomic, strong) UIView *fieldDescriptionDivider;
+
+@end
+
+@implementation FLEXFieldEditorView
+
+- (id)initWithFrame:(CGRect)frame
+{
+    self = [super initWithFrame:frame];
+    if (self) {
+        self.targetDescriptionLabel = [[UILabel alloc] init];
+        self.targetDescriptionLabel.numberOfLines = 0;
+        self.targetDescriptionLabel.font = [[self class] labelFont];
+        [self addSubview:self.targetDescriptionLabel];
+        
+        self.targetDescriptionDivider = [[self class] dividerView];
+        [self addSubview:self.targetDescriptionDivider];
+        
+        self.fieldDescriptionLabel = [[UILabel alloc] init];
+        self.fieldDescriptionLabel.numberOfLines = 0;
+        self.fieldDescriptionLabel.font = [[self class] labelFont];
+        [self addSubview:self.fieldDescriptionLabel];
+        
+        self.fieldDescriptionDivider = [[self class] dividerView];
+        [self addSubview:self.fieldDescriptionDivider];
+    }
+    return self;
+}
+
+- (void)layoutSubviews
+{
+    [super layoutSubviews];
+    
+    CGFloat horizontalPadding = [[self class] horizontalPadding];
+    CGFloat verticalPadding = [[self class] verticalPadding];
+    CGFloat dividerLineHeight = [[self class] dividerLineHeight];
+    
+    CGFloat originY = verticalPadding;
+    CGFloat originX = horizontalPadding;
+    CGFloat contentWidth = self.bounds.size.width - 2.0 * horizontalPadding;
+    CGSize constrainSize = CGSizeMake(contentWidth, CGFLOAT_MAX);
+    
+    CGSize instanceDescriptionSize = [self.targetDescriptionLabel sizeThatFits:constrainSize];
+    self.targetDescriptionLabel.frame = CGRectMake(originX, originY, instanceDescriptionSize.width, instanceDescriptionSize.height);
+    originY = CGRectGetMaxY(self.targetDescriptionLabel.frame) + verticalPadding;
+    
+    self.targetDescriptionDivider.frame = CGRectMake(originX, originY, contentWidth, dividerLineHeight);
+    originY = CGRectGetMaxY(self.targetDescriptionDivider.frame) + verticalPadding;
+    
+    CGSize fieldDescriptionSize = [self.fieldDescriptionLabel sizeThatFits:constrainSize];
+    self.fieldDescriptionLabel.frame = CGRectMake(originX, originY, fieldDescriptionSize.width, fieldDescriptionSize.height);
+    originY = CGRectGetMaxY(self.fieldDescriptionLabel.frame) + verticalPadding;
+    
+    self.fieldDescriptionDivider.frame = CGRectMake(originX, originY, contentWidth, dividerLineHeight);
+    originY = CGRectGetMaxY(self.fieldDescriptionDivider.frame) + verticalPadding;
+
+    for (UIView *argumentInputView in self.argumentInputViews) {
+        CGSize inputViewSize = [argumentInputView sizeThatFits:constrainSize];
+        argumentInputView.frame = CGRectMake(originX, originY, inputViewSize.width, inputViewSize.height);
+        originY = CGRectGetMaxY(argumentInputView.frame) + verticalPadding;
+    }
+}
+
+- (void)setTargetDescription:(NSString *)targetDescription
+{
+    if (![_targetDescription isEqual:targetDescription]) {
+        _targetDescription = targetDescription;
+        self.targetDescriptionLabel.text = targetDescription;
+        [self setNeedsLayout];
+    }
+}
+
+- (void)setFieldDescription:(NSString *)fieldDescription
+{
+    if (![_fieldDescription isEqual:fieldDescription]) {
+        _fieldDescription = fieldDescription;
+        self.fieldDescriptionLabel.text = fieldDescription;
+        [self setNeedsLayout];
+    }
+}
+
+- (void)setArgumentInputViews:(NSArray *)argumentInputViews
+{
+    if (![_argumentInputViews isEqual:argumentInputViews]) {
+        
+        for (FLEXArgumentInputView *inputView in _argumentInputViews) {
+            [inputView removeFromSuperview];
+        }
+        
+        _argumentInputViews = argumentInputViews;
+        
+        for (FLEXArgumentInputView *newInputView in argumentInputViews) {
+            [self addSubview:newInputView];
+        }
+        
+        [self setNeedsLayout];
+    }
+}
+
++ (UIView *)dividerView
+{
+    UIView *dividerView = [[UIView alloc] init];
+    dividerView.backgroundColor = [self dividerColor];
+    return dividerView;
+}
+
++ (UIColor *)dividerColor
+{
+    return [UIColor lightGrayColor];
+}
+
++ (CGFloat)horizontalPadding
+{
+    return 10.0;
+}
+
++ (CGFloat)verticalPadding
+{
+    return 20.0;
+}
+
++ (UIFont *)labelFont
+{
+    return [FLEXUtility defaultFontOfSize:14.0];
+}
+
++ (CGFloat)dividerLineHeight
+{
+    return 1.0;
+}
+
+- (CGSize)sizeThatFits:(CGSize)size
+{
+    CGFloat horizontalPadding = [[self class] horizontalPadding];
+    CGFloat verticalPadding = [[self class] verticalPadding];
+    UIFont *font = [[self class] labelFont];
+    CGFloat dividerLineHeight = [[self class] dividerLineHeight];
+    
+    CGFloat height = 0;
+    CGFloat availableWidth = size.width - 2.0 * horizontalPadding;
+    CGSize constrainSize = CGSizeMake(availableWidth, CGFLOAT_MAX);
+    
+    height += verticalPadding;
+    height += ceil([self.targetDescription sizeWithFont:font constrainedToSize:constrainSize].height);
+    height += verticalPadding;
+    height += dividerLineHeight;
+    height += verticalPadding;
+    height += ceil([self.fieldDescription sizeWithFont:font constrainedToSize:constrainSize].height);
+    height += verticalPadding;
+    height += dividerLineHeight;
+    height += verticalPadding;
+    
+    for (FLEXArgumentInputView *inputView in self.argumentInputViews) {
+        height += [inputView sizeThatFits:constrainSize].height;
+        height += verticalPadding;
+    }
+    
+    return CGSizeMake(size.width, height);
+}
+
+@end

+ 31 - 0
Classes/Editing/FLEXFieldEditorViewController.h

@@ -0,0 +1,31 @@
+//
+//  FLEXFieldEditorViewController.h
+//  Flipboard
+//
+//  Created by Ryan Olson on 5/16/14.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+
+@class FLEXFieldEditorView;
+@class FLEXArgumentInputView;
+
+@interface FLEXFieldEditorViewController : UIViewController
+
+- (id)initWithTarget:(id)target;
+
++ (BOOL)canEditType:(NSString *)typeEncoding currentObjectValue:(id)value;
++ (NSString *)stringTypeEncoding;
+
+// Convenience accessor since many subclasses only use one input view
+@property (nonatomic, readonly) FLEXArgumentInputView *firstInputView;
+
+// For subclass use only.
+@property (nonatomic, strong, readonly) id target;
+@property (nonatomic, strong, readonly) FLEXFieldEditorView *fieldEditorView;
+@property (nonatomic, strong, readonly) UIBarButtonItem *setterButton;
+- (void)actionButtonPressed:(id)sender;
+- (NSString *)titleForActionButton;
+
+@end

+ 194 - 0
Classes/Editing/FLEXFieldEditorViewController.m

@@ -0,0 +1,194 @@
+//
+//  FLEXFieldEditorViewController.m
+//  Flipboard
+//
+//  Created by Ryan Olson on 5/16/14.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import "FLEXFieldEditorViewController.h"
+#import "FLEXFieldEditorView.h"
+#import "FLEXRuntimeUtility.h"
+#import "FLEXUtility.h"
+#import "FLEXArgumentInputView.h"
+
+@interface FLEXFieldEditorViewController () <UIScrollViewDelegate>
+
+@property (nonatomic, strong) UIScrollView *scrollView;
+
+@property (nonatomic, strong, readwrite) id target;
+@property (nonatomic, strong, readwrite) FLEXFieldEditorView *fieldEditorView;
+@property (nonatomic, strong, readwrite) UIBarButtonItem *setterButton;
+
+@end
+
+@implementation FLEXFieldEditorViewController
+
+- (id)initWithTarget:(id)target
+{
+    self = [super initWithNibName:nil bundle:nil];
+    if (self) {
+        self.target = target;
+        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardDidShow:) name:UIKeyboardDidShowNotification object:nil];
+        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil];
+    }
+    return self;
+}
+
+- (void)dealloc
+{
+    [[NSNotificationCenter defaultCenter] removeObserver:self];
+}
+
+- (void)keyboardDidShow:(NSNotification *)notification
+{
+    CGSize keyboardSize = [[[notification userInfo] objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue].size;
+    UIEdgeInsets scrollInsets = self.scrollView.contentInset;
+    scrollInsets.bottom = keyboardSize.height;
+    self.scrollView.contentInset = scrollInsets;
+    self.scrollView.scrollIndicatorInsets = scrollInsets;
+    
+    // Find the active input view and scroll to make sure it's visible.
+    for (FLEXArgumentInputView *argumentInputView in self.fieldEditorView.argumentInputViews) {
+        if (argumentInputView.inputViewIsFirstResponder) {
+            CGRect scrollToVisibleRect = [self.scrollView convertRect:argumentInputView.bounds fromView:argumentInputView];
+            [self.scrollView scrollRectToVisible:scrollToVisibleRect animated:YES];
+            break;
+        }
+    }
+}
+
+- (void)keyboardWillHide:(NSNotification *)notification
+{
+    UIEdgeInsets scrollInsets = self.scrollView.contentInset;
+    scrollInsets.bottom = 0.0;
+    self.scrollView.contentInset = scrollInsets;
+    self.scrollView.scrollIndicatorInsets = scrollInsets;
+}
+
+- (BOOL)prefersStatusBarHidden
+{
+    return YES;
+}
+
+- (void)viewDidLoad
+{
+    [super viewDidLoad];
+    
+    self.view.backgroundColor = [FLEXUtility scrollViewGrayColor];
+    
+    self.scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds];
+    self.scrollView.backgroundColor = self.view.backgroundColor;
+    self.scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
+    self.scrollView.delegate = self;
+    [self.view addSubview:self.scrollView];
+    
+    self.fieldEditorView = [[FLEXFieldEditorView alloc] init];
+    self.fieldEditorView.backgroundColor = self.view.backgroundColor;
+    self.fieldEditorView.targetDescription = [NSString stringWithFormat:@"%@ %p", [self.target class], self.target];
+    [self.scrollView addSubview:self.fieldEditorView];
+    
+    // One argument input view by default. Subclasses can configure the field editor view with more/different argument input views if needed.
+    FLEXArgumentInputView *inputView = [[FLEXArgumentInputView alloc] init];
+    inputView.backgroundColor = self.view.backgroundColor;
+    inputView.numberOfInputLines = 8;
+    self.fieldEditorView.argumentInputViews = @[inputView];
+    
+    self.setterButton = [[UIBarButtonItem alloc] initWithTitle:[self titleForActionButton] style:UIBarButtonItemStyleDone target:self action:@selector(actionButtonPressed:)];
+    self.navigationItem.rightBarButtonItem = self.setterButton;
+}
+
+- (void)viewWillLayoutSubviews
+{
+    CGSize constrainSize = CGSizeMake(self.scrollView.bounds.size.width, CGFLOAT_MAX);
+    CGSize fieldEditorSize = [self.fieldEditorView sizeThatFits:constrainSize];
+    self.fieldEditorView.frame = CGRectMake(0, 0, fieldEditorSize.width, fieldEditorSize.height);
+    self.scrollView.contentSize = fieldEditorSize;
+}
+
+- (FLEXArgumentInputView *)firstInputView
+{
+    return [[self.fieldEditorView argumentInputViews] firstObject];
+}
+
+- (void)actionButtonPressed:(id)sender
+{
+    // Subclasses can override
+    [self.fieldEditorView endEditing:YES];
+}
+
+- (NSString *)titleForActionButton
+{
+    // Subclasses can override.
+    return @"Set";
+}
+
++ (BOOL)canEditType:(NSString *)typeEncoding currentObjectValue:(id)value
+{
+    // Many primitive types can always be edited (numbers and supported structs).
+    static NSArray *primitiveTypes = nil;
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        primitiveTypes = @[@(@encode(char)),
+                           @(@encode(int)),
+                           @(@encode(short)),
+                           @(@encode(long)),
+                           @(@encode(long long)),
+                           @(@encode(unsigned char)),
+                           @(@encode(unsigned int)),
+                           @(@encode(unsigned short)),
+                           @(@encode(unsigned long)),
+                           @(@encode(unsigned long long)),
+                           @(@encode(float)),
+                           @(@encode(double)),
+                           @(@encode(CGRect)),
+                           @(@encode(CGSize)),
+                           @(@encode(CGPoint)),
+                           @(@encode(CGAffineTransform)),
+                           @(@encode(NSRange)),
+                           @(@encode(UIEdgeInsets)),
+                           @(@encode(UIOffset))];
+    });
+    
+    BOOL canEdit = [primitiveTypes containsObject:typeEncoding];
+    
+    // Object types may be
+    if (!canEdit) {
+        if (value) {
+            // If the current value is non-nil and we can represent it with an editable string, then we can suppor editing.
+            canEdit = canEdit || [FLEXRuntimeUtility editiableDescriptionForObject:value] != nil;
+        } else {
+            // Also always edit types that are explicitly typed NSString, NSNumber, NSArray, or NSDictionary and are nil.
+            // This kind of type encoding is only kept by ivars and properties. Method agruments and return types drop the class from the type encoding.
+            // The editor supports populating these types through NSJSONSerilization parsing of the input string.
+            canEdit = canEdit || [typeEncoding isEqual:[self stringTypeEncoding]];
+            canEdit = canEdit || [typeEncoding isEqual:[self numberTypeEncoding]];
+            canEdit = canEdit || [typeEncoding isEqual:[self arrayTypeEncoding]];
+            canEdit = canEdit || [typeEncoding isEqual:[self dictionaryTypeEncoding]];
+        }
+    }
+    
+    return canEdit;
+}
+
++ (NSString *)stringTypeEncoding
+{
+    return @"@\"NSString\"";
+}
+
++ (NSString *)numberTypeEncoding
+{
+    return @"@\"NSNumber\"";
+}
+
++ (NSString *)arrayTypeEncoding
+{
+    return @"@\"NSArray\"";
+}
+
++ (NSString *)dictionaryTypeEncoding
+{
+    return @"@\"NSDictionary\"";
+}
+
+@end

+ 18 - 0
Classes/Editing/FLEXIvarEditorViewController.h

@@ -0,0 +1,18 @@
+//
+//  FLEXIvarEditorViewController.h
+//  Flipboard
+//
+//  Created by Ryan Olson on 5/23/14.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import "FLEXFieldEditorViewController.h"
+#import <objc/runtime.h>
+
+@interface FLEXIvarEditorViewController : FLEXFieldEditorViewController
+
+- (id)initWithTarget:(id)target ivar:(Ivar)ivar;
+
++ (BOOL)canEditIvar:(Ivar)ivar currentValue:(id)value;
+
+@end

+ 73 - 0
Classes/Editing/FLEXIvarEditorViewController.m

@@ -0,0 +1,73 @@
+//
+//  FLEXIvarEditorViewController.m
+//  Flipboard
+//
+//  Created by Ryan Olson on 5/23/14.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import "FLEXIvarEditorViewController.h"
+#import "FLEXFieldEditorView.h"
+#import "FLEXRuntimeUtility.h"
+#import "FLEXArgumentInputView.h"
+
+@interface FLEXIvarEditorViewController ()
+
+@property (nonatomic, assign) Ivar ivar;
+
+@end
+
+@implementation FLEXIvarEditorViewController
+
+- (id)initWithTarget:(id)target ivar:(Ivar)ivar
+{
+    self = [super initWithTarget:target];
+    if (self) {
+        self.ivar = ivar;
+        self.title = @"Instance Variable";
+    }
+    return self;
+}
+
+- (void)viewDidLoad
+{
+    [super viewDidLoad];
+    
+    self.fieldEditorView.fieldDescription = [FLEXRuntimeUtility prettyNameForIvar:self.ivar];
+    
+    [self updateTextFieldString];
+    
+    // Use the numeric keyboard for primitives and the letter keyboard for strings
+    NSString *typeEncoding = @(ivar_getTypeEncoding(self.ivar));
+    if ([typeEncoding isEqual:[[self class] stringTypeEncoding]]) {
+        self.firstInputView.keyboardType = UIKeyboardTypeAlphabet;
+    } else {
+        self.firstInputView.keyboardType = UIKeyboardTypeNumbersAndPunctuation;
+    }
+}
+
+- (void)actionButtonPressed:(id)sender
+{
+    [super actionButtonPressed:sender];
+    
+    [self updatePropertyFromString:self.firstInputView.inputText];
+    [self updateTextFieldString];
+}
+
+- (void)updateTextFieldString
+{
+    id ivarValue = [FLEXRuntimeUtility valueForIvar:self.ivar onObject:self.target];
+    self.firstInputView.inputText = [FLEXRuntimeUtility editiableDescriptionForObject:ivarValue];
+}
+
+- (void)updatePropertyFromString:(NSString *)string
+{
+    [FLEXRuntimeUtility setIvar:self.ivar onObject:self.target withInputString:self.firstInputView.inputText];
+}
+
++ (BOOL)canEditIvar:(Ivar)ivar currentValue:(id)value
+{
+    return [self canEditType:@(ivar_getTypeEncoding(ivar)) currentObjectValue:value];
+}
+
+@end

+ 16 - 0
Classes/Editing/FLEXMethodCallingViewController.h

@@ -0,0 +1,16 @@
+//
+//  FLEXMethodCallingViewController.h
+//  Flipboard
+//
+//  Created by Ryan Olson on 5/23/14.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import "FLEXFieldEditorViewController.h"
+#import <objc/runtime.h>
+
+@interface FLEXMethodCallingViewController : FLEXFieldEditorViewController
+
+- (id)initWithTarget:(id)target method:(Method)method;
+
+@end

+ 118 - 0
Classes/Editing/FLEXMethodCallingViewController.m

@@ -0,0 +1,118 @@
+//
+//  FLEXMethodCallingViewController.m
+//  Flipboard
+//
+//  Created by Ryan Olson on 5/23/14.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import "FLEXMethodCallingViewController.h"
+#import "FLEXRuntimeUtility.h"
+#import "FLEXFieldEditorView.h"
+#import "FLEXObjectExplorerFactory.h"
+#import "FLEXObjectExplorerViewController.h"
+#import "FLEXArgumentInputView.h"
+
+@interface FLEXMethodCallingViewController ()
+
+@property (nonatomic, assign) Method method;
+
+@end
+
+@implementation FLEXMethodCallingViewController
+
+- (id)initWithTarget:(id)target method:(Method)method
+{
+    self = [super initWithTarget:target];
+    if (self) {
+        self.method = method;
+        self.title = [self isClassMethod] ? @"Class Method" : @"Method";
+    }
+    return self;
+}
+
+- (void)viewDidLoad
+{
+    [super viewDidLoad];
+    
+    self.fieldEditorView.fieldDescription = [FLEXRuntimeUtility prettyNameForMethod:self.method isClassMethod:[self isClassMethod]];
+    
+    NSArray *methodComponents = [FLEXRuntimeUtility prettyArgumentComponentsForMethod:self.method];
+    NSMutableArray *argumentInputViews = [NSMutableArray array];
+    unsigned int argumentIndex = kFLEXNumberOfImplicitArgs;
+    for (NSString *methodComponent in methodComponents) {
+        FLEXArgumentInputView *inputView = [[FLEXArgumentInputView alloc] init];
+        inputView.backgroundColor = self.view.backgroundColor;
+        inputView.title = methodComponent;
+        
+        // Prepopulate the structs that we parse from strings with the default values.
+        // This shows the intended formatting which would be less clear otherwise.
+        char *argumentTypeEncoding = method_copyArgumentType(self.method, argumentIndex);
+        NSValue *defaultValue = [[self class] defaultValueForEncoding:argumentTypeEncoding];
+        if (defaultValue) {
+            inputView.inputText = [FLEXRuntimeUtility editiableDescriptionForObject:defaultValue];
+        }
+        free(argumentTypeEncoding);
+        
+        [argumentInputViews addObject:inputView];
+    }
+    self.fieldEditorView.argumentInputViews = argumentInputViews;
+}
+
+- (BOOL)isClassMethod
+{
+    return self.target && self.target == [self.target class];
+}
+
+- (NSString *)titleForActionButton
+{
+    return @"Call";
+}
+
+- (void)actionButtonPressed:(id)sender
+{
+    [super actionButtonPressed:sender];
+    
+    NSMutableArray *arguments = [NSMutableArray array];
+    for (FLEXArgumentInputView *inputView in self.fieldEditorView.argumentInputViews) {
+        NSString *argumentString = inputView.inputText;
+        if (!argumentString) {
+            // Use empty string as a placeholder in the array. It will be interpreted as nil.
+            argumentString = @"";
+        }
+        [arguments addObject:argumentString];
+    }
+    
+    id returnedObject = [FLEXRuntimeUtility performSelector:method_getName(self.method) onObject:self.target withArguments:arguments error:NULL];
+    
+    // For non-nil (or void) return types, push an explorer view controller to display the returned object
+    if (returnedObject) {
+        FLEXObjectExplorerViewController *explorerViewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:returnedObject];
+        [self.navigationController pushViewController:explorerViewController animated:YES];
+    }
+}
+
++ (NSValue *)defaultValueForEncoding:(const char *)typeEncoding
+{
+    NSValue *value = nil;
+    
+    if (strcmp(typeEncoding, @encode(CGRect)) == 0) {
+        value = [NSValue valueWithCGRect:CGRectZero];
+    } else if (strcmp(typeEncoding, @encode(CGSize)) == 0) {
+        value = [NSValue valueWithCGPoint:CGPointZero];
+    } else if (strcmp(typeEncoding, @encode(CGPoint)) == 0) {
+        value = [NSValue valueWithCGSize:CGSizeZero];
+    } else if (strcmp(typeEncoding, @encode(CGAffineTransform)) == 0) {
+        value = [NSValue valueWithCGAffineTransform:CGAffineTransformIdentity];
+    } else if (strcmp(typeEncoding, @encode(NSRange)) == 0) {
+        value = [NSValue valueWithRange:NSMakeRange(0, 0)];
+    } else if (strcmp(typeEncoding, @encode(UIEdgeInsets)) == 0) {
+        value = [NSValue valueWithUIEdgeInsets:UIEdgeInsetsZero];
+    } else if (strcmp(typeEncoding, @encode(UIOffset)) == 0) {
+        value = [NSValue valueWithUIOffset:UIOffsetZero];
+    }
+    
+    return value;
+}
+
+@end

+ 18 - 0
Classes/Editing/FLEXPropertyEditorViewController.h

@@ -0,0 +1,18 @@
+//
+//  FLEXPropertyEditorViewController.h
+//  Flipboard
+//
+//  Created by Ryan Olson on 5/20/14.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import "FLEXFieldEditorViewController.h"
+#import <objc/runtime.h>
+
+@interface FLEXPropertyEditorViewController : FLEXFieldEditorViewController
+
+- (id)initWithTarget:(id)target property:(objc_property_t)property;
+
++ (BOOL)canEditProperty:(objc_property_t)property currentValue:(id)value;
+
+@end

+ 79 - 0
Classes/Editing/FLEXPropertyEditorViewController.m

@@ -0,0 +1,79 @@
+//
+//  FLEXPropertyEditorViewController.m
+//  Flipboard
+//
+//  Created by Ryan Olson on 5/20/14.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import "FLEXPropertyEditorViewController.h"
+#import "FLEXRuntimeUtility.h"
+#import "FLEXFieldEditorView.h"
+#import "FLEXArgumentInputView.h"
+
+@interface FLEXPropertyEditorViewController ()
+
+@property (nonatomic, assign) objc_property_t property;
+
+@end
+
+@implementation FLEXPropertyEditorViewController
+
+- (id)initWithTarget:(id)target property:(objc_property_t)property
+{
+    self = [super initWithTarget:target];
+    if (self) {
+        self.property = property;
+        self.title = @"Property";
+    }
+    return self;
+}
+
+- (void)viewDidLoad
+{
+    [super viewDidLoad];
+    
+    self.fieldEditorView.fieldDescription = [FLEXRuntimeUtility fullDescriptionForProperty:self.property];
+    id currentValue = [FLEXRuntimeUtility valueForProperty:self.property onObject:self.target];
+    self.setterButton.enabled = [[self class] canEditProperty:self.property currentValue:currentValue];
+    
+    [self updateTextFieldString];
+    
+    // Use the numeric keyboard for primitives and the letter keyboard for strings
+    NSString *typeEncoding = [FLEXRuntimeUtility typeEncodingForProperty:self.property];
+    if ([typeEncoding isEqual:[[self class] stringTypeEncoding]]) {
+        self.firstInputView.keyboardType = UIKeyboardTypeAlphabet;
+    } else {
+        self.firstInputView.keyboardType = UIKeyboardTypeNumbersAndPunctuation;
+    }
+}
+
+- (void)actionButtonPressed:(id)sender
+{
+    [super actionButtonPressed:sender];
+    
+    [self updatePropertyFromString:self.firstInputView.inputText];
+    [self updateTextFieldString];
+}
+
+- (void)updateTextFieldString
+{
+    id propertyValue = [FLEXRuntimeUtility valueForProperty:self.property onObject:self.target];
+    self.firstInputView.inputText = [FLEXRuntimeUtility editiableDescriptionForObject:propertyValue];
+}
+
+- (void)updatePropertyFromString:(NSString *)string
+{
+    SEL setterSelector = [FLEXRuntimeUtility setterSelectorForProperty:self.property];
+    NSArray *arguments = string ? @[string] : nil;
+    [FLEXRuntimeUtility performSelector:setterSelector onObject:self.target withArguments:arguments error:NULL];
+}
+
++ (BOOL)canEditProperty:(objc_property_t)property currentValue:(id)value
+{
+    BOOL canEditType = [self canEditType:[FLEXRuntimeUtility typeEncodingForProperty:property] currentObjectValue:value];
+    BOOL isReadonly = [FLEXRuntimeUtility isReadonlyProperty:property];
+    return canEditType && !isReadonly;
+}
+
+@end

+ 49 - 0
Classes/Explorer Toolbar/FLEXExplorerToolbar.h

@@ -0,0 +1,49 @@
+//
+//  FLEXExplorerToolbar.h
+//  Flipboard
+//
+//  Created by Ryan Olson on 4/4/14.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+
+@class FLEXToolbarItem;
+
+@interface FLEXExplorerToolbar : UIView
+
+/// Toolbar item for selecting views.
+/// Users of the toolbar can configure the enabled/selected state and event targets/actions.
+@property (nonatomic, strong, readonly) FLEXToolbarItem *selectItem;
+
+/// Toolbar item for presenting a list with the view hierarchy.
+/// Users of the toolbar can configure the enabled state and event targets/actions.
+@property (nonatomic, strong, readonly) FLEXToolbarItem *hierarchyItem;
+
+/// Toolbar item for moving views.
+/// Users of the toolbar can configure the enabled/selected state and event targets/actions.
+@property (nonatomic, strong, readonly) FLEXToolbarItem *moveItem;
+
+/// Toolbar item for inspecting details of the selected view.
+/// Users of the toolbar can configure the enabled state and event targets/actions.
+@property (nonatomic, strong, readonly) FLEXToolbarItem *globalsItem;
+
+/// Toolbar item for hiding the explorer.
+/// Users of the toolbar can configure the event targets/actions.
+@property (nonatomic, strong, readonly) FLEXToolbarItem *closeItem;
+
+/// A view for moving the entire toolbar.
+/// Users of the toolbar can attach a pan gesture recognizer to decide how to reposition the toolbar.
+@property (nonatomic, strong, readonly) UIView *dragHandle;
+
+/// A color matching the overlay on color on the selected view.
+@property (nonatomic, strong) UIColor *selectedViewOverlayColor;
+
+/// Description text for the selected view displayed below the toolbar items.
+@property (nonatomic, copy) NSString *selectedViewDescription;
+
+/// Area where details of the selected view are shown
+/// Users of the toolbar can attach a tap gesture recognizer to show additional details.
+@property (nonatomic, strong, readonly) UIView *selectedViewDescriptionContainer;
+
+@end

+ 220 - 0
Classes/Explorer Toolbar/FLEXExplorerToolbar.m

@@ -0,0 +1,220 @@
+//
+//  FLEXExplorerToolbar.m
+//  Flipboard
+//
+//  Created by Ryan Olson on 4/4/14.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import "FLEXExplorerToolbar.h"
+#import "FLEXToolbarItem.h"
+#import "FLEXResources.h"
+
+@interface FLEXExplorerToolbar ()
+
+@property (nonatomic, strong, readwrite) FLEXToolbarItem *selectItem;
+@property (nonatomic, strong, readwrite) FLEXToolbarItem *moveItem;
+@property (nonatomic, strong, readwrite) FLEXToolbarItem *globalsItem;
+@property (nonatomic, strong, readwrite) FLEXToolbarItem *closeItem;
+@property (nonatomic, strong, readwrite) FLEXToolbarItem *hierarchyItem;
+@property (nonatomic, strong, readwrite) UIView *dragHandle;
+
+@property (nonatomic, strong) UIImageView *dragHandleImageView;
+
+@property (nonatomic, strong) NSArray *toolbarItems;
+
+@property (nonatomic, strong) UIView *selectedViewDescriptionContainer;
+@property (nonatomic, strong) UIView *selectedViewColorIndicator;
+@property (nonatomic, strong) UILabel *selectedViewDescriptionLabel;
+
+@end
+
+@implementation FLEXExplorerToolbar
+
+- (id)initWithFrame:(CGRect)frame
+{
+    self = [super initWithFrame:frame];
+    if (self) {
+        NSMutableArray *toolbarItems = [NSMutableArray array];
+        
+        self.dragHandle = [[UIView alloc] init];
+        self.dragHandle.backgroundColor = [FLEXToolbarItem defaultBackgroundColor];
+        [self addSubview:self.dragHandle];
+        
+        UIImage *dragHandle = [FLEXResources dragHandle];
+        self.dragHandleImageView = [[UIImageView alloc] initWithImage:dragHandle];
+        [self.dragHandle addSubview:self.dragHandleImageView];
+        
+        UIImage *globalsIcon = [FLEXResources globeIcon];
+        self.globalsItem = [FLEXToolbarItem toolbarItemWithTitle:@"globals" image:globalsIcon];
+        [self addSubview:self.globalsItem];
+        [toolbarItems addObject:self.globalsItem];
+        
+        UIImage *listIcon = [FLEXResources listIcon];
+        self.hierarchyItem = [FLEXToolbarItem toolbarItemWithTitle:@"views" image:listIcon];
+        [self addSubview:self.hierarchyItem];
+        [toolbarItems addObject:self.hierarchyItem];
+        
+        UIImage *selectIcon = [FLEXResources selectIcon];
+        self.selectItem = [FLEXToolbarItem toolbarItemWithTitle:@"select" image:selectIcon];
+        [self addSubview:self.selectItem];
+        [toolbarItems addObject:self.selectItem];
+        
+        UIImage *moveIcon = [FLEXResources moveIcon];
+        self.moveItem = [FLEXToolbarItem toolbarItemWithTitle:@"move" image:moveIcon];
+        [self addSubview:self.moveItem];
+        [toolbarItems addObject:self.moveItem];
+        
+        UIImage *closeIcon = [FLEXResources closeIcon];
+        self.closeItem = [FLEXToolbarItem toolbarItemWithTitle:@"close" image:closeIcon];
+        [self addSubview:self.closeItem];
+        [toolbarItems addObject:self.closeItem];
+        
+        self.toolbarItems = toolbarItems;
+        self.backgroundColor = [UIColor clearColor];
+        
+        self.selectedViewDescriptionContainer = [[UIView alloc] init];
+        self.selectedViewDescriptionContainer.backgroundColor = [UIColor colorWithWhite:0.9 alpha:0.95];
+        self.selectedViewDescriptionContainer.hidden = YES;
+        [self addSubview:self.selectedViewDescriptionContainer];
+        
+        self.selectedViewColorIndicator = [[UIView alloc] init];
+        self.selectedViewColorIndicator.backgroundColor = [UIColor redColor];
+        [self.selectedViewDescriptionContainer addSubview:self.selectedViewColorIndicator];
+        
+        self.selectedViewDescriptionLabel = [[UILabel alloc] init];
+        self.selectedViewDescriptionLabel.backgroundColor = [UIColor clearColor];
+        self.selectedViewDescriptionLabel.font = [[self class] descriptionLabelFont];
+        [self.selectedViewDescriptionContainer addSubview:self.selectedViewDescriptionLabel];
+    }
+    return self;
+}
+
+- (void)layoutSubviews
+{
+    [super layoutSubviews];
+    
+    // Drag Handle
+    const CGFloat kToolbarItemHeight = [[self class] toolbarItemHeight];
+    self.dragHandle.frame = CGRectMake(self.bounds.origin.x, self.bounds.origin.y, [[self class] dragHandleWidth], kToolbarItemHeight);
+    CGRect dragHandleImageFrame = self.dragHandleImageView.frame;
+    dragHandleImageFrame.origin.x = floor((self.dragHandle.frame.size.width - dragHandleImageFrame.size.width) / 2.0);
+    dragHandleImageFrame.origin.y = floor((self.dragHandle.frame.size.height - dragHandleImageFrame.size.height) / 2.0);
+    self.dragHandleImageView.frame = dragHandleImageFrame;
+    
+    
+    // Toolbar Items
+    CGFloat originX = CGRectGetMaxX(self.dragHandle.frame);
+    CGFloat originY = self.bounds.origin.y;
+    CGFloat height = kToolbarItemHeight;
+    CGFloat width = floor((CGRectGetMaxX(self.bounds) - originX) / [self.toolbarItems count]);
+    for (UIView *toolbarItem in self.toolbarItems) {
+        toolbarItem.frame = CGRectMake(originX, originY, width, height);
+        originX = CGRectGetMaxX(toolbarItem.frame);
+    }
+    
+    
+    const CGFloat kSelectedViewColorDiameter = [[self class] selectedViewColorIndicatorDiameter];
+    const CGFloat kDescriptionLabelHeight = [[self class] descriptionLabelHeight];
+    const CGFloat kHorizontalPadding = [[self class] horizontalPadding];
+    const CGFloat kDescriptionVerticalPadding = [[self class] descriptionVerticalPadding];
+    const CGFloat kDescriptionContainerHeight = [[self class] descriptionContainerHeight];
+    
+    CGRect descriptionContainerFrame = CGRectZero;
+    descriptionContainerFrame.size.height = kDescriptionContainerHeight;
+    descriptionContainerFrame.origin.y = CGRectGetMaxY(self.bounds) - kDescriptionContainerHeight;
+    descriptionContainerFrame.size.width = self.bounds.size.width;
+    self.selectedViewDescriptionContainer.frame = descriptionContainerFrame;
+    
+    // Selected View Color
+    CGRect selectedViewColorFrame = CGRectZero;
+    selectedViewColorFrame.size.width = kSelectedViewColorDiameter;
+    selectedViewColorFrame.size.height = kSelectedViewColorDiameter;
+    selectedViewColorFrame.origin.x = kHorizontalPadding;
+    selectedViewColorFrame.origin.y = floor((kDescriptionContainerHeight - kSelectedViewColorDiameter) / 2.0);
+    self.selectedViewColorIndicator.frame = selectedViewColorFrame;
+    self.selectedViewColorIndicator.layer.cornerRadius = ceil(selectedViewColorFrame.size.height / 2.0);
+    
+    // Selected View Description
+    CGRect descriptionLabelFrame = CGRectZero;
+    CGFloat descriptionOriginX = CGRectGetMaxX(selectedViewColorFrame) + kHorizontalPadding;
+    descriptionLabelFrame.size.height = kDescriptionLabelHeight;
+    descriptionLabelFrame.origin.x = descriptionOriginX;
+    descriptionLabelFrame.origin.y = kDescriptionVerticalPadding;
+    descriptionLabelFrame.size.width = CGRectGetMaxX(self.selectedViewDescriptionContainer.bounds) - kHorizontalPadding - descriptionOriginX;
+    self.selectedViewDescriptionLabel.frame = descriptionLabelFrame;
+}
+
+
+#pragma mark - Setter Overrides
+
+- (void)setSelectedViewOverlayColor:(UIColor *)selectedViewOverlayColor
+{
+    if (![_selectedViewOverlayColor isEqual:selectedViewOverlayColor]) {
+        _selectedViewOverlayColor = selectedViewOverlayColor;
+        self.selectedViewColorIndicator.backgroundColor = selectedViewOverlayColor;
+    }
+}
+
+- (void)setSelectedViewDescription:(NSString *)selectedViewDescription
+{
+    if (![_selectedViewDescription isEqual:selectedViewDescription]) {
+        _selectedViewDescription = selectedViewDescription;
+        self.selectedViewDescriptionLabel.text = selectedViewDescription;
+        BOOL showDescription = [selectedViewDescription length] > 0;
+        self.selectedViewDescriptionContainer.hidden = !showDescription;
+    }
+}
+
+
+#pragma mark - Sizing Convenience Methods
+
++ (UIFont *)descriptionLabelFont
+{
+    return [UIFont systemFontOfSize:12.0];
+}
+
++ (CGFloat)toolbarItemHeight
+{
+    return 44.0;
+}
+
++ (CGFloat)dragHandleWidth
+{
+    return 30.0;
+}
+
++ (CGFloat)descriptionLabelHeight
+{
+    return ceil([[self descriptionLabelFont] lineHeight]);
+}
+
++ (CGFloat)descriptionVerticalPadding
+{
+    return 2.0;
+}
+
++ (CGFloat)descriptionContainerHeight
+{
+    return [self descriptionVerticalPadding] * 2.0 + [self descriptionLabelHeight];
+}
+
++ (CGFloat)selectedViewColorIndicatorDiameter
+{
+    return ceil([self descriptionLabelHeight] / 2.0);
+}
+
++ (CGFloat)horizontalPadding
+{
+    return 11.0;
+}
+
+- (CGSize)sizeThatFits:(CGSize)size
+{
+    CGFloat height = 0.0;
+    height += [[self class] toolbarItemHeight];
+    height += [[self class] descriptionContainerHeight];
+    return CGSizeMake(size.width, height);
+}
+
+@end

+ 25 - 0
Classes/Explorer Toolbar/FLEXExplorerViewController.h

@@ -0,0 +1,25 @@
+//
+//  FLEXExplorerViewController.h
+//  Flipboard
+//
+//  Created by Ryan Olson on 4/4/14.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+
+@protocol FLEXExplorerViewControllerDelegate;
+
+@interface FLEXExplorerViewController : UIViewController
+
+@property (nonatomic, weak) id <FLEXExplorerViewControllerDelegate> delegate;
+
+- (BOOL)shouldReceiveTouchAtWindowPoint:(CGPoint)pointInWindowCoordinates;
+
+@end
+
+@protocol FLEXExplorerViewControllerDelegate <NSObject>
+
+- (void)explorerViewControllerDidFinish:(FLEXExplorerViewController *)explorerViewController;
+
+@end

+ 765 - 0
Classes/Explorer Toolbar/FLEXExplorerViewController.m

@@ -0,0 +1,765 @@
+//
+//  FLEXExplorerViewController.m
+//  Flipboard
+//
+//  Created by Ryan Olson on 4/4/14.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import "FLEXExplorerViewController.h"
+#import "FLEXExplorerToolbar.h"
+#import "FLEXToolbarItem.h"
+#import "FLEXUtility.h"
+#import "FLEXHierarchyTableViewController.h"
+#import "FLEXGlobalsTableViewController.h"
+#import "FLEXObjectExplorerViewController.h"
+#import "FLEXObjectExplorerFactory.h"
+
+typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
+    FLEXExplorerModeDefault,
+    FLEXExplorerModeSelect,
+    FLEXExplorerModeMove
+};
+
+@interface FLEXExplorerViewController () <FLEXHierarchyTableViewControllerDelegate, FLEXGlobalsTableViewControllerDelegate>
+
+@property (nonatomic, strong) FLEXExplorerToolbar *explorerToolbar;
+
+/// Tracks the currently active tool/mode
+@property (nonatomic, assign) FLEXExplorerMode currentMode;
+
+/// Gesture recognizer for dragging a view in move mode
+@property (nonatomic, strong) UIPanGestureRecognizer *movePanGR;
+
+/// Gesture recognizer for showing additional details on the selected view
+@property (nonatomic, strong) UITapGestureRecognizer *detailsTapGR;
+
+/// Only valid while a move pan gesture is in progress.
+@property (nonatomic, assign) CGRect selectedViewFrameBeforeDragging;
+
+/// Only valid while a toolbar drag pan gesture is in progress.
+@property (nonatomic, assign) CGRect toolbarFrameBeforeDragging;
+
+/// Borders of all the visible views in the hierarchy at the selection point.
+/// The keys are NSValues with the correponding view (nonretained).
+@property (nonatomic, strong) NSDictionary *outlineViewsForVisibleViews;
+
+/// The actual views at the selection point with the deepest view last.
+@property (nonatomic, strong) NSArray *viewsAtTapPoint;
+
+/// The view that we're currently highlighting with an overlay and displaying details for.
+@property (nonatomic, strong) UIView *selectedView;
+
+/// A colored transparent overlay to indicate that the view is selected.
+@property (nonatomic, strong) UIView *selectedViewOverlay;
+
+/// Tracked so we can restore the key window after dismissing a modal.
+/// We need to become key after modal presentation so we can correctly capture intput.
+/// If we're just showing the toolbar, we want the main app's window to remain key so that we don't interfere with input, status bar, etc.
+@property (nonatomic, strong) UIWindow *previousKeyWindow;
+
+/// All views that we're KVOing. Used to help us clean up properly.
+@property (nonatomic, strong) NSMutableSet *observedViews;
+
+@end
+
+@implementation FLEXExplorerViewController
+
+- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
+{
+    self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
+    if (self) {
+        self.observedViews = [NSMutableSet set];
+    }
+    return self;
+}
+
+-(void)dealloc
+{
+    for (UIView *view in _observedViews) {
+        [self stopObservingView:view];
+    }
+}
+
+- (void)viewDidLoad
+{
+    [super viewDidLoad];
+	
+    // Toolbar
+    self.explorerToolbar = [[FLEXExplorerToolbar alloc] init];
+    CGSize toolbarSize = [self.explorerToolbar sizeThatFits:self.view.bounds.size];
+    CGFloat toolbarOriginY = 0.0;
+    CGRect statusBarFrame = [self.view convertRect:[[UIApplication sharedApplication] statusBarFrame] fromView:nil];
+    CGFloat statusBarBottomEdge = CGRectGetMaxY(statusBarFrame);
+    if (statusBarBottomEdge > 0) {
+        toolbarOriginY = statusBarBottomEdge;
+    }
+    self.explorerToolbar.frame = CGRectMake(0.0, toolbarOriginY, toolbarSize.width, toolbarSize.height);
+    self.explorerToolbar.autoresizingMask = UIViewAutoresizingFlexibleWidth;
+    [self.view addSubview:self.explorerToolbar];
+    [self setupToolbarActions];
+    [self setupToolbarGestures];
+    
+    // View selection
+    UITapGestureRecognizer *selectionTapGR = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleSelectionTap:)];
+    [self.view addGestureRecognizer:selectionTapGR];
+    
+    // View moving
+    self.movePanGR = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleMovePan:)];
+    self.movePanGR.enabled = self.currentMode == FLEXExplorerModeMove;
+    [self.view addGestureRecognizer:self.movePanGR];
+}
+
+- (void)viewWillAppear:(BOOL)animated
+{
+    [self updateButtonStates];
+}
+
+
+#pragma mark - Status Bar Wrangling for iOS 7
+
+// Try to get the preferred status bar properties from the app's root view controller (not us).
+// In general, our window shouldn't be the key window when this view controller is asked about the status bar.
+// However, we guard against infinite recursion and provide a reasonable default for status bar behavior in case our window is the keyWindow.
+
+- (UIViewController *)viewControllerForStatusBarProperties
+{
+    UIViewController *viewControllerToAsk = [[[UIApplication sharedApplication] keyWindow] rootViewController];
+    
+    // On iPhone, modal view controllers get asked
+    if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
+        while (viewControllerToAsk.presentedViewController) {
+            viewControllerToAsk = viewControllerToAsk.presentedViewController;
+        }
+    }
+    
+    return viewControllerToAsk;
+}
+
+- (UIStatusBarStyle)preferredStatusBarStyle
+{
+    UIViewController *viewControllerToAsk = [self viewControllerForStatusBarProperties];
+    UIStatusBarStyle preferredStyle = UIStatusBarStyleDefault;
+    if (viewControllerToAsk != self) {
+        preferredStyle = [viewControllerToAsk preferredStatusBarStyle];
+    }
+    return preferredStyle;
+}
+
+- (UIStatusBarAnimation)preferredStatusBarUpdateAnimation
+{
+    UIViewController *viewControllerToAsk = [self viewControllerForStatusBarProperties];
+    UIStatusBarAnimation preferredAnimation = UIStatusBarAnimationFade;
+    if (viewControllerToAsk != self) {
+        preferredAnimation = [viewControllerToAsk preferredStatusBarUpdateAnimation];
+    }
+    return preferredAnimation;
+}
+
+- (BOOL)prefersStatusBarHidden
+{
+    UIViewController *viewControllerToAsk = [self viewControllerForStatusBarProperties];
+    BOOL prefersHidden = NO;
+    if (viewControllerToAsk != self) {
+        prefersHidden = [viewControllerToAsk prefersStatusBarHidden];
+    }
+    return prefersHidden;
+}
+
+
+#pragma mark - Setter Overrides
+
+- (void)setSelectedView:(UIView *)selectedView
+{
+    if (![_selectedView isEqual:selectedView]) {
+        if (![self.viewsAtTapPoint containsObject:_selectedView]) {
+            [self stopObservingView:_selectedView];
+        }
+        
+        _selectedView = selectedView;
+        
+        [self beginObservingView:selectedView];
+
+        // Update the toolbar and selected overlay
+        self.explorerToolbar.selectedViewDescription = [FLEXUtility descriptionForView:selectedView includingFrame:YES];
+        self.explorerToolbar.selectedViewOverlayColor = [FLEXUtility consistentRandomColorForObject:selectedView];;
+
+        if (selectedView) {
+            if (!self.selectedViewOverlay) {
+                self.selectedViewOverlay = [[UIView alloc] init];
+                [self.view addSubview:self.selectedViewOverlay];
+                self.selectedViewOverlay.layer.borderWidth = 1.0;
+            }
+            UIColor *outlineColor = [FLEXUtility consistentRandomColorForObject:selectedView];
+            self.selectedViewOverlay.backgroundColor = [outlineColor colorWithAlphaComponent:0.2];
+            self.selectedViewOverlay.layer.borderColor = [outlineColor CGColor];
+            self.selectedViewOverlay.frame = [self.view convertRect:selectedView.bounds fromView:selectedView];
+            
+            // Make sure the selected overlay is in front of all the other subviews except the toolbar, which should always stay on top.
+            [self.view bringSubviewToFront:self.selectedViewOverlay];
+            [self.view bringSubviewToFront:self.explorerToolbar];
+        } else {
+            [self.selectedViewOverlay removeFromSuperview];
+            self.selectedViewOverlay = nil;
+        }
+        
+        // Some of the button states depend on whether we have a selected view.
+        [self updateButtonStates];
+    }
+}
+
+- (void)setViewsAtTapPoint:(NSArray *)viewsAtTapPoint
+{
+    if (![_viewsAtTapPoint isEqual:viewsAtTapPoint]) {
+        for (UIView *view in _viewsAtTapPoint) {
+            if (view != self.selectedView) {
+                [self stopObservingView:view];
+            }
+        }
+        
+        _viewsAtTapPoint = viewsAtTapPoint;
+        
+        for (UIView *view in viewsAtTapPoint) {
+            [self beginObservingView:view];
+        }
+    }
+}
+
+- (void)setCurrentMode:(FLEXExplorerMode)currentMode
+{
+    if (_currentMode != currentMode) {
+        _currentMode = currentMode;
+        switch (currentMode) {
+            case FLEXExplorerModeDefault:
+                [self removeAndClearOutlineViews];
+                self.viewsAtTapPoint = nil;
+                self.selectedView = nil;
+                break;
+                
+            case FLEXExplorerModeSelect:
+                // Make sure the outline views are unhidden in case we came from the move mode.
+                for (id key in self.outlineViewsForVisibleViews) {
+                    UIView *outlineView = self.outlineViewsForVisibleViews[key];
+                    outlineView.hidden = NO;
+                }
+                break;
+                
+            case FLEXExplorerModeMove:
+                // Hide all the outline views to focus on the selected view, which is the only one that will move.
+                for (id key in self.outlineViewsForVisibleViews) {
+                    UIView *outlineView = self.outlineViewsForVisibleViews[key];
+                    outlineView.hidden = YES;
+                }
+                break;
+        }
+        self.movePanGR.enabled = currentMode == FLEXExplorerModeMove;
+        [self updateButtonStates];
+    }
+}
+
+
+#pragma mark - View Tracking
+
+- (void)beginObservingView:(UIView *)view
+{
+    // Bail if we're already observing this view or if there's nothing to observe.
+    if (!view || [self.observedViews containsObject:view]) {
+        return;
+    }
+    
+    for (NSString *keyPath in [[self class] viewKeyPathsToTrack]) {
+        [view addObserver:self forKeyPath:keyPath options:0 context:NULL];
+    }
+    
+    [self.observedViews addObject:view];
+}
+
+- (void)stopObservingView:(UIView *)view
+{
+    if (!view) {
+        return;
+    }
+    
+    for (NSString *keyPath in [[self class] viewKeyPathsToTrack]) {
+        [view removeObserver:self forKeyPath:keyPath];
+    }
+    
+    [self.observedViews removeObject:view];
+}
+
++ (NSArray *)viewKeyPathsToTrack
+{
+    static NSArray *trackedViewKeyPaths = nil;
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        NSString *superviewKeyPath = NSStringFromSelector(@selector(superview));
+        NSString *frameKeyPath = NSStringFromSelector(@selector(frame));
+        NSString *superviewFrameKeyPath = [NSString stringWithFormat:@"%@.%@", superviewKeyPath, frameKeyPath];
+        trackedViewKeyPaths = @[frameKeyPath, superviewKeyPath, superviewFrameKeyPath];
+    });
+    return trackedViewKeyPaths;
+}
+
+- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
+{
+    NSUInteger indexOfView = [self.viewsAtTapPoint indexOfObject:object];
+    if (indexOfView != NSNotFound) {
+        UIView *view = [self.viewsAtTapPoint objectAtIndex:indexOfView];
+        NSValue *key = [NSValue valueWithNonretainedObject:view];
+        UIView *outline = [self.outlineViewsForVisibleViews objectForKey:key];
+        if (outline) {
+            CGRect outlineFrame = [self.view convertRect:view.bounds fromView:view];
+            outline.frame = outlineFrame;
+        }
+    }
+    if (object == self.selectedView) {
+        // Update the selected view description since we show the frame value there.
+        self.explorerToolbar.selectedViewDescription = [FLEXUtility descriptionForView:self.selectedView includingFrame:YES];
+        CGRect selectedViewOutlineFrame = [self.view convertRect:self.selectedView.bounds fromView:self.selectedView];
+        self.selectedViewOverlay.frame = selectedViewOutlineFrame;
+    }
+}
+
+
+#pragma mark - Toolbar Buttons
+
+- (void)setupToolbarActions
+{
+    [self.explorerToolbar.selectItem addTarget:self action:@selector(selectButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
+    [self.explorerToolbar.hierarchyItem addTarget:self action:@selector(hierarchyButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
+    [self.explorerToolbar.moveItem addTarget:self action:@selector(moveButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
+    [self.explorerToolbar.globalsItem addTarget:self action:@selector(globalsButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
+    [self.explorerToolbar.closeItem addTarget:self action:@selector(closeButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
+}
+
+- (void)selectButtonTapped:(FLEXToolbarItem *)sender
+{
+    if (self.currentMode == FLEXExplorerModeSelect) {
+        self.currentMode = FLEXExplorerModeDefault;
+    } else {
+        self.currentMode = FLEXExplorerModeSelect;
+    }
+}
+
+- (void)hierarchyButtonTapped:(FLEXToolbarItem *)sender
+{
+    NSArray *allViews = [self allViewsInHierarchy];
+    NSDictionary *depthsForViews = [self hierarchyDepthsForViews:allViews];
+    FLEXHierarchyTableViewController *hierarchyTVC = [[FLEXHierarchyTableViewController alloc] initWithViews:allViews viewsAtTap:self.viewsAtTapPoint selectedView:self.selectedView depths:depthsForViews];
+    hierarchyTVC.delegate = self;
+    UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:hierarchyTVC];
+    [self makeKeyAndPresentViewController:navigationController animated:YES completion:nil];
+}
+
+- (NSArray *)allViewsInHierarchy
+{
+    NSMutableArray *allViews = [NSMutableArray array];
+    NSArray *windows = [self allWindows];
+    for (UIWindow *window in windows) {
+        if (window != self.view.window) {
+            [allViews addObject:window];
+            [allViews addObjectsFromArray:[self allRecursiveSubviewsInView:window]];
+        }
+    }
+    return allViews;
+}
+
+- (NSArray *)allWindows
+{
+    NSString *statusBarString = [NSString stringWithFormat:@"%@arWindow", @"_statusB"];
+    UIWindow *statusWindow = [[UIApplication sharedApplication] valueForKey:statusBarString];
+    NSMutableArray *windows = [[[UIApplication sharedApplication] windows] mutableCopy];
+    if (statusWindow) {
+        // The windows are ordered back to front, so default to inserting the status bar at the end.
+        // However, it there are windows at status bar level, insert the status bar before them.
+        NSInteger insertionIndex = [windows count];
+        for (UIWindow *window in windows) {
+            if (window.windowLevel >= UIWindowLevelStatusBar) {
+                insertionIndex = [windows indexOfObject:window];
+                break;
+            }
+        }
+        [windows insertObject:statusWindow atIndex:insertionIndex];
+    }
+    return windows;
+}
+
+- (void)moveButtonTapped:(FLEXToolbarItem *)sender
+{
+    if (self.currentMode == FLEXExplorerModeMove) {
+        self.currentMode = FLEXExplorerModeDefault;
+    } else {
+        self.currentMode = FLEXExplorerModeMove;
+    }
+}
+
+- (void)globalsButtonTapped:(FLEXToolbarItem *)sender
+{
+    FLEXGlobalsTableViewController *globalsViewController = [[FLEXGlobalsTableViewController alloc] init];
+    globalsViewController.delegate = self;
+    globalsViewController.applicationWindow = [[UIApplication sharedApplication] keyWindow];
+    UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:globalsViewController];
+    [self makeKeyAndPresentViewController:navigationController animated:YES completion:nil];
+}
+
+- (void)closeButtonTapped:(FLEXToolbarItem *)sender
+{
+    [self.delegate explorerViewControllerDidFinish:self];
+}
+
+- (void)updateButtonStates
+{
+    // Move and details only active when an object is selected.
+    BOOL hasSelectedObject = self.selectedView != nil;
+    self.explorerToolbar.moveItem.enabled = hasSelectedObject;
+    self.explorerToolbar.selectItem.selected = self.currentMode == FLEXExplorerModeSelect;
+    self.explorerToolbar.moveItem.selected = self.currentMode == FLEXExplorerModeMove;
+}
+
+
+#pragma mark - Toolbar Dragging
+
+- (void)setupToolbarGestures
+{
+    // Pan gesture for dragging.
+    UIPanGestureRecognizer *panGR = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleToolbarPanGesture:)];
+    [self.explorerToolbar.dragHandle addGestureRecognizer:panGR];
+    
+    // Tap gesture for hinting.
+    UITapGestureRecognizer *hintTapGR = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleToolbarHintTapGesture:)];
+    [self.explorerToolbar.dragHandle addGestureRecognizer:hintTapGR];
+    
+    // Tap gesture for showing additional details
+    self.detailsTapGR = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleToolbarDetailsTapGesture:)];
+    [self.explorerToolbar.selectedViewDescriptionContainer addGestureRecognizer:self.detailsTapGR];
+}
+
+- (void)handleToolbarPanGesture:(UIPanGestureRecognizer *)panGR
+{
+    switch (panGR.state) {
+        case UIGestureRecognizerStateBegan:
+            self.toolbarFrameBeforeDragging = self.explorerToolbar.frame;
+            [self updateToolbarPostionWithDragGesture:panGR];
+            break;
+            
+        case UIGestureRecognizerStateChanged:
+        case UIGestureRecognizerStateEnded:
+            [self updateToolbarPostionWithDragGesture:panGR];
+            break;
+            
+        default:
+            break;
+    }
+}
+
+- (void)updateToolbarPostionWithDragGesture:(UIPanGestureRecognizer *)panGR
+{
+    CGPoint translation = [panGR translationInView:self.view];
+    CGRect newToolbarFrame = self.toolbarFrameBeforeDragging;
+    newToolbarFrame.origin.y += translation.y;
+    
+    CGFloat maxY = CGRectGetMaxY(self.view.bounds) - newToolbarFrame.size.height;
+    if (newToolbarFrame.origin.y < 0.0) {
+        newToolbarFrame.origin.y = 0.0;
+    } else if (newToolbarFrame.origin.y > maxY) {
+        newToolbarFrame.origin.y = maxY;
+    }
+    
+    self.explorerToolbar.frame = newToolbarFrame;
+}
+
+- (void)handleToolbarHintTapGesture:(UITapGestureRecognizer *)tapGR
+{
+    // Bounce the toolbar to indicate that it is draggable.
+    // TODO: make it bouncier.
+    if (tapGR.state == UIGestureRecognizerStateRecognized) {
+        CGRect originalToolbarFrame = self.explorerToolbar.frame;
+        const NSTimeInterval kHalfwayDuration = 0.2;
+        const CGFloat kVerticalOffset = 30.0;
+        [UIView animateWithDuration:kHalfwayDuration delay:0 options:UIViewAnimationOptionCurveEaseOut animations:^{
+            CGRect newToolbarFrame = self.explorerToolbar.frame;
+            newToolbarFrame.origin.y += kVerticalOffset;
+            self.explorerToolbar.frame = newToolbarFrame;
+        } completion:^(BOOL finished) {
+            [UIView animateWithDuration:kHalfwayDuration delay:0 options:UIViewAnimationOptionCurveEaseIn animations:^{
+                self.explorerToolbar.frame = originalToolbarFrame;
+            } completion:nil];
+        }];
+    }
+}
+
+- (void)handleToolbarDetailsTapGesture:(UITapGestureRecognizer *)tapGR
+{
+    if (tapGR.state == UIGestureRecognizerStateRecognized && self.selectedView) {
+        FLEXObjectExplorerViewController *selectedViewExplorer = [FLEXObjectExplorerFactory explorerViewControllerForObject:self.selectedView];
+        selectedViewExplorer.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(selectedViewExplorerFinished:)];
+        UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:selectedViewExplorer];
+        [self makeKeyAndPresentViewController:navigationController animated:YES completion:nil];
+    }
+}
+
+
+#pragma mark - View Selection
+
+- (void)handleSelectionTap:(UITapGestureRecognizer *)tapGR
+{
+    // Only if we're in selection mode
+    if (self.currentMode == FLEXExplorerModeSelect && tapGR.state == UIGestureRecognizerStateRecognized) {
+        [self updateOutlineViewsForSelectionPoint:[tapGR locationInView:nil]];
+    }
+}
+
+- (void)updateOutlineViewsForSelectionPoint:(CGPoint)selectionPointInWindow
+{
+    [self removeAndClearOutlineViews];
+    
+    // Include hidden views in the "viewsAtTapPoint" array so we can show them in the hierarchy list.
+    self.viewsAtTapPoint = [self viewsAtPoint:selectionPointInWindow skipHiddenViews:NO];
+    
+    // For outlined views and the selected view, only use visible views.
+    // Outlining hidden views adds clutter and makes the selection behavior confusing.
+    NSArray *visibleViewsAtTapPoint = [self viewsAtPoint:selectionPointInWindow skipHiddenViews:YES];
+    NSMutableDictionary *newOutlineViewsForVisibleViews = [NSMutableDictionary dictionary];
+    for (UIView *view in visibleViewsAtTapPoint) {
+        UIView *outlineView = [self outlineViewForView:view];
+        [self.view addSubview:outlineView];
+        NSValue *key = [NSValue valueWithNonretainedObject:view];
+        [newOutlineViewsForVisibleViews setObject:outlineView forKey:key];
+    }
+    self.outlineViewsForVisibleViews = newOutlineViewsForVisibleViews;
+    self.selectedView = [self viewForSelectionAtPoint:selectionPointInWindow];
+    
+    // Make sure the explorer toolbar doesn't end up behind the newly added outline views.
+    [self.view bringSubviewToFront:self.explorerToolbar];
+    
+    [self updateButtonStates];
+}
+
+- (UIView *)outlineViewForView:(UIView *)view
+{
+    CGRect outlineFrame = [self.view convertRect:view.bounds fromView:view];
+    UIView *outlineView = [[UIView alloc] initWithFrame:outlineFrame];
+    outlineView.backgroundColor = [UIColor clearColor];
+    outlineView.layer.borderColor = [[FLEXUtility consistentRandomColorForObject:view] CGColor];
+    outlineView.layer.borderWidth = 1.0;
+    return outlineView;
+}
+
+- (void)removeAndClearOutlineViews
+{
+    for (id key in self.outlineViewsForVisibleViews) {
+        UIView *outlineView = self.outlineViewsForVisibleViews[key];
+        [outlineView removeFromSuperview];
+    }
+    self.outlineViewsForVisibleViews = nil;
+}
+
+- (NSArray *)viewsAtPoint:(CGPoint)tapPointInWindow skipHiddenViews:(BOOL)skipHidden
+{
+    NSMutableArray *views = [NSMutableArray array];
+    for (UIWindow *window in [self allWindows]) {
+        // Don't include the explorer's own window or subviews.
+        if (window != self.view.window && [window pointInside:tapPointInWindow withEvent:nil]) {
+            [views addObject:window];
+            [views addObjectsFromArray:[self recursiveSubviewsAtPoint:tapPointInWindow inView:window skipHiddenViews:skipHidden]];
+        }
+    }
+    return views;
+}
+
+- (UIView *)viewForSelectionAtPoint:(CGPoint)tapPointInWindow
+{
+    // Select in the window that would handle the touch, but don't just use the result of hitTest:withEvent: so we can still select views with interaction disabled.
+    // Default to the the application's key window if none of the windows want the touch.
+    UIWindow *windowForSelection = [[UIApplication sharedApplication] keyWindow];
+    for (UIWindow *window in [[self allWindows] reverseObjectEnumerator]) {
+        // Ignore the explorer's own window.
+        if (window != self.view.window) {
+            if ([window hitTest:tapPointInWindow withEvent:nil]) {
+                windowForSelection = window;
+                break;
+            }
+        }
+    }
+    
+    // Select the deepest visible view at the tap point. This generally corresponds to what the user wants to select.
+    return [[self recursiveSubviewsAtPoint:tapPointInWindow inView:windowForSelection skipHiddenViews:YES] lastObject];
+}
+
+- (NSArray *)recursiveSubviewsAtPoint:(CGPoint)pointInView inView:(UIView *)view skipHiddenViews:(BOOL)skipHidden
+{
+    NSMutableArray *subviewsAtPoint = [NSMutableArray array];
+    for (UIView *subview in view.subviews) {
+        BOOL isHidden = subview.hidden || subview.alpha < 0.01;
+        if (skipHidden && isHidden) {
+            continue;
+        }
+        
+        BOOL subviewContainsPoint = CGRectContainsPoint(subview.frame, pointInView);
+        if (subviewContainsPoint) {
+            [subviewsAtPoint addObject:subview];
+        }
+        
+        // If this view doesn't clip to its bounds, we need to check its subviews even if it doesn't contain the selection point.
+        // They may be visible and contain the selection point.
+        if (subviewContainsPoint || !subview.clipsToBounds) {
+            CGPoint pointInSubview = [view convertPoint:pointInView toView:subview];
+            [subviewsAtPoint addObjectsFromArray:[self recursiveSubviewsAtPoint:pointInSubview inView:subview skipHiddenViews:skipHidden]];
+        }
+    }
+    return subviewsAtPoint;
+}
+
+- (NSArray *)allRecursiveSubviewsInView:(UIView *)view
+{
+    NSMutableArray *subviews = [NSMutableArray array];
+    for (UIView *subview in view.subviews) {
+        [subviews addObject:subview];
+        [subviews addObjectsFromArray:[self allRecursiveSubviewsInView:subview]];
+    }
+    return subviews;
+}
+
+- (NSDictionary *)hierarchyDepthsForViews:(NSArray *)views
+{
+    NSMutableDictionary *hierarchyDepths = [NSMutableDictionary dictionary];
+    for (UIView *view in views) {
+        NSInteger depth = 0;
+        UIView *tryView = view;
+        while (tryView.superview) {
+            tryView = tryView.superview;
+            depth++;
+        }
+        [hierarchyDepths setObject:@(depth) forKey:[NSValue valueWithNonretainedObject:view]];
+    }
+    return hierarchyDepths;
+}
+
+
+#pragma mark - Selected View Moving
+
+- (void)handleMovePan:(UIPanGestureRecognizer *)movePanGR
+{
+    switch (movePanGR.state) {
+        case UIGestureRecognizerStateBegan:
+            self.selectedViewFrameBeforeDragging = self.selectedView.frame;
+            [self updateSelectedViewPositionWithDragGesture:movePanGR];
+            break;
+            
+        case UIGestureRecognizerStateChanged:
+        case UIGestureRecognizerStateEnded:
+            [self updateSelectedViewPositionWithDragGesture:movePanGR];
+            break;
+            
+        default:
+            break;
+    }
+}
+
+- (void)updateSelectedViewPositionWithDragGesture:(UIPanGestureRecognizer *)movePanGR
+{
+    CGPoint translation = [movePanGR translationInView:self.selectedView.superview];
+    CGRect newSelectedViewFrame = self.selectedViewFrameBeforeDragging;
+    newSelectedViewFrame.origin.x = floor(newSelectedViewFrame.origin.x + translation.x);
+    newSelectedViewFrame.origin.y = floor(newSelectedViewFrame.origin.y + translation.y);
+    self.selectedView.frame = newSelectedViewFrame;
+}
+
+
+#pragma mark - Touch Handling
+
+- (BOOL)shouldReceiveTouchAtWindowPoint:(CGPoint)pointInWindowCoordinates
+{
+    BOOL shouldReceiveTouch = NO;
+    
+    CGPoint pointInLocalCoordinates = [self.view convertPoint:pointInWindowCoordinates fromView:nil];
+    
+    // Always if it's on the toolbar
+    if (CGRectContainsPoint(self.explorerToolbar.frame, pointInLocalCoordinates)) {
+        shouldReceiveTouch = YES;
+    }
+    
+    // Always if we're in selection mode
+    if (!shouldReceiveTouch && self.currentMode == FLEXExplorerModeSelect) {
+        shouldReceiveTouch = YES;
+    }
+    
+    // Always in move mode too
+    if (!shouldReceiveTouch && self.currentMode == FLEXExplorerModeMove) {
+        shouldReceiveTouch = YES;
+    }
+    
+    // Always if we have a modal presented
+    if (!shouldReceiveTouch && self.presentedViewController) {
+        shouldReceiveTouch = YES;
+    }
+    
+    return shouldReceiveTouch;
+}
+
+
+#pragma mark - FLEXHierarchyTableViewControllerDelegate
+
+- (void)hierarchyViewController:(FLEXHierarchyTableViewController *)hierarchyViewController didFinishWithSelectedView:(UIView *)selectedView
+{
+    // Note that we need to wait until the view controller is dismissed to calculated the frame of the outline view.
+    // Otherwise the coordinate conversion doesn't give the correct result.
+    [self resignKeyAndDismissViewControllerAnimated:YES completion:^{
+        // If the selected view is outside of the tap point array (selected from "Full Hierarchy"),
+        // then clear out the tap point array and remove all the outline views.
+        if (![self.viewsAtTapPoint containsObject:selectedView]) {
+            self.viewsAtTapPoint = nil;
+            [self removeAndClearOutlineViews];
+        }
+        
+        // If we now have a selected view and we didn't have one previously, go to "select" mode.
+        if (self.currentMode == FLEXExplorerModeDefault && selectedView) {
+            self.currentMode = FLEXExplorerModeSelect;
+        }
+        
+        // The selected view setter will also update the selected view overlay appropriately.
+        self.selectedView = selectedView;
+    }];
+}
+
+
+#pragma mark - FLEXGlobalsViewControllerDelegate
+
+- (void)globalsViewControllerDidFinish:(FLEXGlobalsTableViewController *)globalsViewController
+{
+    [self resignKeyAndDismissViewControllerAnimated:YES completion:nil];
+}
+
+
+#pragma mark - FLEXObjectExplorerViewController Done Action
+
+- (void)selectedViewExplorerFinished:(id)sender
+{
+    [self resignKeyAndDismissViewControllerAnimated:YES completion:nil];
+}
+
+
+#pragma mark - Modal Presentation and Window Management
+
+- (void)makeKeyAndPresentViewController:(UIViewController *)viewController animated:(BOOL)animated completion:(void (^)(void))completion
+{
+    // Save the current key window so we can restore it following dismissal.
+    self.previousKeyWindow = [[UIApplication sharedApplication] keyWindow];
+    
+    // Make our window key to correctly handle input.
+    [self.view.window makeKeyWindow];
+    
+    // Show the view controller.
+    [self presentViewController:viewController animated:animated completion:completion];
+}
+
+- (void)resignKeyAndDismissViewControllerAnimated:(BOOL)animated completion:(void (^)(void))completion
+{
+    [self.previousKeyWindow makeKeyWindow];
+    
+    self.previousKeyWindow = nil;
+    
+    [self dismissViewControllerAnimated:animated completion:completion];
+}
+
+@end

+ 20 - 0
Classes/Explorer Toolbar/FLEXManager.h

@@ -0,0 +1,20 @@
+//
+//  FLEXManager.h
+//  Flipboard
+//
+//  Created by Ryan Olson on 4/4/14.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+
+@interface FLEXManager : NSObject
+
++ (instancetype)sharedManager;
+
+@property (nonatomic, readonly) BOOL isHidden;
+
+- (void)showExplorer;
+- (void)hideExplorer;
+
+@end

+ 79 - 0
Classes/Explorer Toolbar/FLEXManager.m

@@ -0,0 +1,79 @@
+//
+//  FLEXManager.m
+//  Flipboard
+//
+//  Created by Ryan Olson on 4/4/14.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import "FLEXManager.h"
+#import "FLEXExplorerViewController.h"
+#import "FLEXWindow.h"
+
+@interface FLEXManager () <FLEXWindowEventDelegate, FLEXExplorerViewControllerDelegate>
+
+@property (nonatomic, strong) FLEXWindow *explorerWindow;
+@property (nonatomic, strong) FLEXExplorerViewController *explorerViewController;
+
+@end
+
+@implementation FLEXManager
+
++ (instancetype)sharedManager
+{
+    static FLEXManager *sharedManager = nil;
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        sharedManager = [[[self class] alloc] init];
+    });
+    return sharedManager;
+}
+
+- (instancetype)init
+{
+    self = [super init];
+    if (self) {
+        self.explorerWindow = [[FLEXWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
+        self.explorerWindow.eventDelegate = self;
+        
+        self.explorerViewController = [[FLEXExplorerViewController alloc] init];
+        self.explorerViewController.delegate = self;
+        self.explorerWindow.rootViewController = self.explorerViewController;
+        [self.explorerWindow addSubview:self.explorerViewController.view];
+    }
+    return self;
+}
+
+- (void)showExplorer
+{
+    self.explorerWindow.hidden = NO;
+}
+
+- (void)hideExplorer
+{
+    self.explorerWindow.hidden = YES;
+}
+
+- (BOOL)isHidden
+{
+    return self.explorerWindow.isHidden;
+}
+
+
+#pragma mark - FLEXWindowEventDelegate
+
+- (BOOL)shouldHandleTouchAtPoint:(CGPoint)pointInWindow
+{
+    // Ask the explorer view controller
+    return [self.explorerViewController shouldReceiveTouchAtWindowPoint:pointInWindow];
+}
+
+
+#pragma mark - FLEXExplorerViewControllerDelegate
+
+- (void)explorerViewControllerDidFinish:(FLEXExplorerViewController *)explorerViewController
+{
+    [self hideExplorer];
+}
+
+@end

+ 17 - 0
Classes/Explorer Toolbar/FLEXToolbarItem.h

@@ -0,0 +1,17 @@
+//
+//  FLEXToolbarItem.h
+//  Flipboard
+//
+//  Created by Ryan Olson on 4/4/14.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+
+@interface FLEXToolbarItem : UIButton
+
++ (instancetype)toolbarItemWithTitle:(NSString *)title image:(UIImage *)image;
+
++ (UIColor *)defaultBackgroundColor;
+
+@end

+ 132 - 0
Classes/Explorer Toolbar/FLEXToolbarItem.m

@@ -0,0 +1,132 @@
+//
+//  FLEXToolbarItem.m
+//  Flipboard
+//
+//  Created by Ryan Olson on 4/4/14.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import "FLEXToolbarItem.h"
+
+@interface FLEXToolbarItem ()
+
+@property (nonatomic, copy) NSString *title;
+@property (nonatomic, strong) UIImage *image;
+
+@end
+
+@implementation FLEXToolbarItem
+
+- (id)initWithFrame:(CGRect)frame
+{
+    self = [super initWithFrame:frame];
+    if (self) {
+        self.titleLabel.font = [[self class] titleFont];
+        self.backgroundColor = [[self class] defaultBackgroundColor];
+        [self setTitleColor:[[self class] defaultTitleColor] forState:UIControlStateNormal];
+        [self setTitleColor:[[self class] disabledTitleColor] forState:UIControlStateDisabled];
+    }
+    return self;
+}
+
++ (instancetype)toolbarItemWithTitle:(NSString *)title image:(UIImage *)image
+{
+    FLEXToolbarItem *toolbarItem = [self buttonWithType:UIButtonTypeCustom];
+    toolbarItem.title = title;
+    toolbarItem.image = image;
+    [toolbarItem setTitle:title forState:UIControlStateNormal];
+    [toolbarItem setImage:image forState:UIControlStateNormal];
+    return toolbarItem;
+}
+
+
+#pragma mark - Display Defaults
+
++ (UIFont *)titleFont
+{
+    return [UIFont systemFontOfSize:12.0];
+}
+
++ (UIColor *)defaultTitleColor
+{
+    return [UIColor blackColor];
+}
+
++ (UIColor *)disabledTitleColor
+{
+    return [UIColor colorWithWhite:121.0/255.0 alpha:1.0];
+}
+
++ (UIColor *)highlightedBackgroundColor
+{
+    return [UIColor colorWithWhite:0.9 alpha:1.0];
+}
+
++ (UIColor *)selectedBackgroundColor
+{
+    return [UIColor colorWithRed:199.0/255.0 green:199.0/255.0 blue:255.0/255.0 alpha:1.0];
+}
+
++ (UIColor *)defaultBackgroundColor
+{
+    return [UIColor colorWithWhite:1.0 alpha:0.95];
+}
+
++ (CGFloat)topMargin
+{
+    return 2.0;
+}
+
+
+#pragma mark - State Changes
+
+- (void)setHighlighted:(BOOL)highlighted
+{
+    [super setHighlighted:highlighted];
+    [self updateBackgroundColor];
+}
+
+- (void)setSelected:(BOOL)selected
+{
+    [super setSelected:selected];
+    [self updateBackgroundColor];
+}
+
+- (void)updateBackgroundColor
+{
+    if (self.highlighted) {
+        self.backgroundColor = [[self class] highlightedBackgroundColor];
+    } else if (self.selected) {
+        self.backgroundColor = [[self class] selectedBackgroundColor];
+    } else {
+        self.backgroundColor = [[self class] defaultBackgroundColor];
+    }
+}
+
+
+#pragma mark - UIButton Layout Overrides
+
+- (CGRect)titleRectForContentRect:(CGRect)contentRect
+{
+    // Bottom aligned and centered.
+    CGRect titleRect = CGRectZero;
+    CGSize titleSize = ([self.title sizeWithFont:[[self class] titleFont] constrainedToSize:contentRect.size]);
+    titleSize = CGSizeMake(ceil(titleSize.width), ceil(titleSize.height));
+    titleRect.size = titleSize;
+    titleRect.origin.y = contentRect.origin.y + CGRectGetMaxY(contentRect) - titleSize.height;
+    titleRect.origin.x = contentRect.origin.x + floor((contentRect.size.width - titleSize.width) / 2.0);
+    return titleRect;
+}
+
+- (CGRect)imageRectForContentRect:(CGRect)contentRect
+{
+    CGSize imageSize = self.image.size;
+    CGRect titleRect = [self titleRectForContentRect:contentRect];
+    CGFloat availableHeight = contentRect.size.height - titleRect.size.height - [[self class] topMargin];
+    CGFloat originY = [[self class] topMargin] + floor((availableHeight - imageSize.height) / 2.0);
+    CGFloat originX = floor((contentRect.size.width - imageSize.width) / 2.0);
+    CGRect imageRect = CGRectMake(originX, originY, imageSize.width, imageSize.height);
+    return imageRect;
+}
+
+@end

+ 23 - 0
Classes/Explorer Toolbar/FLEXWindow.h

@@ -0,0 +1,23 @@
+//
+//  FLEXWindow.h
+//  Flipboard
+//
+//  Created by Ryan Olson on 4/13/14.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+
+@protocol FLEXWindowEventDelegate;
+
+@interface FLEXWindow : UIWindow
+
+@property (nonatomic, weak) id <FLEXWindowEventDelegate> eventDelegate;
+
+@end
+
+@protocol FLEXWindowEventDelegate <NSObject>
+
+- (BOOL)shouldHandleTouchAtPoint:(CGPoint)pointInWindow;
+
+@end

+ 34 - 0
Classes/Explorer Toolbar/FLEXWindow.m

@@ -0,0 +1,34 @@
+//
+//  FLEXWindow.m
+//  Flipboard
+//
+//  Created by Ryan Olson on 4/13/14.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import "FLEXWindow.h"
+
+@implementation FLEXWindow
+
+- (id)initWithFrame:(CGRect)frame
+{
+    self = [super initWithFrame:frame];
+    if (self) {
+        self.backgroundColor = [UIColor clearColor];
+        // Some apps have windows at UIWindowLevelStatusBar + 1.0, so we go one more above that.
+        // If necessary, this could be increased further.
+        self.windowLevel = UIWindowLevelStatusBar + 2.0;
+    }
+    return self;
+}
+
+- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
+{
+    BOOL pointInside = NO;
+    if ([self.eventDelegate shouldHandleTouchAtPoint:point]) {
+        pointInside = [super pointInside:point withEvent:event];
+    }
+    return pointInside;
+}
+
+@end

+ 15 - 0
Classes/Global State Explorers/FLEXClassesTableViewController.h

@@ -0,0 +1,15 @@
+//
+//  FLEXClassesTableViewController.h
+//  Flipboard
+//
+//  Created by Ryan Olson on 2014-05-03.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+
+@interface FLEXClassesTableViewController : UITableViewController
+
+@property (nonatomic, copy) NSString *binaryImageName;
+
+@end

+ 140 - 0
Classes/Global State Explorers/FLEXClassesTableViewController.m

@@ -0,0 +1,140 @@
+//
+//  FLEXClassesTableViewController.m
+//  Flipboard
+//
+//  Created by Ryan Olson on 2014-05-03.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import "FLEXClassesTableViewController.h"
+#import "FLEXObjectExplorerViewController.h"
+#import "FLEXObjectExplorerFactory.h"
+#import "FLEXUtility.h"
+#import <objc/runtime.h>
+
+@interface FLEXClassesTableViewController () <UISearchBarDelegate>
+
+@property (nonatomic, strong) NSArray *classNames;
+@property (nonatomic, strong) NSArray *filteredClassNames;
+@property (nonatomic, strong) UISearchBar *searchBar;
+
+@end
+
+@implementation FLEXClassesTableViewController
+
+- (void)viewDidLoad
+{
+    [super viewDidLoad];
+    
+    self.searchBar = [[UISearchBar alloc] init];
+    self.searchBar.placeholder = @"Filter";
+    self.searchBar.delegate = self;
+    [self.searchBar sizeToFit];
+    self.tableView.tableHeaderView = self.searchBar;
+}
+
+- (BOOL)prefersStatusBarHidden
+{
+    return YES;
+}
+
+- (void)setBinaryImageName:(NSString *)binaryImageName
+{
+    if (![_binaryImageName isEqual:binaryImageName]) {
+        _binaryImageName = binaryImageName;
+        [self loadClassNames];
+        [self updateTitle];
+    }
+}
+
+- (void)setClassNames:(NSArray *)classNames
+{
+    _classNames = classNames;
+    self.filteredClassNames = classNames;
+}
+
+- (void)loadClassNames
+{
+    unsigned int classNamesCount = 0;
+    const char **classNames = objc_copyClassNamesForImage([self.binaryImageName UTF8String], &classNamesCount);
+    if (classNames) {
+        NSMutableArray *classNameStrings = [NSMutableArray array];
+        for (int i = 0; i < classNamesCount; i++) {
+            const char *className = classNames[i];
+            NSString *classNameString = [NSString stringWithUTF8String:className];
+            [classNameStrings addObject:classNameString];
+        }
+        
+        self.classNames = [classNameStrings sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)];
+        
+        free(classNames);
+    }
+}
+
+- (void)updateTitle
+{
+    NSString *shortImageName = [[self.binaryImageName componentsSeparatedByString:@"/"] lastObject];
+    self.title = [NSString stringWithFormat:@"%@ Classes (%lu)", shortImageName, (unsigned long)[self.filteredClassNames count]];
+}
+
+
+#pragma mark - Search
+
+- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
+{
+    if ([searchText length] > 0) {
+        NSPredicate *searchPreidcate = [NSPredicate predicateWithFormat:@"SELF CONTAINS[cd] %@", searchText];
+        self.filteredClassNames = [self.classNames filteredArrayUsingPredicate:searchPreidcate];
+    } else {
+        self.filteredClassNames = self.classNames;
+    }
+    [self updateTitle];
+    [self.tableView reloadData];
+}
+
+- (void)scrollViewDidScroll:(UIScrollView *)scrollView
+{
+    // Dismiss the keyboard when interacting with filtered results.
+    [self.searchBar endEditing:YES];
+}
+
+
+#pragma mark - Table View Data Source
+
+- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
+{
+    return 1;
+}
+
+- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
+{
+    return [self.filteredClassNames 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 = [FLEXUtility defaultTableViewCellLabelFont];
+    }
+    
+    cell.textLabel.text = self.filteredClassNames[indexPath.row];
+    
+    return cell;
+}
+
+
+#pragma mark - Table View Delegate
+
+- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
+{
+    NSString *className = [self.filteredClassNames objectAtIndex:indexPath.row];
+    Class selectedClass = objc_getClass([className UTF8String]);
+    FLEXObjectExplorerViewController *objectExplorer = [FLEXObjectExplorerFactory explorerViewControllerForObject:selectedClass];
+    [self.navigationController pushViewController:objectExplorer animated:YES];
+}
+
+@end

+ 27 - 0
Classes/Global State Explorers/FLEXGlobalsTableViewController.h

@@ -0,0 +1,27 @@
+//
+//  FLEXGlobalsTableViewController.h
+//  Flipboard
+//
+//  Created by Ryan Olson on 2014-05-03.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+
+@protocol FLEXGlobalsTableViewControllerDelegate;
+
+@interface FLEXGlobalsTableViewController : UITableViewController
+
+@property (nonatomic, weak) id <FLEXGlobalsTableViewControllerDelegate> delegate;
+
+/// We pretend that one of the app's windows is still the key window, even though the explorer window may have become key.
+/// We want to display debug state about the application, not about this tool.
+@property (nonatomic, strong) UIWindow *applicationWindow;
+
+@end
+
+@protocol FLEXGlobalsTableViewControllerDelegate <NSObject>
+
+- (void)globalsViewControllerDidFinish:(FLEXGlobalsTableViewController *)globalsViewController;
+
+@end

+ 226 - 0
Classes/Global State Explorers/FLEXGlobalsTableViewController.m

@@ -0,0 +1,226 @@
+//
+//  FLEXGlobalsTableViewController.m
+//  Flipboard
+//
+//  Created by Ryan Olson on 2014-05-03.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import "FLEXGlobalsTableViewController.h"
+#import "FLEXUtility.h"
+#import "FLEXLibrariesTableViewController.h"
+#import "FLEXClassesTableViewController.h"
+#import "FLEXObjectExplorerViewController.h"
+#import "FLEXObjectExplorerFactory.h"
+#import "FLEXLiveObjectsTableViewController.h"
+
+typedef NS_ENUM(NSUInteger, FLEXGlobalsRow) {
+    FLEXGlobalsRowAppClasses,
+    FLEXGlobalsRowSystemLibraries,
+    FLEXGlobalsRowLiveObjects,
+    FLEXGlobalsRowAppDelegate,
+    FLEXGlobalsRowRootViewController,
+    FLEXGlobalsRowUserDefaults,
+    FLEXGlobalsRowApplication,
+    FLEXGlobalsRowKeyWindow,
+    FLEXGlobalsRowMainScreen,
+    FLEXGlobalsRowCurrentDevice
+};
+
+@interface FLEXGlobalsTableViewController ()
+
+@property (nonatomic, strong) NSArray *rows;
+
+@end
+
+@implementation FLEXGlobalsTableViewController
+
+- (id)initWithStyle:(UITableViewStyle)style
+{
+    // Force grouped style.
+    self = [super initWithStyle:UITableViewStyleGrouped];
+    if (self) {
+        self.title = @"🌎  Global State";
+    }
+    return self;
+}
+
+- (BOOL)prefersStatusBarHidden
+{
+    return YES;
+}
+
+- (void)viewDidLoad
+{
+    [super viewDidLoad];
+
+    self.rows = @[@(FLEXGlobalsRowLiveObjects),
+                  @(FLEXGlobalsRowSystemLibraries),
+                  @(FLEXGlobalsRowAppClasses),
+                  @(FLEXGlobalsRowAppDelegate),
+                  @(FLEXGlobalsRowRootViewController),
+                  @(FLEXGlobalsRowUserDefaults),
+                  @(FLEXGlobalsRowApplication),
+                  @(FLEXGlobalsRowKeyWindow),
+                  @(FLEXGlobalsRowMainScreen),
+                  @(FLEXGlobalsRowCurrentDevice)];
+    
+    self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(donePressed:)];
+}
+
+- (void)donePressed:(id)sender
+{
+    [self.delegate globalsViewControllerDidFinish:self];
+}
+
+
+#pragma mark - Table Data Helpers
+
+- (FLEXGlobalsRow)rowTypeAtIndexPath:(NSIndexPath *)indexPath
+{
+    return [[self.rows objectAtIndex:indexPath.row] unsignedIntegerValue];
+}
+
+- (NSString *)titleForRowType:(FLEXGlobalsRow)rowType
+{
+    NSString *title = nil;
+    switch (rowType) {
+        case FLEXGlobalsRowAppClasses:
+            title = [NSString stringWithFormat:@"📕  %@ Classes", [FLEXUtility applicationName]];
+            break;
+            
+        case FLEXGlobalsRowSystemLibraries:
+            title = @"📚  System Libraries";
+            break;
+        
+        case FLEXGlobalsRowLiveObjects:
+            title = @"💩  Heap Objects";
+            break;
+            
+        case FLEXGlobalsRowAppDelegate:
+            title = [NSString stringWithFormat:@"👉  %@", [[[UIApplication sharedApplication] delegate] class]];
+            break;
+            
+        case FLEXGlobalsRowRootViewController:
+            title = [NSString stringWithFormat:@"🌴  %@", [[self.applicationWindow rootViewController] class]];
+            break;
+            
+        case FLEXGlobalsRowUserDefaults:
+            title = @"🚶  +[NSUserDefaults standardUserDefaults]";
+            break;
+            
+        case FLEXGlobalsRowApplication:
+            title = @"💾  +[UIApplication sharedApplication]";
+            break;
+            
+        case FLEXGlobalsRowKeyWindow:
+            title = @"🔑  -[UIApplication keyWindow]";
+            break;
+            
+        case FLEXGlobalsRowMainScreen:
+            title = @"💻  +[UIScreen mainScreen]";
+            break;
+            
+        case FLEXGlobalsRowCurrentDevice:
+            title = @"📱  +[UIDevice currentDevice]";
+            break;
+    }
+    return title;
+}
+
+- (UIViewController *)drillDownViewControllerForRowType:(FLEXGlobalsRow)rowType
+{
+    UIViewController *viewController = nil;
+    switch (rowType) {
+        case FLEXGlobalsRowAppClasses: {
+            FLEXClassesTableViewController *classesViewController = [[FLEXClassesTableViewController alloc] init];
+            classesViewController.binaryImageName = [FLEXUtility applicationImageName];
+            viewController = classesViewController;
+        } break;
+            
+        case FLEXGlobalsRowSystemLibraries: {
+            FLEXLibrariesTableViewController *librariesViewController = [[FLEXLibrariesTableViewController alloc] init];
+            librariesViewController.title = [self titleForRowType:FLEXGlobalsRowSystemLibraries];
+            viewController = librariesViewController;
+        } break;
+            
+        case FLEXGlobalsRowLiveObjects: {
+            viewController = [[FLEXLiveObjectsTableViewController alloc] init];
+        } break;
+            
+        case FLEXGlobalsRowAppDelegate: {
+            id <UIApplicationDelegate> appDelegate = [[UIApplication sharedApplication] delegate];
+            viewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:appDelegate];
+        } break;
+            
+        case FLEXGlobalsRowRootViewController: {
+            UIViewController *rootViewController = [self.applicationWindow rootViewController];
+            viewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:rootViewController];
+        } break;
+            
+        case FLEXGlobalsRowUserDefaults: {
+            NSUserDefaults *standardUserDefaults = [NSUserDefaults standardUserDefaults];
+            viewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:standardUserDefaults];
+        } break;
+            
+        case FLEXGlobalsRowApplication: {
+            UIApplication *sharedApplication = [UIApplication sharedApplication];
+            viewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:sharedApplication];
+        } break;
+            
+        case FLEXGlobalsRowKeyWindow: {
+            viewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:self.applicationWindow];
+        } break;
+            
+        case FLEXGlobalsRowMainScreen: {
+            UIScreen *mainScreen = [UIScreen mainScreen];
+            viewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:mainScreen];
+        } break;
+            
+        case FLEXGlobalsRowCurrentDevice: {
+            UIDevice *currentDevice = [UIDevice currentDevice];
+            viewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:currentDevice];
+        } break;
+    }
+    return viewController;
+}
+
+
+#pragma mark - Table View Data Source
+
+- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
+{
+    return 1;
+}
+
+- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
+{
+    return [self.rows 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 = [FLEXUtility defaultTableViewCellLabelFont];
+    }
+    FLEXGlobalsRow rowType = [self rowTypeAtIndexPath:indexPath];
+    cell.textLabel.text = [self titleForRowType:rowType];
+    
+    return cell;
+}
+
+
+#pragma mark - Table View Delegate
+
+- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
+{
+    FLEXGlobalsRow rowType = [self rowTypeAtIndexPath:indexPath];
+    UIViewController *drillDownViewController = [self drillDownViewControllerForRowType:rowType];
+    [self.navigationController pushViewController:drillDownViewController animated:YES];
+}
+
+@end

+ 15 - 0
Classes/Global State Explorers/FLEXInstancesTableViewController.h

@@ -0,0 +1,15 @@
+//
+//  FLEXInstancesTableViewController.h
+//  Flipboard
+//
+//  Created by Ryan Olson on 5/28/14.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+
+@interface FLEXInstancesTableViewController : UITableViewController
+
+@property (nonatomic, strong) NSArray *instances;
+
+@end

+ 69 - 0
Classes/Global State Explorers/FLEXInstancesTableViewController.m

@@ -0,0 +1,69 @@
+//
+//  FLEXInstancesTableViewController.m
+//  Flipboard
+//
+//  Created by Ryan Olson on 5/28/14.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import "FLEXInstancesTableViewController.h"
+#import "FLEXObjectExplorerFactory.h"
+#import "FLEXObjectExplorerViewController.h"
+#import "FLEXRuntimeUtility.h"
+#import "FLEXUtility.h"
+
+@interface FLEXInstancesTableViewController ()
+
+@end
+
+@implementation FLEXInstancesTableViewController
+
+- (BOOL)prefersStatusBarHidden
+{
+    return YES;
+}
+
+
+#pragma mark - Table View Data Source
+
+- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
+{
+    return 1;
+}
+
+- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
+{
+    return [self.instances 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.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
+        UIFont *cellFont = [FLEXUtility defaultTableViewCellLabelFont];
+        cell.textLabel.font = cellFont;
+        cell.detailTextLabel.font = cellFont;
+        cell.detailTextLabel.textColor = [UIColor grayColor];
+    }
+    
+    id instance = [self.instances objectAtIndex:indexPath.row];
+    cell.textLabel.text = [NSString stringWithFormat:@"%p", instance];
+    cell.detailTextLabel.text = [FLEXRuntimeUtility descriptionForIvarOrPropertyValue:instance];
+    
+    return cell;
+}
+
+
+#pragma mark - Table View Delegate
+
+- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
+{
+    id instance = [self.instances objectAtIndex:indexPath.row];
+    FLEXObjectExplorerViewController *drillInViewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:instance];
+    [self.navigationController pushViewController:drillInViewController animated:YES];
+}
+
+@end

+ 13 - 0
Classes/Global State Explorers/FLEXLibrariesTableViewController.h

@@ -0,0 +1,13 @@
+//
+//  FLEXLibrariesTableViewController.h
+//  Flipboard
+//
+//  Created by Ryan Olson on 2014-05-02.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+
+@interface FLEXLibrariesTableViewController : UITableViewController
+
+@end

+ 116 - 0
Classes/Global State Explorers/FLEXLibrariesTableViewController.m

@@ -0,0 +1,116 @@
+//
+//  FLEXLibrariesTableViewController.m
+//  Flipboard
+//
+//  Created by Ryan Olson on 2014-05-02.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import "FLEXLibrariesTableViewController.h"
+#import "FLEXUtility.h"
+#import "FLEXClassesTableViewController.h"
+#import <objc/runtime.h>
+
+@interface FLEXLibrariesTableViewController ()
+
+@property (nonatomic, strong) NSArray *imageNames;
+
+@end
+
+@implementation FLEXLibrariesTableViewController
+
+- (id)initWithStyle:(UITableViewStyle)style
+{
+    self = [super initWithStyle:UITableViewStyleGrouped];
+    if (self) {
+        [self loadImageNames];
+    }
+    return self;
+}
+
+- (BOOL)prefersStatusBarHidden
+{
+    return YES;
+}
+
+
+#pragma mark - Binary Images
+
+- (void)loadImageNames
+{
+    unsigned int imageNamesCount = 0;
+    const char **imageNames = objc_copyImageNames(&imageNamesCount);
+    if (imageNames) {
+        NSMutableArray *imageNameStrings = [NSMutableArray array];
+        NSString *appImageName = [FLEXUtility applicationImageName];
+        for (int i = 0; i < imageNamesCount; i++) {
+            const char *imageName = imageNames[i];
+            NSString *imageNameString = [NSString stringWithUTF8String:imageName];
+            // Skip the app's image. We're just showing system libraries and frameworks.
+            if (![imageNameString isEqual:appImageName]) {
+                [imageNameStrings addObject:imageNameString];
+            }
+        }
+        
+        // Sort alphabetically
+        self.imageNames = [imageNameStrings sortedArrayWithOptions:0 usingComparator:^NSComparisonResult(NSString *name1, NSString *name2) {
+                NSString *shortName1 = [self shortNameForImageName:name1];
+                NSString *shortName2 = [self shortNameForImageName:name2];
+                return [shortName1 caseInsensitiveCompare:shortName2];
+        }];
+        
+        free(imageNames);
+    }
+}
+
+- (NSString *)shortNameForImageName:(NSString *)imageName
+{
+    NSString *shortName = nil;
+    NSArray *components = [imageName componentsSeparatedByString:@"/"];
+    NSUInteger componentsCount = [components count];
+    if (componentsCount >= 2) {
+        shortName = [NSString stringWithFormat:@"%@/%@", components[componentsCount - 2], components[componentsCount - 1]];
+    }
+    return shortName;
+}
+
+
+#pragma mark - Table View Data Source
+
+- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
+{
+    return 1;
+}
+
+- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
+{
+    return [self.imageNames 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 = [FLEXUtility defaultTableViewCellLabelFont];
+    }
+    
+    NSString *fullImageName = self.imageNames[indexPath.row];
+    cell.textLabel.text = [self shortNameForImageName:fullImageName];
+    
+    return cell;
+}
+
+
+#pragma mark - Table View Delegate
+
+- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
+{
+    FLEXClassesTableViewController *classesViewController = [[FLEXClassesTableViewController alloc] init];
+    classesViewController.binaryImageName = self.imageNames[indexPath.row];
+    [self.navigationController pushViewController:classesViewController animated:YES];
+}
+
+@end

+ 13 - 0
Classes/Global State Explorers/FLEXLiveObjectsTableViewController.h

@@ -0,0 +1,13 @@
+//
+//  FLEXLiveObjectsTableViewController.h
+//  Flipboard
+//
+//  Created by Ryan Olson on 5/28/14.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+
+@interface FLEXLiveObjectsTableViewController : UITableViewController
+
+@end

+ 220 - 0
Classes/Global State Explorers/FLEXLiveObjectsTableViewController.m

@@ -0,0 +1,220 @@
+//
+//  FLEXLiveObjectsTableViewController.m
+//  Flipboard
+//
+//  Created by Ryan Olson on 5/28/14.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import "FLEXLiveObjectsTableViewController.h"
+#import "FLEXHeapEnumerator.h"
+#import "FLEXInstancesTableViewController.h"
+#import "FLEXUtility.h"
+#import <objc/runtime.h>
+
+static const NSInteger kFLEXLiveObjectsSortAlphabeticallyIndex = 0;
+static const NSInteger kFLEXLiveObjectsSortByCountIndex = 1;
+
+@interface FLEXLiveObjectsTableViewController () <UISearchBarDelegate>
+
+@property (nonatomic, strong) NSDictionary *instanceCountsForClassNames;
+@property (nonatomic, readonly) NSArray *allClassNames;
+@property (nonatomic, strong) NSArray *filteredClassNames;
+@property (nonatomic, strong) UISearchBar *searchBar;
+
+@end
+
+@implementation FLEXLiveObjectsTableViewController
+
+- (void)viewDidLoad
+{
+    [super viewDidLoad];
+    
+    self.searchBar = [[UISearchBar alloc] init];
+    self.searchBar.placeholder = @"Filter";
+    self.searchBar.delegate = self;
+    self.searchBar.showsScopeBar = YES;
+    self.searchBar.scopeButtonTitles = @[@"Sort Alphabetically", @"Sort by Count"];
+    [self.searchBar sizeToFit];
+    self.tableView.tableHeaderView = self.searchBar;
+    
+    self.refreshControl = [[UIRefreshControl alloc] init];
+    [self.refreshControl addTarget:self action:@selector(refreshControlDidRefresh:) forControlEvents:UIControlEventValueChanged];
+    
+    [self reloadTableData];
+}
+
+- (BOOL)prefersStatusBarHidden
+{
+    return YES;
+}
+
+- (NSArray *)allClassNames
+{
+    return [self.instanceCountsForClassNames allKeys];
+}
+
+- (void)reloadTableData
+{
+    // Set up a CFMutableDictionary with class pointer keys and NSUInteger values.
+    // We abuse CFMutableDictionary a little to have primitive keys through judicious casting, but it gets the job done.
+    // The dictionary is intialized with a 0 count for each class so that it doesn't have to expand during enumeration.
+    // While it might be a little cleaner to populate an NSMutableDictionary with class name string keys to NSNumber counts,
+    // we choose the CF/primitives approach because it lets us enumerate the objects in the heap without allocating any memory during enumeration.
+    // The alternative of creating one NSString/NSNumber per object on the heap ends up polluting the count of live objects quite a bit.
+    unsigned int count = 0;
+    Class *classes = objc_copyClassList(&count);
+    CFMutableDictionaryRef mutableCountsForClasses = CFDictionaryCreateMutable(NULL, count, NULL, NULL);
+    for (unsigned int i = 0; i < count; i++) {
+        CFDictionarySetValue(mutableCountsForClasses, (__bridge const void *)classes[i], (const void *)0);
+    }
+    
+    // Enumerate all objects on the heap to build the counts of instances for each class.
+    [FLEXHeapEnumerator enumerateLiveObjectsUsingBlock:^(__unsafe_unretained id object, __unsafe_unretained Class actualClass) {
+        NSUInteger count = (NSUInteger)CFDictionaryGetValue(mutableCountsForClasses, (__bridge const void *)actualClass);
+        count++;
+        CFDictionarySetValue(mutableCountsForClasses, (__bridge const void *)actualClass, (const void *)count);
+    }];
+    
+    // Convert our CF primitive dictionary into a nicer mapping of class name strings to counts that we will use as the table's model.
+    NSMutableDictionary *mutableCountsForClassNames = [NSMutableDictionary dictionary];
+    for (unsigned int i = 0; i < count; i++) {
+        Class class = classes[i];
+        NSUInteger count = (NSUInteger)CFDictionaryGetValue(mutableCountsForClasses, (__bridge const void *)(class));
+        if (count > 0) {
+            NSString *className = @(class_getName(class));
+            [mutableCountsForClassNames setObject:@(count) forKey:className];
+        }
+    }
+    free(classes);
+    
+    self.instanceCountsForClassNames = mutableCountsForClassNames;
+    
+    [self updateTableDataForSearchFilter];
+}
+
+- (void)refreshControlDidRefresh:(id)sender
+{
+    [self reloadTableData];
+    [self.refreshControl endRefreshing];
+}
+
+- (void)updateTitle
+{
+    NSString *title = @"Live Objects";
+    
+    NSUInteger totalCount = 0;
+    for (NSString *className in self.allClassNames) {
+        totalCount += [[self.instanceCountsForClassNames objectForKey:className] unsignedIntegerValue];
+    }
+    NSUInteger filteredCount = 0;
+    for (NSString *className in self.filteredClassNames) {
+        filteredCount += [[self.instanceCountsForClassNames objectForKey:className] unsignedIntegerValue];
+    }
+    
+    if (filteredCount == totalCount) {
+        // Unfiltered
+        title = [title stringByAppendingFormat:@" (%lu)", (unsigned long)totalCount];
+    } else {
+        title = [title stringByAppendingFormat:@" (filtered, %lu)", (unsigned long)filteredCount];
+    }
+    
+    self.title = title;
+}
+
+
+#pragma mark - Search
+
+- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
+{
+    [self updateTableDataForSearchFilter];
+}
+
+- (void)searchBar:(UISearchBar *)searchBar selectedScopeButtonIndexDidChange:(NSInteger)selectedScope
+{
+    [self updateTableDataForSearchFilter];
+}
+
+- (void)scrollViewDidScroll:(UIScrollView *)scrollView
+{
+    // Dismiss the keyboard when interacting with filtered results.
+    [self.searchBar endEditing:YES];
+}
+
+- (void)updateTableDataForSearchFilter
+{
+    if ([self.searchBar.text length] > 0) {
+        NSPredicate *searchPreidcate = [NSPredicate predicateWithFormat:@"SELF CONTAINS[cd] %@", self.searchBar.text];
+        self.filteredClassNames = [self.allClassNames filteredArrayUsingPredicate:searchPreidcate];
+    } else {
+        self.filteredClassNames = self.allClassNames;
+    }
+    
+    if (self.searchBar.selectedScopeButtonIndex == kFLEXLiveObjectsSortAlphabeticallyIndex) {
+        self.filteredClassNames = [self.filteredClassNames sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)];
+    } else if (self.searchBar.selectedScopeButtonIndex == kFLEXLiveObjectsSortByCountIndex) {
+        self.filteredClassNames = [self.filteredClassNames sortedArrayUsingComparator:^NSComparisonResult(NSString *className1, NSString *className2) {
+            NSNumber *count1 = [self.instanceCountsForClassNames objectForKey:className1];
+            NSNumber *count2 = [self.instanceCountsForClassNames objectForKey:className2];
+            // Reversed for descending counts.
+            return [count2 compare:count1];
+        }];
+    }
+    
+    [self updateTitle];
+    [self.tableView reloadData];
+}
+
+
+#pragma mark - Table view data source
+
+- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
+{
+    return 1;
+}
+
+- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
+{
+    return [self.filteredClassNames 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 = [FLEXUtility defaultTableViewCellLabelFont];
+    }
+    
+    NSString *className = self.filteredClassNames[indexPath.row];
+    NSNumber *count = [self.instanceCountsForClassNames objectForKey:className];
+    cell.textLabel.text = [NSString stringWithFormat:@"%@ (%ld)", className, (long)[count integerValue]];
+    
+    return cell;
+}
+
+
+#pragma mark - Table view delegate
+
+- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
+{
+    NSString *className = [self.filteredClassNames objectAtIndex:indexPath.row];
+    const char *classNameCString = [className UTF8String];
+    NSMutableArray *instances = [NSMutableArray array];
+    [FLEXHeapEnumerator enumerateLiveObjectsUsingBlock:^(__unsafe_unretained id object, __unsafe_unretained Class actualClass) {
+        if (strcmp(classNameCString, class_getName(actualClass)) == 0) {
+            // Note: objects of certain classes crash when retain is called. It is up to the user to avoid tapping into instance lists for these classes.
+            // Ex. OS_dispatch_queue_specific_queue
+            // In the future, we could provide some kind of warning for classes that are known to be problematic.
+            [instances addObject:object];
+        }
+    }];
+    FLEXInstancesTableViewController *instancesViewController = [[FLEXInstancesTableViewController alloc] init];
+    instancesViewController.instances = instances;
+    instancesViewController.title = [NSString stringWithFormat:@"%@ (%lu)", className, (unsigned long)[instances count]];
+    [self.navigationController pushViewController:instancesViewController animated:YES];
+}
+
+@end

+ 13 - 0
Classes/Object Explorers/FLEXArrayExplorerViewController.h

@@ -0,0 +1,13 @@
+//
+//  FLEXArrayExplorerViewController.h
+//  Flipboard
+//
+//  Created by Ryan Olson on 5/15/14.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import "FLEXObjectExplorerViewController.h"
+
+@interface FLEXArrayExplorerViewController : FLEXObjectExplorerViewController
+
+@end

+ 78 - 0
Classes/Object Explorers/FLEXArrayExplorerViewController.m

@@ -0,0 +1,78 @@
+//
+//  FLEXArrayExplorerViewController.m
+//  Flipboard
+//
+//  Created by Ryan Olson on 5/15/14.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import "FLEXArrayExplorerViewController.h"
+#import "FLEXRuntimeUtility.h"
+#import "FLEXObjectExplorerFactory.h"
+
+@interface FLEXArrayExplorerViewController ()
+
+@property (nonatomic, readonly) NSArray *array;
+
+@end
+
+@implementation FLEXArrayExplorerViewController
+
+- (NSArray *)array
+{
+    return [self.object isKindOfClass:[NSArray class]] ? self.object : nil;
+}
+
+
+#pragma mark - Superclass Overrides
+
+- (NSString *)customSectionTitle
+{
+    return @"Array Indices";
+}
+
+- (NSArray *)customSectionRowCookies
+{
+    // Use index numbers as the row cookies
+    NSMutableArray *cookies = [NSMutableArray arrayWithCapacity:[self.array count]];
+    for (NSUInteger i = 0; i < [self.array count]; i++) {
+        [cookies addObject:@(i)];
+    }
+    return cookies;
+}
+
+- (NSString *)customSectionTitleForRowCookie:(id)rowCookie
+{
+    return [rowCookie description];
+}
+
+- (NSString *)customSectionSubtitleForRowCookie:(id)rowCookie
+{
+    return [FLEXRuntimeUtility descriptionForIvarOrPropertyValue:[self detailObjectForRowCookie:rowCookie]];
+}
+
+- (BOOL)customSectionCanDrillIntoRowWithCookie:(id)rowCookie
+{
+    return YES;
+}
+
+- (UIViewController *)customSectionDrillInViewControllerForRowCookie:(id)rowCookie
+{
+    return [FLEXObjectExplorerFactory explorerViewControllerForObject:[self detailObjectForRowCookie:rowCookie]];
+}
+
+- (BOOL)shouldShowDescription
+{
+    return NO;
+}
+
+
+#pragma mark - Helpers
+
+- (id)detailObjectForRowCookie:(id)rowCookie
+{
+    NSUInteger index = [rowCookie unsignedIntegerValue];
+    return [self.array objectAtIndex:index];
+}
+
+@end

+ 13 - 0
Classes/Object Explorers/FLEXDefaultsExplorerViewController.h

@@ -0,0 +1,13 @@
+//
+//  FLEXDefaultsExplorerViewController.h
+//  Flipboard
+//
+//  Created by Ryan Olson on 5/23/14.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import "FLEXObjectExplorerViewController.h"
+
+@interface FLEXDefaultsExplorerViewController : FLEXObjectExplorerViewController
+
+@end

+ 73 - 0
Classes/Object Explorers/FLEXDefaultsExplorerViewController.m

@@ -0,0 +1,73 @@
+//
+//  FLEXDefaultsExplorerViewController.m
+//  Flipboard
+//
+//  Created by Ryan Olson on 5/23/14.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import "FLEXDefaultsExplorerViewController.h"
+#import "FLEXObjectExplorerFactory.h"
+#import "FLEXRuntimeUtility.h"
+#import "FLEXDefaultEditorViewController.h"
+
+@interface FLEXDefaultsExplorerViewController ()
+
+@property (nonatomic, readonly) NSUserDefaults *defaults;
+
+@end
+
+@implementation FLEXDefaultsExplorerViewController
+
+- (NSUserDefaults *)defaults
+{
+    return [self.object isKindOfClass:[NSUserDefaults class]] ? self.object : nil;
+}
+
+
+#pragma mark - Superclass Overrides
+
+- (NSString *)customSectionTitle
+{
+    return @"Defaults";
+}
+
+- (NSArray *)customSectionRowCookies
+{
+    return [[[self.defaults dictionaryRepresentation] allKeys] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)];
+}
+
+- (NSString *)customSectionTitleForRowCookie:(id)rowCookie
+{
+    return rowCookie;
+}
+
+- (NSString *)customSectionSubtitleForRowCookie:(id)rowCookie
+{
+    return [FLEXRuntimeUtility descriptionForIvarOrPropertyValue:[self.defaults objectForKey:rowCookie]];
+}
+
+- (BOOL)customSectionCanDrillIntoRowWithCookie:(id)rowCookie
+{
+    return YES;
+}
+
+- (UIViewController *)customSectionDrillInViewControllerForRowCookie:(id)rowCookie
+{
+    UIViewController *drillInViewController = nil;
+    NSString *key = rowCookie;
+    id drillInObject = [self.defaults objectForKey:key];
+    if ([FLEXDefaultEditorViewController canEditDefaultWithValue:drillInObject]) {
+        drillInViewController = [[FLEXDefaultEditorViewController alloc] initWithDefaults:self.defaults key:key];
+    } else {
+        drillInViewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:drillInObject];
+    }
+    return drillInViewController;
+}
+
+- (BOOL)shouldShowDescription
+{
+    return NO;
+}
+
+@end

+ 15 - 0
Classes/Object Explorers/FLEXDescriptionTableViewCell.h

@@ -0,0 +1,15 @@
+//
+//  FLEXDescriptionTableViewCell.h
+//  Flipboard
+//
+//  Created by Ryan Olson on 2014-05-05.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+
+@interface FLEXDescriptionTableViewCell : UITableViewCell
+
++ (CGFloat)preferredHeightWithText:(NSString *)text inTableViewWidth:(CGFloat)tableViewWidth;
+
+@end

+ 82 - 0
Classes/Object Explorers/FLEXDescriptionTableViewCell.m

@@ -0,0 +1,82 @@
+//
+//  FLEXDescriptionTableViewCell.m
+//  Flipboard
+//
+//  Created by Ryan Olson on 2014-05-05.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import "FLEXDescriptionTableViewCell.h"
+#import "FLEXUtility.h"
+
+@interface FLEXDescriptionTableViewCell ()
+
+@end
+
+@implementation FLEXDescriptionTableViewCell
+
+- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
+{
+    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
+    if (self) {
+        self.textLabel.numberOfLines = 0;
+        self.textLabel.font = [[self class] labelFont];
+    }
+    return self;
+}
+
+- (void)layoutSubviews
+{
+    [super layoutSubviews];
+    
+    self.textLabel.frame = UIEdgeInsetsInsetRect(self.contentView.bounds, [[self class] labelInsets]);
+}
+
++ (UIFont *)labelFont
+{
+    return [FLEXUtility defaultTableViewCellLabelFont];
+}
+
++ (UIEdgeInsets)labelInsets
+{
+    UIEdgeInsets labelInsets = UIEdgeInsetsZero;
+    labelInsets.top = 15.0;
+    labelInsets.bottom = 15.0;
+    if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_6_1) {
+        labelInsets.left = 15.0;
+        labelInsets.right = 0.0;
+    } else {
+        labelInsets.left = 10.0;
+        labelInsets.right = 10.0;
+    }
+    return labelInsets;
+}
+
++ (CGFloat)preferredHeightWithText:(NSString *)text inTableViewWidth:(CGFloat)tableViewWidth
+{
+    // Hardcoded margins from observation of cells in a grouped table on iOS 6.
+    // There is no API to get the insets of the content view proir to layout.
+    // Thankfully they removed the magic margins in iOS 7.
+    // Differences are between the content view's width and the table view's width
+    // Full screen iPhone - 20
+    // Full screen iPad - 90
+    
+    CGFloat labelWidth = tableViewWidth;
+    if (NSFoundationVersionNumber <= NSFoundationVersionNumber_iOS_6_1) {
+        if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
+            labelWidth -= 40.0;
+        } else {
+            labelWidth -= 90.0;
+        }
+    }
+    
+    UIEdgeInsets labelInsets = [self labelInsets];
+    labelWidth -= (labelInsets.left + labelInsets.right);
+    
+    CGFloat preferredLabelHeight = ceil([text sizeWithFont:[self labelFont] constrainedToSize:CGSizeMake(labelWidth, CGFLOAT_MAX)].height);
+    CGFloat preferredCellHeight = preferredLabelHeight + labelInsets.top + labelInsets.bottom + 1.0;
+    
+    return preferredCellHeight;
+}
+
+@end

+ 13 - 0
Classes/Object Explorers/FLEXDictionaryExplorerViewController.h

@@ -0,0 +1,13 @@
+//
+//  FLEXDictionaryExplorerViewController.h
+//  Flipboard
+//
+//  Created by Ryan Olson on 5/16/14.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import "FLEXObjectExplorerViewController.h"
+
+@interface FLEXDictionaryExplorerViewController : FLEXObjectExplorerViewController
+
+@end

+ 64 - 0
Classes/Object Explorers/FLEXDictionaryExplorerViewController.m

@@ -0,0 +1,64 @@
+//
+//  FLEXDictionaryExplorerViewController.m
+//  Flipboard
+//
+//  Created by Ryan Olson on 5/16/14.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import "FLEXDictionaryExplorerViewController.h"
+#import "FLEXRuntimeUtility.h"
+#import "FLEXObjectExplorerFactory.h"
+
+@interface FLEXDictionaryExplorerViewController ()
+
+@property (nonatomic, readonly) NSDictionary *dictionary;
+
+@end
+
+@implementation FLEXDictionaryExplorerViewController
+
+- (NSArray *)dictionary
+{
+    return [self.object isKindOfClass:[NSDictionary class]] ? self.object : nil;
+}
+
+
+#pragma mark - Superclass Overrides
+
+- (NSString *)customSectionTitle
+{
+    return @"Dictionary Objects";
+}
+
+- (NSArray *)customSectionRowCookies
+{
+    return [self.dictionary allKeys];
+}
+
+- (NSString *)customSectionTitleForRowCookie:(id)rowCookie
+{
+    return [FLEXRuntimeUtility descriptionForIvarOrPropertyValue:rowCookie];
+}
+
+- (NSString *)customSectionSubtitleForRowCookie:(id)rowCookie
+{
+    return [FLEXRuntimeUtility descriptionForIvarOrPropertyValue:[self.dictionary objectForKey:rowCookie]];
+}
+
+- (BOOL)customSectionCanDrillIntoRowWithCookie:(id)rowCookie
+{
+    return YES;
+}
+
+- (UIViewController *)customSectionDrillInViewControllerForRowCookie:(id)rowCookie
+{
+    return [FLEXObjectExplorerFactory explorerViewControllerForObject:[self.dictionary objectForKey:rowCookie]];
+}
+
+- (BOOL)shouldShowDescription
+{
+    return NO;
+}
+
+@end

+ 17 - 0
Classes/Object Explorers/FLEXObjectExplorerFactory.h

@@ -0,0 +1,17 @@
+//
+//  FLEXObjectExplorerFactory.h
+//  Flipboard
+//
+//  Created by Ryan Olson on 5/15/14.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+
+@class FLEXObjectExplorerViewController;
+
+@interface FLEXObjectExplorerFactory : NSObject
+
++ (FLEXObjectExplorerViewController *)explorerViewControllerForObject:(id)object;
+
+@end

+ 49 - 0
Classes/Object Explorers/FLEXObjectExplorerFactory.m

@@ -0,0 +1,49 @@
+//
+//  FLEXObjectExplorerFactory.m
+//  Flipboard
+//
+//  Created by Ryan Olson on 5/15/14.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import "FLEXObjectExplorerFactory.h"
+#import "FLEXObjectExplorerViewController.h"
+#import "FLEXArrayExplorerViewController.h"
+#import "FLEXSetExplorerViewController.h"
+#import "FLEXDictionaryExplorerViewController.h"
+#import "FLEXDefaultsExplorerViewController.h"
+
+@implementation FLEXObjectExplorerFactory
+
++ (FLEXObjectExplorerViewController *)explorerViewControllerForObject:(id)object
+{
+    // Bail for nil object. We can't explore nil.
+    if (!object) {
+        return nil;
+    }
+    
+    static NSDictionary *explorerSubclassesForObjectTypeStrings = nil;
+    static dispatch_once_t once;
+    dispatch_once(&once, ^{
+        explorerSubclassesForObjectTypeStrings = @{NSStringFromClass([NSArray class])        : [FLEXArrayExplorerViewController class],
+                                                   NSStringFromClass([NSSet class])          : [FLEXSetExplorerViewController class],
+                                                   NSStringFromClass([NSDictionary class])   : [FLEXDictionaryExplorerViewController class],
+                                                   NSStringFromClass([NSUserDefaults class]) : [FLEXDefaultsExplorerViewController class]};
+    });
+    
+    Class explorerClass = [FLEXObjectExplorerViewController class];
+    for (NSString *objectTypeString in explorerSubclassesForObjectTypeStrings) {
+        Class objectClass = NSClassFromString(objectTypeString);
+        if ([object isKindOfClass:objectClass]) {
+            explorerClass = [explorerSubclassesForObjectTypeStrings objectForKey:objectTypeString];
+            break;
+        }
+    }
+    
+    FLEXObjectExplorerViewController *explorerViewController = [[explorerClass alloc] init];
+    explorerViewController.object = object;
+    
+    return explorerViewController;
+}
+
+@end

+ 29 - 0
Classes/Object Explorers/FLEXObjectExplorerViewController.h

@@ -0,0 +1,29 @@
+//
+//  FLEXObjectExplorerViewController.h
+//  Flipboard
+//
+//  Created by Ryan Olson on 2014-05-03.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+
+@interface FLEXObjectExplorerViewController : UITableViewController
+
+@property (nonatomic, strong) id object;
+
+// Sublasses can override the methods below to provide data in a custom section.
+// The subclass should provide an array of "row cookies" to allow retreival of individual row data later on.
+// The objects in the rowCookies array will be used to call the row title, subtitle, etc methods to consturct the rows.
+// The cookies approach is used here because we may filter the visible rows based on the search text entered by the user.
+- (NSString *)customSectionTitle;
+- (NSArray *)customSectionRowCookies;
+- (NSString *)customSectionTitleForRowCookie:(id)rowCookie;
+- (NSString *)customSectionSubtitleForRowCookie:(id)rowCookie;
+- (BOOL)customSectionCanDrillIntoRowWithCookie:(id)rowCookie;
+- (UIViewController *)customSectionDrillInViewControllerForRowCookie:(id)rowCookie;
+
+// If the custom section data makes the description redundant, subclasses can choose to hide it.
+- (BOOL)shouldShowDescription;
+
+@end

+ 851 - 0
Classes/Object Explorers/FLEXObjectExplorerViewController.m

@@ -0,0 +1,851 @@
+//
+//  FLEXObjectExplorerViewController.m
+//  Flipboard
+//
+//  Created by Ryan Olson on 2014-05-03.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import "FLEXObjectExplorerViewController.h"
+#import "FLEXUtility.h"
+#import "FLEXRuntimeUtility.h"
+#import "FLEXDescriptionTableViewCell.h"
+#import "FLEXObjectExplorerFactory.h"
+#import "FLEXPropertyEditorViewController.h"
+#import "FLEXIvarEditorViewController.h"
+#import "FLEXMethodCallingViewController.h"
+#import <objc/runtime.h>
+
+typedef NS_ENUM(NSUInteger, FLEXObjectExplorerSection) {
+    FLEXObjectExplorerSectionDescription,
+    FLEXObjectExplorerSectionCustom,
+    FLEXObjectExplorerSectionProperties,
+    FLEXObjectExplorerSectionIvars,
+    FLEXObjectExplorerSectionMethods,
+    FLEXObjectExplorerSectionClassMethods
+};
+
+// Convenience boxes to keep runtime properties, ivars, and methods in foundation collections.
+@interface FLEXPropertyBox : NSObject
+@property (nonatomic, assign) objc_property_t property;
+@end
+@implementation FLEXPropertyBox
+@end
+
+@interface FLEXIvarBox : NSObject
+@property (nonatomic, assign) Ivar ivar;
+@end
+@implementation FLEXIvarBox
+@end
+
+@interface FLEXMethodBox : NSObject
+@property (nonatomic, assign) Method method;
+@end
+@implementation FLEXMethodBox
+@end
+
+static const NSInteger kFLEXObjectExplorerScopeNoInheritanceIndex = 0;
+static const NSInteger kFLEXObjectExplorerScopeIncludeInheritanceIndex = 1;
+
+@interface FLEXObjectExplorerViewController () <UISearchBarDelegate>
+
+@property (nonatomic, strong) NSArray *properties;
+@property (nonatomic, strong) NSArray *inheritedProperties;
+@property (nonatomic, strong) NSArray *filteredProperties;
+
+@property (nonatomic, strong) NSArray *ivars;
+@property (nonatomic, strong) NSArray *inheritedIvars;
+@property (nonatomic, strong) NSArray *filteredIvars;
+
+@property (nonatomic, strong) NSArray *methods;
+@property (nonatomic, strong) NSArray *inheritedMethods;
+@property (nonatomic, strong) NSArray *filteredMethods;
+
+@property (nonatomic, strong) NSArray *classMethods;
+@property (nonatomic, strong) NSArray *inheritedClassMethods;
+@property (nonatomic, strong) NSArray *filteredClassMethods;
+
+@property (nonatomic, strong) NSArray *cachedCustomSectionRowCookies;
+@property (nonatomic, strong) NSIndexSet *customSectionVisibleIndexes;
+
+@property (nonatomic, strong) UISearchBar *searchBar;
+@property (nonatomic, strong) NSString *filterText;
+@property (nonatomic, assign) BOOL includeInheritance;
+
+@end
+
+@implementation FLEXObjectExplorerViewController
+
+- (id)initWithStyle:(UITableViewStyle)style
+{
+    // Force grouped style
+    return [super initWithStyle:UITableViewStyleGrouped];
+}
+
+- (void)viewDidLoad
+{
+    [super viewDidLoad];
+    
+    self.searchBar = [[UISearchBar alloc] init];
+    self.searchBar.placeholder = @"Filter";
+    self.searchBar.delegate = self;
+    self.searchBar.showsScopeBar = YES;
+    self.searchBar.scopeButtonTitles = @[@"No Inheritance", @"Include Inheritance"];
+    [self.searchBar sizeToFit];
+    self.tableView.tableHeaderView = self.searchBar;
+}
+
+- (void)viewWillAppear:(BOOL)animated
+{
+    NSMutableArray *visibleIndexPaths = [NSMutableArray array];
+    for (UITableViewCell *cell in [self.tableView visibleCells]) {
+        [visibleIndexPaths addObject:[self.tableView indexPathForCell:cell]];
+    }
+    [self.tableView reloadRowsAtIndexPaths:visibleIndexPaths withRowAnimation:UITableViewRowAnimationNone];
+}
+
+- (BOOL)prefersStatusBarHidden
+{
+    return YES;
+}
+
+- (void)scrollViewDidScroll:(UIScrollView *)scrollView
+{
+    [self.searchBar endEditing:YES];
+}
+
+
+#pragma mark - Search
+
+- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
+{
+    self.filterText = searchText;
+}
+
+- (void)searchBar:(UISearchBar *)searchBar selectedScopeButtonIndexDidChange:(NSInteger)selectedScope
+{
+    if (selectedScope == kFLEXObjectExplorerScopeIncludeInheritanceIndex) {
+        self.includeInheritance = YES;
+    } else if (selectedScope == kFLEXObjectExplorerScopeNoInheritanceIndex) {
+        self.includeInheritance = NO;
+    }
+}
+
+#pragma mark - Setter overrides
+
+- (void)setObject:(id)object
+{
+    _object = object;
+    self.title = [[object class] description];
+    [self updateTableData];
+}
+
+- (void)setIncludeInheritance:(BOOL)includeInheritance
+{
+    if (_includeInheritance != includeInheritance) {
+        _includeInheritance = includeInheritance;
+        [self updateDisplayedData];
+    }
+}
+
+- (void)setFilterText:(NSString *)filterText
+{
+    if (_filterText != filterText || ![_filterText isEqual:filterText]) {
+        _filterText = filterText;
+        [self updateDisplayedData];
+    }
+}
+
+
+#pragma mark - Reloading
+
+- (void)updateTableData
+{
+    [self updateCustomData];
+    [self updateProperties];
+    [self updateIvars];
+    [self updateMethods];
+    [self updateClassMethods];
+    [self updateDisplayedData];
+}
+
+- (void)updateDisplayedData
+{
+    [self updateFilteredCustomData];
+    [self updateFilteredProperties];
+    [self updateFilteredIvars];
+    [self updateFilteredMethods];
+    [self updateFilteredClassMethods];
+    [self.tableView reloadData];
+}
+
+- (BOOL)objectIsClass
+{
+    return self.object && [self.object class] == self.object;
+}
+
+- (BOOL)shouldShowDescription
+{
+    return ![self objectIsClass] && [[FLEXUtility safeDescriptionForObject:self.object] length] > 0;
+}
+
+
+#pragma mark - Properties
+
+- (void)updateProperties
+{
+    Class class = [self.object class];
+    self.properties = [[self class] propertiesForClass:class];
+    self.inheritedProperties = [[self class] inheritedPropertiesForClass:class];
+}
+
++ (NSArray *)propertiesForClass:(Class)class
+{
+    NSMutableArray *boxedProperties = [NSMutableArray array];
+    unsigned int propertyCount = 0;
+    objc_property_t *propertyList = class_copyPropertyList(class, &propertyCount);
+    if (propertyList) {
+        for (int i = 0; i < propertyCount; i++) {
+            FLEXPropertyBox *propertyBox = [[FLEXPropertyBox alloc] init];
+            propertyBox.property = propertyList[i];
+            [boxedProperties addObject:propertyBox];
+        }
+        free(propertyList);
+    }
+    return boxedProperties;
+}
+
++ (NSArray *)inheritedPropertiesForClass:(Class)class
+{
+    NSMutableArray *inheritedProperties = [NSMutableArray array];
+    while ((class = [class superclass])) {
+        [inheritedProperties addObjectsFromArray:[self propertiesForClass:class]];
+    }
+    return inheritedProperties;
+}
+
+- (void)updateFilteredProperties
+{
+    NSArray *candidateProperties = self.properties;
+    if (self.includeInheritance) {
+        candidateProperties = [candidateProperties arrayByAddingObjectsFromArray:self.inheritedProperties];
+    }
+    
+    NSArray *unsortedFilteredProperties = nil;
+    if ([self.filterText length] > 0) {
+        NSMutableArray *mutableUnsortedFilteredProperties = [NSMutableArray array];
+        for (FLEXPropertyBox *propertyBox in candidateProperties) {
+            NSString *prettyName = [FLEXRuntimeUtility prettyNameForProperty:propertyBox.property];
+            if ([prettyName rangeOfString:self.filterText options:NSCaseInsensitiveSearch].location != NSNotFound) {
+                [mutableUnsortedFilteredProperties addObject:propertyBox];
+            }
+        }
+        unsortedFilteredProperties = mutableUnsortedFilteredProperties;
+    } else {
+        unsortedFilteredProperties = candidateProperties;
+    }
+    
+    self.filteredProperties = [unsortedFilteredProperties sortedArrayUsingComparator:^NSComparisonResult(FLEXPropertyBox *propertyBox1, FLEXPropertyBox *propertyBox2) {
+        NSString *name1 = [NSString stringWithUTF8String:property_getName(propertyBox1.property)];
+        NSString *name2 = [NSString stringWithUTF8String:property_getName(propertyBox2.property)];
+        return [name1 caseInsensitiveCompare:name2];
+    }];
+}
+
+- (NSString *)titleForPropertyAtIndex:(NSInteger)index
+{
+    FLEXPropertyBox *propertyBox = [self.filteredProperties objectAtIndex:index];
+    return [FLEXRuntimeUtility prettyNameForProperty:propertyBox.property];
+}
+
+- (id)valueForPropertyAtIndex:(NSInteger)index
+{
+    id value = nil;
+    if (![self objectIsClass]) {
+        FLEXPropertyBox *propertyBox = [self.filteredProperties objectAtIndex:index];
+        value = [FLEXRuntimeUtility valueForProperty:propertyBox.property onObject:self.object];
+    }
+    return value;
+}
+
+
+#pragma mark - Ivars
+
+- (void)updateIvars
+{
+    Class class = [self.object class];
+    self.ivars = [[self class] ivarsForClass:class];
+    self.inheritedIvars = [[self class] inheritedIvarsForClass:class];
+}
+
++ (NSArray *)ivarsForClass:(Class)class
+{
+    NSMutableArray *boxedIvars = [NSMutableArray array];
+    unsigned int ivarCount = 0;
+    Ivar *ivarList = class_copyIvarList(class, &ivarCount);
+    if (ivarList) {
+        for (int i = 0; i < ivarCount; i++) {
+            FLEXIvarBox *ivarBox = [[FLEXIvarBox alloc] init];
+            ivarBox.ivar = ivarList[i];
+            [boxedIvars addObject:ivarBox];
+        }
+        free(ivarList);
+    }
+    return boxedIvars;
+}
+
++ (NSArray *)inheritedIvarsForClass:(Class)class
+{
+    NSMutableArray *inheritedIvars = [NSMutableArray array];
+    while ((class = [class superclass])) {
+        [inheritedIvars addObjectsFromArray:[self ivarsForClass:class]];
+    }
+    return inheritedIvars;
+}
+
+- (void)updateFilteredIvars
+{
+    NSArray *candidateIvars = self.ivars;
+    if (self.includeInheritance) {
+        candidateIvars = [candidateIvars arrayByAddingObjectsFromArray:self.inheritedIvars];
+    }
+    
+    NSArray *unsortedFilteredIvars = nil;
+    if ([self.filterText length] > 0) {
+        NSMutableArray *mutableUnsortedFilteredIvars = [NSMutableArray array];
+        for (FLEXIvarBox *ivarBox in candidateIvars) {
+            NSString *prettyName = [FLEXRuntimeUtility prettyNameForIvar:ivarBox.ivar];
+            if ([prettyName rangeOfString:self.filterText options:NSCaseInsensitiveSearch].location != NSNotFound) {
+                [mutableUnsortedFilteredIvars addObject:ivarBox];
+            }
+        }
+        unsortedFilteredIvars = mutableUnsortedFilteredIvars;
+    } else {
+        unsortedFilteredIvars = candidateIvars;
+    }
+    
+    self.filteredIvars = [unsortedFilteredIvars sortedArrayUsingComparator:^NSComparisonResult(FLEXIvarBox *ivarBox1, FLEXIvarBox *ivarBox2) {
+        NSString *name1 = [NSString stringWithUTF8String:ivar_getName(ivarBox1.ivar)];
+        NSString *name2 = [NSString stringWithUTF8String:ivar_getName(ivarBox2.ivar)];
+        return [name1 caseInsensitiveCompare:name2];
+    }];
+}
+
+- (NSString *)titleForIvarAtIndex:(NSInteger)index
+{
+    FLEXIvarBox *ivarBox = [self.filteredIvars objectAtIndex:index];
+    return [FLEXRuntimeUtility prettyNameForIvar:ivarBox.ivar];
+}
+
+- (id)valueForIvarAtIndex:(NSInteger)index
+{
+    id value = nil;
+    if (![self objectIsClass]) {
+        FLEXIvarBox *ivarBox = [self.filteredIvars objectAtIndex:index];
+        value = [FLEXRuntimeUtility valueForIvar:ivarBox.ivar onObject:self.object];
+    }
+    return value;
+}
+
+
+#pragma mark - Methods
+
+- (void)updateMethods
+{
+    Class class = [self.object class];
+    self.methods = [[self class] methodsForClass:class];
+    self.inheritedMethods = [[self class] inheritedMethodsForClass:class];
+}
+
+- (void)updateFilteredMethods
+{
+    self.filteredMethods = [self filteredMethodsFromMethods:self.methods inheritedMethods:self.inheritedMethods areClassMethods:NO];
+}
+
+- (void)updateClassMethods
+{
+    const char *className = [NSStringFromClass([self.object class]) UTF8String];
+    Class metaClass = objc_getMetaClass(className);
+    self.classMethods = [[self class] methodsForClass:metaClass];
+    self.inheritedClassMethods = [[self class] inheritedMethodsForClass:metaClass];
+}
+
+- (void)updateFilteredClassMethods
+{
+    self.filteredClassMethods = [self filteredMethodsFromMethods:self.classMethods inheritedMethods:self.inheritedClassMethods areClassMethods:YES];
+}
+
++ (NSArray *)methodsForClass:(Class)class
+{
+    NSMutableArray *boxedMethods = [NSMutableArray array];
+    unsigned int methodCount = 0;
+    Method *methodList = class_copyMethodList(class, &methodCount);
+    if (methodList) {
+        for (int i = 0; i < methodCount; i++) {
+            FLEXMethodBox *methodBox = [[FLEXMethodBox alloc] init];
+            methodBox.method = methodList[i];
+            [boxedMethods addObject:methodBox];
+        }
+        free(methodList);
+    }
+    return boxedMethods;
+}
+
++ (NSArray *)inheritedMethodsForClass:(Class)class
+{
+    NSMutableArray *inheritedMethods = [NSMutableArray array];
+    while ((class = [class superclass])) {
+        [inheritedMethods addObjectsFromArray:[self methodsForClass:class]];
+    }
+    return inheritedMethods;
+}
+
+- (NSArray *)filteredMethodsFromMethods:(NSArray *)methods inheritedMethods:(NSArray *)inheritedMethods areClassMethods:(BOOL)areClassMethods
+{
+    NSArray *candidateMethods = methods;
+    if (self.includeInheritance) {
+        candidateMethods = [candidateMethods arrayByAddingObjectsFromArray:inheritedMethods];
+    }
+    
+    NSArray *unsortedFilteredMethods = nil;
+    if ([self.filterText length] > 0) {
+        NSMutableArray *mutableUnsortedFilteredMethods = [NSMutableArray array];
+        for (FLEXMethodBox *methodBox in candidateMethods) {
+            NSString *prettyName = [FLEXRuntimeUtility prettyNameForMethod:methodBox.method isClassMethod:areClassMethods];
+            if ([prettyName rangeOfString:self.filterText options:NSCaseInsensitiveSearch].location != NSNotFound) {
+                [mutableUnsortedFilteredMethods addObject:methodBox];
+            }
+        }
+        unsortedFilteredMethods = mutableUnsortedFilteredMethods;
+    } else {
+        unsortedFilteredMethods = candidateMethods;
+    }
+    
+    NSArray *sortedFilteredMethods = [unsortedFilteredMethods sortedArrayUsingComparator:^NSComparisonResult(FLEXMethodBox *methodBox1, FLEXMethodBox *methodBox2) {
+        NSString *name1 = NSStringFromSelector(method_getName(methodBox1.method));
+        NSString *name2 = NSStringFromSelector(method_getName(methodBox2.method));
+        return [name1 caseInsensitiveCompare:name2];
+    }];
+    
+    return sortedFilteredMethods;
+}
+
+- (NSString *)titleForMethodAtIndex:(NSInteger)index
+{
+    FLEXMethodBox *methodBox = [self.filteredMethods objectAtIndex:index];
+    return [FLEXRuntimeUtility prettyNameForMethod:methodBox.method isClassMethod:NO];
+}
+
+- (NSString *)titleForClassMethodAtIndex:(NSInteger)index
+{
+    FLEXMethodBox *classMethodBox = [self.filteredClassMethods objectAtIndex:index];
+    return [FLEXRuntimeUtility prettyNameForMethod:classMethodBox.method isClassMethod:YES];
+}
+
+
+#pragma mark - Table View Data Helpers
+
+- (NSArray *)visibleExplorerSections
+{
+    NSMutableArray *visibleSections = [NSMutableArray array];
+    NSArray *possibleSections = @[@(FLEXObjectExplorerSectionDescription),
+                                  @(FLEXObjectExplorerSectionCustom),
+                                  @(FLEXObjectExplorerSectionProperties),
+                                  @(FLEXObjectExplorerSectionIvars),
+                                  @(FLEXObjectExplorerSectionMethods),
+                                  @(FLEXObjectExplorerSectionClassMethods)];
+    
+    for (NSNumber *possibleSection in possibleSections) {
+        FLEXObjectExplorerSection explorerSection = [possibleSection unsignedIntegerValue];
+        if ([self numberOfRowsForExplorerSection:explorerSection] > 0) {
+            [visibleSections addObject:possibleSection];
+        }
+    }
+    
+    return visibleSections;
+}
+
+- (NSString *)sectionTitleWithBaseName:(NSString *)baseName totalCount:(NSUInteger)totalCount filteredCount:(NSUInteger)filteredCount
+{
+    NSString *sectionTitle = nil;
+    if (totalCount == filteredCount) {
+        sectionTitle = [baseName stringByAppendingFormat:@" (%lu)", (unsigned long)totalCount];
+    } else {
+        sectionTitle = [baseName stringByAppendingFormat:@" (%lu of %lu)", (unsigned long)filteredCount, (unsigned long)totalCount];
+    }
+    return sectionTitle;
+}
+
+- (FLEXObjectExplorerSection)explorerSectionAtIndex:(NSInteger)sectionIndex
+{
+    return [[[self visibleExplorerSections] objectAtIndex:sectionIndex] unsignedIntegerValue];
+}
+
+- (NSInteger)numberOfRowsForExplorerSection:(FLEXObjectExplorerSection)section
+{
+    NSInteger numberOfRows = 0;
+    switch (section) {
+        case FLEXObjectExplorerSectionDescription:
+            numberOfRows = [self shouldShowDescription] ? 1 : 0;
+            break;
+            
+        case FLEXObjectExplorerSectionCustom:
+            numberOfRows = [self.customSectionVisibleIndexes count];
+            break;
+            
+        case FLEXObjectExplorerSectionProperties:
+            numberOfRows = [self.filteredProperties count];
+            break;
+            
+        case FLEXObjectExplorerSectionIvars:
+            numberOfRows = [self.filteredIvars count];
+            break;
+            
+        case FLEXObjectExplorerSectionMethods:
+            numberOfRows = [self.filteredMethods count];
+            break;
+            
+        case FLEXObjectExplorerSectionClassMethods:
+            numberOfRows = [self.filteredClassMethods count];
+            break;
+    }
+    return numberOfRows;
+}
+
+- (NSString *)titleForRow:(NSInteger)row inExplorerSection:(FLEXObjectExplorerSection)section
+{
+    NSString *title = nil;
+    switch (section) {
+        case FLEXObjectExplorerSectionDescription:
+            title = [FLEXUtility safeDescriptionForObject:self.object];
+            break;
+            
+        case FLEXObjectExplorerSectionCustom:
+            title = [self customSectionTitleForRowCookie:[self customSectionRowCookieForVisibleRow:row]];
+            break;
+            
+        case FLEXObjectExplorerSectionProperties:
+            title = [self titleForPropertyAtIndex:row];
+            break;
+            
+        case FLEXObjectExplorerSectionIvars:
+            title = [self titleForIvarAtIndex:row];
+            break;
+            
+        case FLEXObjectExplorerSectionMethods:
+            title = [self titleForMethodAtIndex:row];
+            break;
+            
+        case FLEXObjectExplorerSectionClassMethods:
+            title = [self titleForClassMethodAtIndex:row];
+            break;
+    }
+    return title;
+}
+
+- (NSString *)subtitleForRow:(NSInteger)row inExplorerSection:(FLEXObjectExplorerSection)section
+{
+    // No subtitles for if we're showing a class rather than an instance of the class.
+    if ([self objectIsClass]) {
+        return nil;
+    }
+    
+    NSString *subtitle = nil;
+    switch (section) {
+        case FLEXObjectExplorerSectionDescription:
+            break;
+            
+        case FLEXObjectExplorerSectionCustom:
+            subtitle = [self customSectionSubtitleForRowCookie:[self customSectionRowCookieForVisibleRow:row]];
+            break;
+            
+        case FLEXObjectExplorerSectionProperties:
+            subtitle = [FLEXRuntimeUtility descriptionForIvarOrPropertyValue:[self valueForPropertyAtIndex:row]];
+            break;
+            
+        case FLEXObjectExplorerSectionIvars:
+            subtitle = [FLEXRuntimeUtility descriptionForIvarOrPropertyValue:[self valueForIvarAtIndex:row]];
+            break;
+            
+        case FLEXObjectExplorerSectionMethods:
+            break;
+            
+        case FLEXObjectExplorerSectionClassMethods:
+            break;
+    }
+    return subtitle;
+}
+
+- (BOOL)canDrillInToRow:(NSInteger)row inExplorerSection:(FLEXObjectExplorerSection)section
+{
+    BOOL canDrillIn = NO;
+    BOOL objectIsClass = [self objectIsClass];
+    switch (section) {
+        case FLEXObjectExplorerSectionDescription:
+            break;
+            
+        case FLEXObjectExplorerSectionCustom:
+            canDrillIn = [self customSectionCanDrillIntoRowWithCookie:[self customSectionRowCookieForVisibleRow:row]];
+            break;
+            
+        case FLEXObjectExplorerSectionProperties: {
+            if (!objectIsClass) {
+                objc_property_t property = [[self.filteredProperties objectAtIndex:row] property];
+                id currentValue = [self valueForPropertyAtIndex:row];
+                BOOL canEdit = [FLEXPropertyEditorViewController canEditProperty:property currentValue:currentValue];
+                BOOL canExplore = currentValue != nil;
+                canDrillIn = canEdit || canExplore;
+            }
+        }   break;
+            
+        case FLEXObjectExplorerSectionIvars: {
+            if (!objectIsClass) {
+                Ivar ivar = [[self.filteredIvars objectAtIndex:row] ivar];
+                id currentValue = [self valueForIvarAtIndex:row];
+                BOOL canEdit = [FLEXIvarEditorViewController canEditIvar:ivar currentValue:currentValue];
+                BOOL canExplore = currentValue != nil;
+                canDrillIn = canEdit || canExplore;
+            }
+        }   break;
+            
+        case FLEXObjectExplorerSectionMethods:
+            canDrillIn = !objectIsClass;
+            break;
+            
+        case FLEXObjectExplorerSectionClassMethods:
+            canDrillIn = YES;
+            break;
+    }
+    return canDrillIn;
+}
+
+- (NSString *)titleForExplorerSection:(FLEXObjectExplorerSection)section
+{
+    NSString *title = nil;
+    switch (section) {
+        case FLEXObjectExplorerSectionDescription: {
+            title = @"Description";
+        } break;
+            
+        case FLEXObjectExplorerSectionCustom: {
+            title = [self customSectionTitle];
+        } break;
+            
+        case FLEXObjectExplorerSectionProperties: {
+            NSUInteger totalCount = [self.properties count];
+            if (self.includeInheritance) {
+                totalCount += [self.inheritedProperties count];
+            }
+            title = [self sectionTitleWithBaseName:@"Properties" totalCount:totalCount filteredCount:[self.filteredProperties count]];
+        } break;
+            
+        case FLEXObjectExplorerSectionIvars: {
+            NSUInteger totalCount = [self.ivars count];
+            if (self.includeInheritance) {
+                totalCount += [self.inheritedIvars count];
+            }
+            title = [self sectionTitleWithBaseName:@"Ivars" totalCount:totalCount filteredCount:[self.filteredIvars count]];
+        } break;
+            
+        case FLEXObjectExplorerSectionMethods: {
+            NSUInteger totalCount = [self.methods count];
+            if (self.includeInheritance) {
+                totalCount += [self.inheritedMethods count];
+            }
+            title = [self sectionTitleWithBaseName:@"Methods" totalCount:totalCount filteredCount:[self.filteredMethods count]];
+        } break;
+            
+        case FLEXObjectExplorerSectionClassMethods: {
+            NSUInteger totalCount = [self.classMethods count];
+            if (self.includeInheritance) {
+                totalCount += [self.inheritedClassMethods count];
+            }
+            title = [self sectionTitleWithBaseName:@"Class Methods" totalCount:totalCount filteredCount:[self.filteredClassMethods count]];
+        } break;
+    }
+    return title;
+}
+
+- (UIViewController *)drillInViewControllerForRow:(NSUInteger)row inExplorerSection:(FLEXObjectExplorerSection)section
+{
+    UIViewController *viewController = nil;
+    switch (section) {
+        case FLEXObjectExplorerSectionDescription:
+            break;
+            
+        case FLEXObjectExplorerSectionCustom:
+            viewController = [self customSectionDrillInViewControllerForRowCookie:[self customSectionRowCookieForVisibleRow:row]];
+            break;
+            
+        case FLEXObjectExplorerSectionProperties: {
+            objc_property_t property = [[self.filteredProperties objectAtIndex:row] property];
+            id currentValue = [self valueForPropertyAtIndex:row];
+            if ([FLEXPropertyEditorViewController canEditProperty:property currentValue:currentValue]) {
+                viewController = [[FLEXPropertyEditorViewController alloc] initWithTarget:self.object property:property];
+            } else if (currentValue) {
+                viewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:currentValue];
+            }
+        } break;
+            
+        case FLEXObjectExplorerSectionIvars: {
+            Ivar ivar = [[self.filteredIvars objectAtIndex:row] ivar];
+            id currentValue = [self valueForIvarAtIndex:row];
+            if ([FLEXIvarEditorViewController canEditIvar:ivar currentValue:currentValue]) {
+                viewController = [[FLEXIvarEditorViewController alloc] initWithTarget:self.object ivar:ivar];
+            } else if (currentValue) {
+                viewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:currentValue];
+            }
+        } break;
+            
+        case FLEXObjectExplorerSectionMethods: {
+            Method method = [[self.filteredMethods objectAtIndex:row] method];
+            viewController = [[FLEXMethodCallingViewController alloc] initWithTarget:self.object method:method];
+        } break;
+            
+        case FLEXObjectExplorerSectionClassMethods: {
+            Method method = [[self.filteredClassMethods objectAtIndex:row] method];
+            viewController = [[FLEXMethodCallingViewController alloc] initWithTarget:[self.object class] method:method];
+        } break;
+    }
+    return viewController;
+}
+
+
+#pragma mark - Table View Data Source
+
+- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
+{
+    return [[self visibleExplorerSections] count];
+}
+
+- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
+{
+    FLEXObjectExplorerSection explorerSection = [self explorerSectionAtIndex:section];
+    return [self numberOfRowsForExplorerSection:explorerSection];
+}
+
+- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
+{
+    FLEXObjectExplorerSection explorerSection = [self explorerSectionAtIndex:section];
+    return [self titleForExplorerSection:explorerSection];
+}
+
+- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
+{
+    FLEXObjectExplorerSection explorerSection = [self explorerSectionAtIndex:indexPath.section];
+    
+    BOOL useDescriptionCell = explorerSection == FLEXObjectExplorerSectionDescription;
+    NSString *cellIdentifier = useDescriptionCell ? @"descriptionCell" : @"cell";
+    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
+    if (!cell) {
+        if (useDescriptionCell) {
+            cell = [[FLEXDescriptionTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];
+        } else {
+            cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:cellIdentifier];
+            UIFont *cellFont = [FLEXUtility defaultTableViewCellLabelFont];
+            cell.textLabel.font = cellFont;
+            cell.detailTextLabel.font = cellFont;
+            cell.detailTextLabel.textColor = [UIColor grayColor];
+        }
+    }
+    
+    cell.textLabel.text = [self titleForRow:indexPath.row inExplorerSection:explorerSection];
+    cell.detailTextLabel.text = [self subtitleForRow:indexPath.row inExplorerSection:explorerSection];
+    cell.accessoryType = [self canDrillInToRow:indexPath.row inExplorerSection:explorerSection] ? UITableViewCellAccessoryDisclosureIndicator : UITableViewCellAccessoryNone;
+    
+    return cell;
+}
+
+- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
+{
+    FLEXObjectExplorerSection explorerSection = [self explorerSectionAtIndex:indexPath.section];
+    CGFloat height = self.tableView.rowHeight;
+    if (explorerSection == FLEXObjectExplorerSectionDescription) {
+        NSString *text = [self titleForRow:indexPath.row inExplorerSection:explorerSection];
+        CGFloat preferredHeight = [FLEXDescriptionTableViewCell preferredHeightWithText:text inTableViewWidth:self.tableView.frame.size.width];
+        height = MAX(height, preferredHeight);
+    }
+    return height;
+}
+
+
+#pragma mark - Table View Delegate
+
+- (BOOL)tableView:(UITableView *)tableView shouldHighlightRowAtIndexPath:(NSIndexPath *)indexPath
+{
+    FLEXObjectExplorerSection explorerSection = [self explorerSectionAtIndex:indexPath.section];
+    return [self canDrillInToRow:indexPath.row inExplorerSection:explorerSection];
+}
+
+- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
+{
+    FLEXObjectExplorerSection explorerSection = [self explorerSectionAtIndex:indexPath.section];
+    UIViewController *detailViewController = [self drillInViewControllerForRow:indexPath.row inExplorerSection:explorerSection];
+    if (detailViewController) {
+        [self.navigationController pushViewController:detailViewController animated:YES];
+    }
+}
+
+
+#pragma mark - Custom Section
+
+- (void)updateCustomData
+{
+    self.cachedCustomSectionRowCookies = [self customSectionRowCookies];
+}
+
+- (void)updateFilteredCustomData
+{
+    NSIndexSet *filteredIndexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [self.cachedCustomSectionRowCookies count])];
+    if ([self.filterText length] > 0) {
+        filteredIndexSet = [filteredIndexSet indexesPassingTest:^BOOL(NSUInteger index, BOOL *stop) {
+            BOOL matches = NO;
+            NSString *rowTitle = [self customSectionTitleForRowCookie:[self.cachedCustomSectionRowCookies objectAtIndex:index]];
+            if ([rowTitle rangeOfString:self.filterText options:NSCaseInsensitiveSearch].location != NSNotFound) {
+                matches = YES;
+            }
+            return matches;
+        }];
+    }
+    self.customSectionVisibleIndexes = filteredIndexSet;
+}
+
+- (id)customSectionRowCookieForVisibleRow:(NSUInteger)row
+{
+    return [[self.cachedCustomSectionRowCookies objectsAtIndexes:self.customSectionVisibleIndexes] objectAtIndex:row];
+}
+
+
+#pragma mark - Subclasses Can Override
+
+- (NSString *)customSectionTitle
+{
+    return nil;
+}
+
+- (NSArray *)customSectionRowCookies
+{
+    return nil;
+}
+
+- (NSString *)customSectionTitleForRowCookie:(id)rowCookie
+{
+    return nil;
+}
+
+- (NSString *)customSectionSubtitleForRowCookie:(id)rowCookie
+{
+    return nil;
+}
+
+- (BOOL)customSectionCanDrillIntoRowWithCookie:(id)rowCookie
+{
+    return NO;
+}
+
+- (UIViewController *)customSectionDrillInViewControllerForRowCookie:(id)rowCookie
+{
+    return nil;
+}
+
+@end

+ 13 - 0
Classes/Object Explorers/FLEXSetExplorerViewController.h

@@ -0,0 +1,13 @@
+//
+//  FLEXSetExplorerViewController.h
+//  Flipboard
+//
+//  Created by Ryan Olson on 5/16/14.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import "FLEXObjectExplorerViewController.h"
+
+@interface FLEXSetExplorerViewController : FLEXObjectExplorerViewController
+
+@end

+ 64 - 0
Classes/Object Explorers/FLEXSetExplorerViewController.m

@@ -0,0 +1,64 @@
+//
+//  FLEXSetExplorerViewController.m
+//  Flipboard
+//
+//  Created by Ryan Olson on 5/16/14.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import "FLEXSetExplorerViewController.h"
+#import "FLEXRuntimeUtility.h"
+#import "FLEXObjectExplorerFactory.h"
+
+@interface FLEXSetExplorerViewController ()
+
+@property (nonatomic, readonly) NSSet *set;
+
+@end
+
+@implementation FLEXSetExplorerViewController
+
+- (NSSet *)set
+{
+    return [self.object isKindOfClass:[NSSet class]] ? self.object : nil;
+}
+
+
+#pragma mark - Superclass Overrides
+
+- (NSString *)customSectionTitle
+{
+    return @"Set Objects";
+}
+
+- (NSArray *)customSectionRowCookies
+{
+    return [self.set allObjects];
+}
+
+- (NSString *)customSectionTitleForRowCookie:(id)rowCookie
+{
+    return [FLEXRuntimeUtility descriptionForIvarOrPropertyValue:rowCookie];
+}
+
+- (NSString *)customSectionSubtitleForRowCookie:(id)rowCookie
+{
+    return nil;
+}
+
+- (BOOL)customSectionCanDrillIntoRowWithCookie:(id)rowCookie
+{
+    return YES;
+}
+
+- (UIViewController *)customSectionDrillInViewControllerForRowCookie:(id)rowCookie
+{
+    return [FLEXObjectExplorerFactory explorerViewControllerForObject:rowCookie];
+}
+
+- (BOOL)shouldShowDescription
+{
+    return NO;
+}
+
+@end

+ 17 - 0
Classes/Utility/FLEXHeapEnumerator.h

@@ -0,0 +1,17 @@
+//
+//  FLEXHeapEnumerator.h
+//  Flipboard
+//
+//  Created by Ryan Olson on 5/28/14.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+
+typedef void (^flex_object_enumeration_block_t)(__unsafe_unretained id object, __unsafe_unretained Class actualClass);
+
+@interface FLEXHeapEnumerator : NSObject
+
++ (void)enumerateLiveObjectsUsingBlock:(flex_object_enumeration_block_t)block;
+
+@end

+ 94 - 0
Classes/Utility/FLEXHeapEnumerator.m

@@ -0,0 +1,94 @@
+//
+//  FLEXHeapEnumerator.m
+//  Flipboard
+//
+//  Created by Ryan Olson on 5/28/14.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import "FLEXHeapEnumerator.h"
+#import <malloc/malloc.h>
+#import <mach/mach.h>
+#import <objc/runtime.h>
+
+static CFMutableSetRef registeredClasses;
+
+// Mimics the objective-c object stucture for checking if a range of memory is an object.
+typedef struct {
+    Class isa;
+} flex_maybe_object_t;
+
+@implementation FLEXHeapEnumerator
+
+static kern_return_t memory_reader(task_t task, vm_address_t remote_address, vm_size_t size, void **local_memory)
+{
+    *local_memory = (void *)remote_address;
+    return KERN_SUCCESS;
+}
+
+static void range_callback(task_t task, void *context, unsigned type, vm_range_t *ranges, unsigned rangeCount)
+{
+    flex_object_enumeration_block_t block = (__bridge flex_object_enumeration_block_t)context;
+    if (!block) {
+        return;
+    }
+    
+    for (unsigned int i = 0; i < rangeCount; i++) {
+        vm_range_t range = ranges[i];
+        flex_maybe_object_t *tryObject = (flex_maybe_object_t *)range.address;
+        Class tryClass = NULL;
+#ifdef __arm64__
+        // See http://www.sealiesoftware.com/blog/archive/2013/09/24/objc_explain_Non-pointer_isa.html
+        extern uint64_t objc_debug_isa_class_mask WEAK_IMPORT_ATTRIBUTE;
+        tryClass = (__bridge Class)((void *)((uint64_t)tryObject->isa & objc_debug_isa_class_mask));
+#else
+        tryClass = tryObject->isa;
+#endif
+        // If the class pointer matches one in our set of class pointers from the runtime, then we should have an object.
+        if (CFSetContainsValue(registeredClasses, (__bridge const void *)(tryClass))) {
+            block((__bridge id)tryObject, tryClass);
+        }
+    }
+}
+
++ (void)enumerateLiveObjectsUsingBlock:(flex_object_enumeration_block_t)block
+{
+    if (!block) {
+        return;
+    }
+    
+    // Refresh the class list on every call in case classes are added to the runtime.
+    [self updateRegisteredClasses];
+    
+    // For another exmple of enumerating through malloc ranges (which helped my understanding of the api) see:
+    // http://llvm.org/svn/llvm-project/lldb/tags/RELEASE_34/final/examples/darwin/heap_find/heap/heap_find.cpp
+    // Also https://gist.github.com/samdmarshall/17f4e66b5e2e579fd396
+    vm_address_t *zones = NULL;
+    unsigned int zoneCount = 0;
+    kern_return_t result = malloc_get_all_zones(mach_task_self(), &memory_reader, &zones, &zoneCount);
+    if (result == KERN_SUCCESS) {
+        for (unsigned int i = 0; i < zoneCount; i++) {
+            malloc_zone_t *zone = (malloc_zone_t *)zones[i];
+            if (zone->introspect && zone->introspect->enumerator) {
+                zone->introspect->enumerator(mach_task_self(), (__bridge void *)(block), MALLOC_PTR_IN_USE_RANGE_TYPE, zones[i], &memory_reader, &range_callback);
+            }
+        }
+    }
+}
+
++ (void)updateRegisteredClasses
+{
+    if (!registeredClasses) {
+        registeredClasses = CFSetCreateMutable(NULL, 0, NULL);
+    } else {
+        CFSetRemoveAllValues(registeredClasses);
+    }
+    unsigned int count = 0;
+    Class *classes = objc_copyClassList(&count);
+    for (unsigned int i = 0; i < count; i++) {
+        CFSetAddValue(registeredClasses, (__bridge const void *)(classes[i]));
+    }
+    free(classes);
+}
+
+@end

+ 22 - 0
Classes/Utility/FLEXResources.h

@@ -0,0 +1,22 @@
+//
+//  FLEXResources.h
+//  FLEX
+//
+//  Created by Ryan Olson on 6/8/14.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+#import <UIKit/UIKit.h>
+
+@interface FLEXResources : NSObject
+
++ (UIImage *)closeIcon;
++ (UIImage *)dragHandle;
++ (UIImage *)globeIcon;
++ (UIImage *)hierarchyIndentPattern;
++ (UIImage *)listIcon;
++ (UIImage *)moveIcon;
++ (UIImage *)selectIcon;
+
+@end

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 94 - 0
Classes/Utility/FLEXResources.m


+ 39 - 0
Classes/Utility/FLEXRuntimeUtility.h

@@ -0,0 +1,39 @@
+//
+//  FLEXRuntimeUtility.h
+//  Flipboard
+//
+//  Created by Ryan Olson on 6/8/14.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+#import <objc/runtime.h>
+
+extern const unsigned int kFLEXNumberOfImplicitArgs;
+
+@interface FLEXRuntimeUtility : NSObject
+
+// Property Helpers
++ (NSString *)prettyNameForProperty:(objc_property_t)property;
++ (NSString *)typeEncodingForProperty:(objc_property_t)property;
++ (BOOL)isReadonlyProperty:(objc_property_t)property;
++ (SEL)setterSelectorForProperty:(objc_property_t)property;
++ (NSString *)fullDescriptionForProperty:(objc_property_t)property;
++ (id)valueForProperty:(objc_property_t)property onObject:(id)object;
++ (NSString *)descriptionForIvarOrPropertyValue:(id)value;
+
+// Ivar Helpers
++ (NSString *)prettyNameForIvar:(Ivar)ivar;
++ (id)valueForIvar:(Ivar)ivar onObject:(id)object;
++ (void)setIvar:(Ivar)ivar onObject:(id)object withInputString:(NSString *)inputString;
+
+// Method Helpers
++ (NSString *)prettyNameForMethod:(Method)method isClassMethod:(BOOL)isClassMethod;
++ (NSArray *)prettyArgumentComponentsForMethod:(Method)method;
+
+// Method Calling/Field Editing
++ (id)performSelector:(SEL)selector onObject:(id)object withArguments:(NSArray *)arguments error:(NSError * __autoreleasing *)error;
++ (NSString *)editiableDescriptionForObject:(id)object;
++ (id)objectValueFromEditableString:(NSString *)string;
+
+@end

+ 589 - 0
Classes/Utility/FLEXRuntimeUtility.m

@@ -0,0 +1,589 @@
+//
+//  FLEXRuntimeUtility.m
+//  Flipboard
+//
+//  Created by Ryan Olson on 6/8/14.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import "FLEXRuntimeUtility.h"
+
+// See https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtPropertyIntrospection.html#//apple_ref/doc/uid/TP40008048-CH101-SW6
+static NSString *const kFLEXUtilityAttributeTypeEncoding = @"T";
+static NSString *const kFLEXUtilityAttributeBackingIvar = @"V";
+static NSString *const kFLEXUtilityAttributeReadOnly = @"R";
+static NSString *const kFLEXUtilityAttributeCopy = @"C";
+static NSString *const kFLEXUtilityAttributeRetain = @"&";
+static NSString *const kFLEXUtilityAttributeNonAtomic = @"N";
+static NSString *const kFLEXUtilityAttributeCustomGetter = @"G";
+static NSString *const kFLEXUtilityAttributeCustomSetter = @"S";
+static NSString *const kFLEXUtilityAttributeDynamic = @"D";
+static NSString *const kFLEXUtilityAttributeWeak = @"W";
+static NSString *const kFLEXUtilityAttributeGarbageCollectable = @"P";
+static NSString *const kFLEXUtilityAttributeOldStyleTypeEncoding = @"t";
+
+static NSString *const FLEXRuntimeUtilityErrorDomain = @"FLEXRuntimeUtilityErrorDomain";
+typedef NS_ENUM(NSInteger, FLEXRuntimeUtilityErrorCode) {
+    FLEXRuntimeUtilityErrorCodeDoesNotRecognizeSelector = 0,
+    FLEXRuntimeUtilityErrorCodeInvocationFailed = 1
+};
+
+// Arguments 0 and 1 are self and _cmd always
+const unsigned int kFLEXNumberOfImplicitArgs = 2;
+
+@implementation FLEXRuntimeUtility
+
+
+#pragma mark - Property Helpers (Public)
+
++ (NSString *)prettyNameForProperty:(objc_property_t)property
+{
+    NSString *name = @(property_getName(property));
+    NSString *encoding = [self typeEncodingForProperty:property];
+    NSString *readableType = [self readableTypeForEncoding:encoding];
+    return [self appendName:name toType:readableType];
+}
+
++ (NSString *)typeEncodingForProperty:(objc_property_t)property
+{
+    NSDictionary *attributesDictionary = [self attributesDictionaryForProperty:property];
+    return [attributesDictionary objectForKey:kFLEXUtilityAttributeTypeEncoding];
+}
+
++ (BOOL)isReadonlyProperty:(objc_property_t)property
+{
+    return [[self attributesDictionaryForProperty:property] objectForKey:kFLEXUtilityAttributeReadOnly] != nil;
+}
+
++ (SEL)setterSelectorForProperty:(objc_property_t)property
+{
+    SEL setterSelector = NULL;
+    NSString *setterSelectorString = [[self attributesDictionaryForProperty:property] objectForKey:kFLEXUtilityAttributeCustomSetter];
+    if (!setterSelectorString) {
+        NSString *propertyName = @(property_getName(property));
+        setterSelectorString = [NSString stringWithFormat:@"set%@%@:", [[propertyName substringToIndex:1] uppercaseString], [propertyName substringFromIndex:1]];
+    }
+    if (setterSelectorString) {
+        setterSelector = NSSelectorFromString(setterSelectorString);
+    }
+    return setterSelector;
+}
+
++ (NSString *)fullDescriptionForProperty:(objc_property_t)property
+{
+    NSDictionary *attributesDictionary = [self attributesDictionaryForProperty:property];
+    NSMutableArray *attributesStrings = [NSMutableArray array];
+    
+    // Atomicity
+    if ([attributesDictionary objectForKey:kFLEXUtilityAttributeNonAtomic]) {
+        [attributesStrings addObject:@"nonatomic"];
+    } else {
+        [attributesStrings addObject:@"atomic"];
+    }
+    
+    // Storage
+    if ([attributesDictionary objectForKey:kFLEXUtilityAttributeRetain]) {
+        [attributesStrings addObject:@"strong"];
+    } else if ([attributesDictionary objectForKey:kFLEXUtilityAttributeCopy]) {
+        [attributesStrings addObject:@"copy"];
+    } else if ([attributesDictionary objectForKey:kFLEXUtilityAttributeWeak]) {
+        [attributesStrings addObject:@"weak"];
+    } else {
+        [attributesStrings addObject:@"assign"];
+    }
+    
+    // Mutability
+    if ([attributesDictionary objectForKey:kFLEXUtilityAttributeReadOnly]) {
+        [attributesStrings addObject:@"readonly"];
+    } else {
+        [attributesStrings addObject:@"readwrite"];
+    }
+    
+    // Custom getter/setter
+    NSString *customGetter = [attributesDictionary objectForKey:kFLEXUtilityAttributeCustomGetter];
+    NSString *customSetter = [attributesDictionary objectForKey:kFLEXUtilityAttributeCustomSetter];
+    if (customGetter) {
+        [attributesStrings addObject:[NSString stringWithFormat:@"getter=%@", customGetter]];
+    }
+    if (customSetter) {
+        [attributesStrings addObject:[NSString stringWithFormat:@"setter=%@", customSetter]];
+    }
+    
+    NSString *attributesString = [attributesStrings componentsJoinedByString:@", "];
+    NSString *shortName = [self prettyNameForProperty:property];
+    
+    return [NSString stringWithFormat:@"@property (%@) %@", attributesString, shortName];
+}
+
++ (id)valueForProperty:(objc_property_t)property onObject:(id)object
+{
+    NSString *customGetterString = nil;
+    char *customGetterName = property_copyAttributeValue(property, "G");
+    if (customGetterName) {
+        customGetterString = @(customGetterName);
+        free(customGetterName);
+    }
+    
+    SEL getterSelector;
+    if ([customGetterString length] > 0) {
+        getterSelector = NSSelectorFromString(customGetterString);
+    } else {
+        NSString *propertyName = @(property_getName(property));
+        getterSelector = NSSelectorFromString(propertyName);
+    }
+    
+    return [self performSelector:getterSelector onObject:object withArguments:nil error:NULL];
+}
+
++ (NSString *)descriptionForIvarOrPropertyValue:(id)value
+{
+    NSString *description = nil;
+    
+    // Special case BOOL for better readability.
+    if ([value isKindOfClass:[NSValue class]]) {
+        const char *type = [value objCType];
+        if (strcmp(type, @encode(BOOL)) == 0) {
+            BOOL boolValue = NO;
+            [value getValue:&boolValue];
+            description = boolValue ? @"YES" : @"NO";
+        }
+    }
+    
+    if (!description) {
+        // Single line display - replace newlines and tabs with spaces.
+        description = [[value description] stringByReplacingOccurrencesOfString:@"\n" withString:@" "];
+        description = [description stringByReplacingOccurrencesOfString:@"\t" withString:@" "];
+    }
+    
+    if (!description) {
+        description = @"nil";
+    }
+    
+    return description;
+}
+
+
+#pragma mark - Ivar Helpers (Public)
+
++ (NSString *)prettyNameForIvar:(Ivar)ivar
+{
+    NSString *name = @(ivar_getName(ivar));
+    NSString *encoding = @(ivar_getTypeEncoding(ivar));
+    NSString *readableType = [self readableTypeForEncoding:encoding];
+    return [self appendName:name toType:readableType];
+}
+
++ (id)valueForIvar:(Ivar)ivar onObject:(id)object
+{
+    id value = nil;
+    const char *type = ivar_getTypeEncoding(ivar);
+    if (type[0] == @encode(id)[0] || type[0] == @encode(Class)[0]) {
+        value = object_getIvar(object, ivar);
+    } else {
+        ptrdiff_t offset = ivar_getOffset(ivar);
+        void *pointer = (__bridge void *)object + offset;
+        value = [self valueForPrimitivePointer:pointer objcType:type];
+    }
+    return value;
+}
+
++ (void)setIvar:(Ivar)ivar onObject:(id)object withInputString:(NSString *)inputString
+{
+    const char *typeEncodingCString = ivar_getTypeEncoding(ivar);
+    if (typeEncodingCString[0] == '@') {
+        // Object - use NSJSONSerialization
+        id ivarValue = [self objectValueFromEditableString:inputString];
+        object_setIvar(object, ivar, ivarValue);
+    } else {
+        // Primitive - try to parse a number or struct from the string based on the type encoding
+        NSUInteger bufferSize = 0;
+        NSGetSizeAndAlignment(typeEncodingCString, &bufferSize, NULL);
+        void *buffer = malloc(bufferSize);
+        [self getPrimitiveValue:buffer withObjCType:typeEncodingCString fromInputString:inputString];
+        ptrdiff_t offset = ivar_getOffset(ivar);
+        void *pointer = (__bridge void *)object + offset;
+        memcpy(pointer, buffer, bufferSize);
+    }
+}
+
+
+#pragma mark - Method Helpers (Public)
+
++ (NSString *)prettyNameForMethod:(Method)method isClassMethod:(BOOL)isClassMethod
+{
+    NSString *selectorName = NSStringFromSelector(method_getName(method));
+    NSString *methodTypeString = isClassMethod ? @"+" : @"-";
+    char *returnType = method_copyReturnType(method);
+    NSString *readableReturnType = [self readableTypeForEncoding:@(returnType)];
+    free(returnType);
+    NSString *prettyName = [NSString stringWithFormat:@"%@ (%@)", methodTypeString, readableReturnType];
+    NSArray *components = [self prettyArgumentComponentsForMethod:method];
+    if ([components count] > 0) {
+        prettyName = [prettyName stringByAppendingString:[components componentsJoinedByString:@" "]];
+    } else {
+        prettyName = [prettyName stringByAppendingString:selectorName];
+    }
+    
+    return prettyName;
+}
+
++ (NSArray *)prettyArgumentComponentsForMethod:(Method)method
+{
+    NSMutableArray *components = [NSMutableArray array];
+    
+    NSString *selectorName = NSStringFromSelector(method_getName(method));
+    NSArray *selectorComponents = [selectorName componentsSeparatedByString:@":"];
+    unsigned int numberOfArguments = method_getNumberOfArguments(method);
+    
+    for (unsigned int argIndex = kFLEXNumberOfImplicitArgs; argIndex < numberOfArguments; argIndex++) {
+        char *argType = method_copyArgumentType(method, argIndex);
+        NSString *readableArgType = [self readableTypeForEncoding:@(argType)];
+        free(argType);
+        NSString *prettyComponent = [NSString stringWithFormat:@"%@:(%@) ", [selectorComponents objectAtIndex:argIndex - kFLEXNumberOfImplicitArgs], readableArgType];
+        [components addObject:prettyComponent];
+    }
+    
+    return components;
+}
+
+
+#pragma mark - Method Calling/Field Editing (Public)
+
++ (id)performSelector:(SEL)selector onObject:(id)object withArguments:(NSArray *)arguments error:(NSError * __autoreleasing *)error
+{
+    // Bail if the object won't respond to this selector.
+    if (![object respondsToSelector:selector]) {
+        if (error) {
+            NSDictionary *userInfo = @{ NSLocalizedDescriptionKey : [NSString stringWithFormat:@"%@ does not respond to the selector %@", object, NSStringFromSelector(selector)]};
+            *error = [NSError errorWithDomain:FLEXRuntimeUtilityErrorDomain code:FLEXRuntimeUtilityErrorCodeDoesNotRecognizeSelector userInfo:userInfo];
+        }
+        return nil;
+    }
+    
+    // Build the invocation
+    NSMethodSignature *methodSignature = [object methodSignatureForSelector:selector];
+    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
+    [invocation setSelector:selector];
+    [invocation setTarget:object];
+    [invocation retainArguments];
+    
+    // Always self and _cmd
+    NSUInteger numberOfArguments = [methodSignature numberOfArguments];
+    for (NSUInteger argumentIndex = kFLEXNumberOfImplicitArgs; argumentIndex < numberOfArguments; argumentIndex++) {
+        NSUInteger argumentsArrayIndex = argumentIndex - kFLEXNumberOfImplicitArgs;
+        NSString *argumentString = [arguments count] > argumentsArrayIndex ? [arguments objectAtIndex:argumentsArrayIndex] : nil;
+        const char *typeEncodingCString = [methodSignature getArgumentTypeAtIndex:argumentIndex];
+        if (typeEncodingCString[0] == @encode(id)[0] || typeEncodingCString[0] == @encode(Class)[0]) {
+            // Object
+            id argumentObject = [self objectValueFromEditableString:argumentString];
+            [invocation setArgument:&argumentObject atIndex:argumentIndex];
+        } else if (strcmp(typeEncodingCString, @encode(const char *)) == 0 || strcmp(typeEncodingCString, @encode(char *)) == 0) {
+            // C string
+            const char *argumentCString = [argumentString UTF8String];
+            [invocation setArgument:&argumentCString atIndex:argumentIndex];
+        } else {
+            // Primitive number or struct
+            NSUInteger bufferSize = 0;
+            NSGetSizeAndAlignment(typeEncodingCString, &bufferSize, NULL);
+            void *buffer = malloc(bufferSize);
+            [self getPrimitiveValue:buffer withObjCType:typeEncodingCString fromInputString:argumentString];
+            [invocation setArgument:buffer atIndex:argumentIndex];
+            free(buffer);
+        }
+    }
+    
+    // Try to invoke the invocation but guard against an exception being thrown.
+    BOOL successfullyInvoked = NO;
+    @try {
+        // Some methods are not fit to be called...
+        // Looking at you -[UIResponder(UITextInputAdditions) _caretRect]
+        [invocation invoke];
+        successfullyInvoked = YES;
+    } @catch (NSException *exception) {
+        // Bummer...
+        if (error) {
+            NSDictionary *userInfo = @{ NSLocalizedDescriptionKey : [NSString stringWithFormat:@"Exception thrown while performing selector %@ on object %@", object, NSStringFromSelector(selector)]};
+            *error = [NSError errorWithDomain:FLEXRuntimeUtilityErrorDomain code:FLEXRuntimeUtilityErrorCodeInvocationFailed userInfo:userInfo];
+        }
+    }
+    
+    // Retreive the return value and box if necessary.
+    id returnObject = nil;
+    if (successfullyInvoked) {
+        const char *returnType = [methodSignature methodReturnType];
+        if (returnType[0] == @encode(id)[0] || returnType[0] == @encode(Class)[0]) {
+            __unsafe_unretained id objectReturnedFromMethod = nil;
+            [invocation getReturnValue:&objectReturnedFromMethod];
+            returnObject = objectReturnedFromMethod;
+        } else if (returnType[0] != @encode(void)[0]) {
+            void *returnValue = malloc([methodSignature methodReturnLength]);
+            if (returnValue) {
+                [invocation getReturnValue:returnValue];
+                returnObject = [self valueForPrimitivePointer:returnValue objcType:returnType];
+                free(returnValue);
+            }
+        }
+    }
+    
+    return returnObject;
+}
+
++ (NSString *)editiableDescriptionForObject:(id)object
+{
+    NSString *editableDescription = nil;
+    
+    if ([object isKindOfClass:[NSValue class]]) {
+        NSValue *value = (NSValue *)object;
+        const char *typeEncoding = [value objCType];
+        if (strcmp(typeEncoding, @encode(CGRect)) == 0) {
+            editableDescription = NSStringFromCGRect([value CGRectValue]);
+        } else if (strcmp(typeEncoding, @encode(CGSize)) == 0) {
+            editableDescription = NSStringFromCGSize([value CGSizeValue]);
+        } else if (strcmp(typeEncoding, @encode(CGPoint)) == 0) {
+            editableDescription = NSStringFromCGPoint([value CGPointValue]);
+        } else if (strcmp(typeEncoding, @encode(CGAffineTransform)) == 0) {
+            editableDescription = NSStringFromCGAffineTransform([value CGAffineTransformValue]);
+        } else if (strcmp(typeEncoding, @encode(NSRange)) == 0) {
+            editableDescription = NSStringFromRange([value rangeValue]);
+        } else if (strcmp(typeEncoding, @encode(UIEdgeInsets)) == 0) {
+            editableDescription = NSStringFromUIEdgeInsets([value UIEdgeInsetsValue]);
+        } else if (strcmp(typeEncoding, @encode(UIOffset)) == 0) {
+            editableDescription = NSStringFromUIOffset([value UIOffsetValue]);
+        }
+    }
+    
+    if (object && !editableDescription) {
+        // This is a hack to use JSON serialzation for our editable objects.
+        // NSJSONSerialization doesn't allow writing fragments - the top level object must be an array or dictionary.
+        // We always wrap the object inside an array and then strip the outter square braces off the final string.
+        NSArray *wrappedObject = @[object];
+        if ([NSJSONSerialization isValidJSONObject:wrappedObject]) {
+            NSString *wrappedDescription = [[NSString alloc] initWithData:[NSJSONSerialization dataWithJSONObject:wrappedObject options:0 error:NULL] encoding:NSUTF8StringEncoding];
+            editableDescription = [wrappedDescription substringWithRange:NSMakeRange(1, [wrappedDescription length] - 2)];
+        }
+    }
+    
+    return editableDescription;
+}
+
++ (id)objectValueFromEditableString:(NSString *)string
+{
+    id value = nil;
+    // nil for empty string/whitespace
+    if ([[string stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] length] > 0) {
+        value = [NSJSONSerialization JSONObjectWithData:[string dataUsingEncoding:NSUTF8StringEncoding] options:NSJSONReadingAllowFragments error:NULL];
+    }
+    return value;
+}
+
+
+#pragma mark - Internal Helpers
+
++ (NSDictionary *)attributesDictionaryForProperty:(objc_property_t)property
+{
+    NSString *attributes = @(property_getAttributes(property));
+    // Thanks to MAObjcRuntime for inspiration here.
+    NSArray *attributePairs = [attributes componentsSeparatedByString:@","];
+    NSMutableDictionary *attributesDictionary = [NSMutableDictionary dictionaryWithCapacity:[attributePairs count]];
+    for (NSString *attributePair in attributePairs) {
+        [attributesDictionary setObject:[attributePair substringFromIndex:1] forKey:[attributePair substringToIndex:1]];
+    }
+    return attributesDictionary;
+}
+
++ (NSString *)appendName:(NSString *)name toType:(NSString *)type
+{
+    NSString *combined = nil;
+    if ([type characterAtIndex:[type length] - 1] == '*') {
+        combined = [type stringByAppendingString:name];
+    } else {
+        combined = [type stringByAppendingFormat:@" %@", name];
+    }
+    return combined;
+}
+
++ (NSString *)readableTypeForEncoding:(NSString *)encodingString
+{
+    // See https://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html
+    // class-dump has a much nicer and much more complete implementation for this task, but it is distributed under GPLv2 :/
+    // See https://github.com/nygard/class-dump/blob/master/Source/CDType.m
+    // Warning: this method uses multiple middle returns and macros to cut down on boilerplate.
+    // The use of macros here was inspired by https://www.mikeash.com/pyblog/friday-qa-2013-02-08-lets-build-key-value-coding.html
+    const char *encodingCString = [encodingString UTF8String];
+    
+    // Objects
+    if (encodingCString[0] == '@') {
+        NSString *class = [encodingString substringFromIndex:1];
+        class = [class stringByReplacingOccurrencesOfString:@"\"" withString:@""];
+        if ([class length] == 0) {
+            class = @"id";
+        } else {
+            class = [class stringByAppendingString:@" *"];
+        }
+        return class;
+    }
+    
+    // C Types
+#define TRANSLATE(ctype) \
+    if (strcmp(encodingCString, @encode(ctype)) == 0) { \
+        return (NSString *)CFSTR(#ctype); \
+    }
+    
+    // Order matters here since some of the cocoa types are typedefed to c types.
+    // We can't recover the exact mapping, but we choose to prefer the cocoa types.
+    // This is not an exhaustive list, but it covers the most common types
+    TRANSLATE(CGRect);
+    TRANSLATE(CGPoint);
+    TRANSLATE(CGSize);
+    TRANSLATE(UIEdgeInsets);
+    TRANSLATE(UIOffset);
+    TRANSLATE(NSRange);
+    TRANSLATE(CGAffineTransform);
+    TRANSLATE(NSInteger);
+    TRANSLATE(NSUInteger);
+    TRANSLATE(CGFloat);
+    TRANSLATE(BOOL);
+    TRANSLATE(int);
+    TRANSLATE(short);
+    TRANSLATE(long);
+    TRANSLATE(long long);
+    TRANSLATE(unsigned char);
+    TRANSLATE(unsigned int);
+    TRANSLATE(unsigned short);
+    TRANSLATE(unsigned long);
+    TRANSLATE(unsigned long long);
+    TRANSLATE(float);
+    TRANSLATE(double);
+    TRANSLATE(long double);
+    TRANSLATE(char *);
+    TRANSLATE(Class);
+    TRANSLATE(objc_property_t);
+    TRANSLATE(Ivar);
+    TRANSLATE(Method);
+    TRANSLATE(Category);
+    TRANSLATE(NSZone *);
+    TRANSLATE(SEL);
+    TRANSLATE(void);
+    
+#undef TRANSLATE
+    
+    // Qualifier Prefixes
+    // Do this after the checks above since some of the direct translations (i.e. Method) contain a prefix.
+#define RECURSIVE_TRANSLATE(prefix, formatString) \
+    if (encodingCString[0] == prefix) { \
+        NSString *recursiveType = [self readableTypeForEncoding:[encodingString substringFromIndex:1]]; \
+        return [NSString stringWithFormat:formatString, recursiveType]; \
+    }
+    
+    // If there's a qualifier prefix on the encoding, translate it and then
+    // recursively call this method with the rest of the encoding string.
+    RECURSIVE_TRANSLATE('^', @"%@ *");
+    RECURSIVE_TRANSLATE('r', @"const %@");
+    RECURSIVE_TRANSLATE('n', @"in %@");
+    RECURSIVE_TRANSLATE('N', @"inout %@");
+    RECURSIVE_TRANSLATE('o', @"out %@");
+    RECURSIVE_TRANSLATE('O', @"bycopy %@");
+    RECURSIVE_TRANSLATE('R', @"byref %@");
+    RECURSIVE_TRANSLATE('V', @"oneway %@");
+    RECURSIVE_TRANSLATE('b', @"bitfield(%@)");
+    
+#undef RECURSIVE_TRANSLATE
+    
+    // If we couldn't translate, just return the original encoding string
+    return encodingString;
+}
+
++ (NSValue *)valueForPrimitivePointer:(void *)pointer objcType:(const char *)type
+{
+    // CASE marcro inspired by https://www.mikeash.com/pyblog/friday-qa-2013-02-08-lets-build-key-value-coding.html
+#define CASE(ctype, selectorpart) \
+    if(strcmp(type, @encode(ctype)) == 0) { \
+        return [NSNumber numberWith ## selectorpart: *(ctype *)pointer]; \
+    }
+    
+    CASE(BOOL, Bool);
+    CASE(unsigned char, UnsignedChar);
+    CASE(short, Short);
+    CASE(unsigned short, UnsignedShort);
+    CASE(int, Int);
+    CASE(unsigned int, UnsignedInt);
+    CASE(long, Long);
+    CASE(unsigned long, UnsignedLong);
+    CASE(long long, LongLong);
+    CASE(unsigned long long, UnsignedLongLong);
+    CASE(float, Float);
+    CASE(double, Double);
+    
+#undef CASE
+    
+    NSValue *value = nil;
+    @try {
+        value = [NSValue valueWithBytes:pointer objCType:type];
+    } @catch (NSException *exception) {
+        // Certain type encodings are not supported by valueWithBytes:objCType:. Just fail silently if an exception is thrown.
+    }
+    
+    return value;
+}
+
++ (void)getPrimitiveValue:(void *)value withObjCType:(const char *)typeEncoding fromInputString:(NSString *)inputString
+{
+    if (strcmp(typeEncoding, @encode(CGRect)) == 0) {
+        *(CGRect *)value = CGRectFromString(inputString);
+    } else if (strcmp(typeEncoding, @encode(CGSize)) == 0) {
+        *(CGSize *)value = CGSizeFromString(inputString);
+    } else if (strcmp(typeEncoding, @encode(CGPoint)) == 0) {
+        *(CGPoint *)value = CGPointFromString(inputString);
+    } else if (strcmp(typeEncoding, @encode(CGAffineTransform)) == 0) {
+        *(CGAffineTransform *)value = CGAffineTransformFromString(inputString);
+    } else if (strcmp(typeEncoding, @encode(NSRange)) == 0) {
+        *(NSRange *)value = NSRangeFromString(inputString);
+    } else if (strcmp(typeEncoding, @encode(UIEdgeInsets)) == 0) {
+        *(UIEdgeInsets *)value = UIEdgeInsetsFromString(inputString);
+    } else if (strcmp(typeEncoding, @encode(UIOffset)) == 0) {
+        *(UIOffset *)value = UIOffsetFromString(inputString);
+    } else {
+        NSNumberFormatter *formatter = [[NSNumberFormatter alloc] init];
+        [formatter setNumberStyle:NSNumberFormatterDecimalStyle];
+        NSNumber *number = [formatter numberFromString:inputString];
+        
+        if (strcmp(typeEncoding, @encode(char)) == 0) {
+            // Special case - ask for the string's BOOL value if we couldn't parse a number.
+            // This allows us to take YES/NO/true/false input.
+            if (number) {
+                *(char *)value = [number charValue];
+            } else {
+                *(char *)value = [inputString boolValue];
+            }
+        } else if (strcmp(typeEncoding, @encode(int)) == 0) {
+            *(int *)value = [number intValue];
+        } else if (strcmp(typeEncoding, @encode(short)) == 0) {
+            *(short *)value = [number shortValue];
+        } else if (strcmp(typeEncoding, @encode(long)) == 0) {
+            *(long *)value = [number longValue];
+        } else if (strcmp(typeEncoding, @encode(long long)) == 0) {
+            *(long long *)value = [number longLongValue];
+        } else if (strcmp(typeEncoding, @encode(unsigned char)) == 0) {
+            *(unsigned char *)value = [number unsignedCharValue];
+        } else if (strcmp(typeEncoding, @encode(unsigned int)) == 0) {
+            *(unsigned int *)value = [number unsignedIntValue];
+        } else if (strcmp(typeEncoding, @encode(unsigned short)) == 0) {
+            *(unsigned short *)value = [number unsignedShortValue];
+        } else if (strcmp(typeEncoding, @encode(unsigned long)) == 0) {
+            *(unsigned long *)value = [number unsignedLongValue];
+        } else if (strcmp(typeEncoding, @encode(unsigned long long)) == 0) {
+            *(unsigned long long *)value = [number unsignedLongLongValue];
+        } else if (strcmp(typeEncoding, @encode(float)) == 0) {
+            *(float *)value = [number floatValue];
+        } else if (strcmp(typeEncoding, @encode(double)) == 0) {
+            *(double *)value = [number doubleValue];
+        } else {
+            // If we didn't match one of the supported types, fill the buffer with zeros.
+            NSUInteger bytes = 0;
+            NSGetSizeAndAlignment(typeEncoding, &bytes, NULL);
+            memset(value, 0, bytes);
+        }
+    }
+}
+
+@end

+ 26 - 0
Classes/Utility/FLEXUtility.h

@@ -0,0 +1,26 @@
+//
+//  FLEXUtility.h
+//  Flipboard
+//
+//  Created by Ryan Olson on 4/18/14.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+#import <UIKit/UIKit.h>
+
+@interface FLEXUtility : NSObject
+
++ (UIColor *)consistentRandomColorForObject:(id)object;
++ (NSString *)descriptionForView:(UIView *)view includingFrame:(BOOL)includeFrame;
++ (NSString *)detailDescriptionForView:(UIView *)view;
++ (UIImage *)circularImageWithColor:(UIColor *)color radius:(CGFloat)radius;
++ (UIColor *)scrollViewGrayColor;
++ (UIColor *)hierarchyIndentPatternColor;
++ (NSString *)applicationImageName;
++ (NSString *)applicationName;
++ (NSString *)safeDescriptionForObject:(id)object;
++ (UIFont *)defaultFontOfSize:(CGFloat)size;
++ (UIFont *)defaultTableViewCellLabelFont;
+
+@end

+ 117 - 0
Classes/Utility/FLEXUtility.m

@@ -0,0 +1,117 @@
+//
+//  FLEXUtility.m
+//  Flipboard
+//
+//  Created by Ryan Olson on 4/18/14.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import "FLEXUtility.h"
+#import "FLEXResources.h"
+
+@implementation FLEXUtility
+
++ (UIColor *)consistentRandomColorForObject:(id)object
+{
+    CGFloat hue = (((NSUInteger)object >> 4) % 256) / 255.0;
+    return [UIColor colorWithHue:hue saturation:1.0 brightness:1.0 alpha:1.0];
+}
+
++ (NSString *)descriptionForView:(UIView *)view includingFrame:(BOOL)includeFrame
+{
+    NSString *description = [[view class] description];
+    
+    NSString *viewControllerDescription = nil;
+    SEL viewDelSel = NSSelectorFromString([NSString stringWithFormat:@"%@ewDelegate", @"_vi"]);
+    if ([view respondsToSelector:viewDelSel]) {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
+        id viewDel = [view performSelector:viewDelSel];
+#pragma clang diagnostic pop
+        if (viewDel) {
+            viewControllerDescription = [[viewDel class] description];
+        }
+    }
+    
+    if ([viewControllerDescription length] > 0) {
+        description = [description stringByAppendingFormat:@" (%@)", viewControllerDescription];
+    }
+    
+    if (includeFrame) {
+        description = [description stringByAppendingFormat:@" frame:%@", NSStringFromCGRect(view.frame)];
+    }
+    
+    if ([view.accessibilityLabel length] > 0) {
+        description = [description stringByAppendingFormat:@" · %@", view.accessibilityLabel];
+    }
+    
+    return description;
+}
+
++ (NSString *)detailDescriptionForView:(UIView *)view
+{
+    return [NSString stringWithFormat:@"frame = %@", NSStringFromCGRect(view.frame)];
+}
+
++ (UIImage *)circularImageWithColor:(UIColor *)color radius:(CGFloat)radius
+{
+    CGFloat diameter = radius * 2.0;
+    UIGraphicsBeginImageContextWithOptions(CGSizeMake(diameter, diameter), NO, 0.0);
+    CGContextRef imageContext = UIGraphicsGetCurrentContext();
+    CGContextSetFillColorWithColor(imageContext, [color CGColor]);
+    CGContextFillEllipseInRect(imageContext, CGRectMake(0, 0, diameter, diameter));
+    UIImage *circularImage = UIGraphicsGetImageFromCurrentImageContext();
+    UIGraphicsEndImageContext();
+    return circularImage;
+}
+
++ (UIColor *)scrollViewGrayColor
+{
+    return [UIColor colorWithRed:239.0/255.0 green:239.0/255.0 blue:244.0/255.0 alpha:1.0];
+}
+
++ (UIColor *)hierarchyIndentPatternColor
+{
+    static UIColor *patternColor = nil;
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        UIImage *indentationPatternImage = [FLEXResources hierarchyIndentPattern];
+        patternColor = [UIColor colorWithPatternImage:indentationPatternImage];
+    });
+    return patternColor;
+}
+
++ (NSString *)applicationImageName
+{
+    return [[[NSProcessInfo processInfo] arguments] objectAtIndex:0];
+}
+
++ (NSString *)applicationName
+{
+    return [[[FLEXUtility applicationImageName] componentsSeparatedByString:@"/"] lastObject];
+}
+
++ (NSString *)safeDescriptionForObject:(id)object
+{
+    // Don't assume that we have an NSObject subclass.
+    // Check to make sure the object responds to the description methods.
+    NSString *description = nil;
+    if ([object respondsToSelector:@selector(debugDescription)]) {
+        description = [object debugDescription];
+    } else if ([object respondsToSelector:@selector(description)]) {
+        description = [object description];
+    }
+    return description;
+}
+
++ (UIFont *)defaultFontOfSize:(CGFloat)size
+{
+    return [UIFont fontWithName:@"HelveticaNeue" size:size];
+}
+
++ (UIFont *)defaultTableViewCellLabelFont
+{
+    return [self defaultFontOfSize:12.0];
+}
+
+@end

+ 18 - 0
Classes/View Hierarchy/FLEXHierarchyTableViewCell.h

@@ -0,0 +1,18 @@
+//
+//  FLEXHierarchyTableViewCell.h
+//  Flipboard
+//
+//  Created by Ryan Olson on 2014-05-02.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+
+@interface FLEXHierarchyTableViewCell : UITableViewCell
+
+- (id)initWithReuseIdentifier:(NSString *)reuseIdentifier;
+
+@property (nonatomic, assign) NSInteger viewDepth;
+@property (nonatomic, strong) UIColor *viewColor;
+
+@end

+ 112 - 0
Classes/View Hierarchy/FLEXHierarchyTableViewCell.m

@@ -0,0 +1,112 @@
+//
+//  FLEXHierarchyTableViewCell.m
+//  Flipboard
+//
+//  Created by Ryan Olson on 2014-05-02.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import "FLEXHierarchyTableViewCell.h"
+#import "FLEXUtility.h"
+
+@interface FLEXHierarchyTableViewCell ()
+
+@property (nonatomic, strong) UIView *depthIndicatorView;
+@property (nonatomic, strong) UIImageView *colorCircleImageView;
+
+@end
+
+@implementation FLEXHierarchyTableViewCell
+
+- (id)initWithReuseIdentifier:(NSString *)reuseIdentifier
+{
+    return [self initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:reuseIdentifier];
+}
+
+- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
+{
+    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
+    if (self) {
+        self.depthIndicatorView = [[UIView alloc] init];
+        self.depthIndicatorView.backgroundColor = [FLEXUtility hierarchyIndentPatternColor];
+        [self.contentView addSubview:self.depthIndicatorView];
+        
+        UIImage *defaultCircleImage = [FLEXUtility circularImageWithColor:[UIColor blackColor] radius:5.0];
+        self.colorCircleImageView = [[UIImageView alloc] initWithImage:defaultCircleImage];
+        [self.contentView addSubview:self.colorCircleImageView];
+        
+        self.textLabel.font = [UIFont fontWithName:@"HelveticaNeue-Medium" size:14.0];
+        self.detailTextLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
+        
+        if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_6_1) {
+            // Supported starting with iOS 7
+            self.accessoryType = UITableViewCellAccessoryDetailButton;
+        } else {
+            self.accessoryType = UITableViewCellAccessoryDetailDisclosureButton;
+        }
+    }
+    return self;
+}
+
+- (void)setHighlighted:(BOOL)highlighted animated:(BOOL)animated
+{
+    [super setHighlighted:highlighted animated:animated];
+    
+    // UITableViewCell changes all subviews in the contentView to backgroundColor = clearColor.
+    // We want to preserve the hierarchy background color when highlighted.
+    self.depthIndicatorView.backgroundColor = [FLEXUtility hierarchyIndentPatternColor];
+}
+
+- (void)setSelected:(BOOL)selected animated:(BOOL)animated
+{
+    [super setSelected:selected animated:animated];
+    
+    // See setHighlighted above.
+    self.depthIndicatorView.backgroundColor = [FLEXUtility hierarchyIndentPatternColor];
+}
+
+- (void)layoutSubviews
+{
+    [super layoutSubviews];
+    
+    const CGFloat kContentPadding = 10.0;
+    const CGFloat kDepthIndicatorWidthMultiplier = 4.0;
+    
+    CGRect depthIndicatorFrame = CGRectMake(kContentPadding, 0, self.viewDepth * kDepthIndicatorWidthMultiplier, self.contentView.bounds.size.height);
+    self.depthIndicatorView.frame = depthIndicatorFrame;
+    
+    CGRect circleFrame = self.colorCircleImageView.frame;
+    circleFrame.origin.x = CGRectGetMaxX(depthIndicatorFrame);
+    circleFrame.origin.y = self.textLabel.frame.origin.y + floor((self.textLabel.frame.size.height - circleFrame.size.height) / 2.0);
+    self.colorCircleImageView.frame = circleFrame;
+    
+    CGRect textLabelFrame = self.textLabel.frame;
+    CGFloat textOriginX = CGRectGetMaxX(circleFrame) + 4.0;
+    textLabelFrame.origin.x = textOriginX;
+    textLabelFrame.size.width = CGRectGetMaxX(self.contentView.bounds) - kContentPadding - textOriginX;
+    self.textLabel.frame = textLabelFrame;
+    
+    CGRect detailTextLabelFrame = self.detailTextLabel.frame;
+    CGFloat detailOriginX = CGRectGetMaxX(depthIndicatorFrame);
+    detailTextLabelFrame.origin.x = detailOriginX;
+    detailTextLabelFrame.size.width = CGRectGetMaxX(self.contentView.bounds) - kContentPadding - detailOriginX;
+    self.detailTextLabel.frame = detailTextLabelFrame;
+}
+
+- (void)setViewColor:(UIColor *)viewColor
+{
+    if (![_viewColor isEqual:viewColor]) {
+        _viewColor = viewColor;
+        self.colorCircleImageView.image = [FLEXUtility circularImageWithColor:viewColor radius:6.0];
+    }
+}
+
+- (void)setViewDepth:(NSInteger)viewDepth
+{
+    if (_viewDepth != viewDepth) {
+        _viewDepth = viewDepth;
+        [self setNeedsLayout];
+    }
+}
+
+@end

+ 25 - 0
Classes/View Hierarchy/FLEXHierarchyTableViewController.h

@@ -0,0 +1,25 @@
+//
+//  FLEXHierarchyTableViewController.h
+//  Flipboard
+//
+//  Created by Ryan Olson on 2014-05-01.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+
+@protocol FLEXHierarchyTableViewControllerDelegate;
+
+@interface FLEXHierarchyTableViewController : UITableViewController
+
+- (id)initWithViews:(NSArray *)allViews viewsAtTap:(NSArray *)viewsAtTap selectedView:(UIView *)selectedView depths:(NSDictionary *)depthsForViews;
+
+@property (nonatomic, weak) id <FLEXHierarchyTableViewControllerDelegate> delegate;
+
+@end
+
+@protocol FLEXHierarchyTableViewControllerDelegate <NSObject>
+
+- (void)hierarchyViewController:(FLEXHierarchyTableViewController *)hierarchyViewController didFinishWithSelectedView:(UIView *)selectedView;
+
+@end

+ 206 - 0
Classes/View Hierarchy/FLEXHierarchyTableViewController.m

@@ -0,0 +1,206 @@
+//
+//  FLEXHierarchyTableViewController.m
+//  Flipboard
+//
+//  Created by Ryan Olson on 2014-05-01.
+//  Copyright (c) 2014 Flipboard. All rights reserved.
+//
+
+#import "FLEXHierarchyTableViewController.h"
+#import "FLEXUtility.h"
+#import "FLEXHierarchyTableViewCell.h"
+#import "FLEXObjectExplorerViewController.h"
+#import "FLEXObjectExplorerFactory.h"
+
+static const NSInteger kFLEXHierarchyScopeViewsAtTapIndex = 0;
+static const NSInteger kFLEXHierarchyScopeFullHierarchyIndex = 1;
+
+@interface FLEXHierarchyTableViewController () <UISearchBarDelegate>
+
+@property (nonatomic, strong) NSArray *allViews;
+@property (nonatomic, strong) NSDictionary *depthsForViews;
+@property (nonatomic, strong) NSArray *viewsAtTap;
+@property (nonatomic, strong) UIView *selectedView;
+@property (nonatomic, strong) NSArray *displayedViews;
+
+@property (nonatomic, strong) UISearchBar *searchBar;
+
+@end
+
+@implementation FLEXHierarchyTableViewController
+
+- (id)initWithViews:(NSArray *)allViews viewsAtTap:(NSArray *)viewsAtTap selectedView:(UIView *)selectedView depths:(NSDictionary *)depthsForViews
+{
+    self = [super initWithStyle:UITableViewStylePlain];
+    if (self) {
+        self.allViews = allViews;
+        self.depthsForViews = depthsForViews;
+        self.viewsAtTap = viewsAtTap;
+        self.selectedView = selectedView;
+        
+        self.title = @"View Hierarchy";
+    }
+    return self;
+}
+
+- (void)viewDidLoad
+{
+    [super viewDidLoad];
+
+    // Preserve selection between presentations.
+    self.clearsSelectionOnViewWillAppear = NO;
+    // Done button.
+    self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(donePressed:)];
+    
+    // A little more breathing room.
+    self.tableView.rowHeight = 50.0;
+    self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
+    // Separator inset clashes with persistent cell selection.
+    if ([self.tableView respondsToSelector:@selector(setSeparatorInset:)]) {
+        [self.tableView setSeparatorInset:UIEdgeInsetsZero];
+    }
+    
+    self.searchBar = [[UISearchBar alloc] init];
+    self.searchBar.placeholder = @"Filter";
+    self.searchBar.delegate = self;
+    if ([self showScopeBar]) {
+        self.searchBar.showsScopeBar = YES;
+        self.searchBar.scopeButtonTitles = @[@"Views at Tap", @"Full Hierarchy"];
+    }
+    [self.searchBar sizeToFit];
+    self.tableView.tableHeaderView = self.searchBar;
+    
+    [self updateDisplayedViews];
+}
+
+- (void)viewDidAppear:(BOOL)animated
+{
+    [super viewDidAppear:animated];
+    
+    [self trySelectCellForSelectedViewWithScrollPosition:UITableViewScrollPositionMiddle];
+}
+
+- (BOOL)prefersStatusBarHidden
+{
+    return YES;
+}
+
+
+#pragma mark Selection and Filtering Helpers
+
+- (void)trySelectCellForSelectedViewWithScrollPosition:(UITableViewScrollPosition)scrollPosition
+{
+    NSUInteger selectedViewIndex = [self.displayedViews indexOfObject:self.selectedView];
+    if (selectedViewIndex != NSNotFound) {
+        NSIndexPath *selectedViewIndexPath = [NSIndexPath indexPathForRow:selectedViewIndex inSection:0];
+        [self.tableView selectRowAtIndexPath:selectedViewIndexPath animated:YES scrollPosition:scrollPosition];
+    }
+}
+
+- (void)updateDisplayedViews
+{
+    NSArray *candidateViews = nil;
+    if ([self showScopeBar]) {
+        if (self.searchBar.selectedScopeButtonIndex == kFLEXHierarchyScopeViewsAtTapIndex) {
+            candidateViews = self.viewsAtTap;
+        } else if (self.searchBar.selectedScopeButtonIndex == kFLEXHierarchyScopeFullHierarchyIndex) {
+            candidateViews = self.allViews;
+        }
+    } else {
+        candidateViews = self.allViews;
+    }
+    
+    if ([self.searchBar.text length] > 0) {
+        self.displayedViews = [candidateViews filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(UIView *candidateView, NSDictionary *bindings) {
+            NSString *title = [FLEXUtility descriptionForView:candidateView includingFrame:NO];
+            return [title rangeOfString:self.searchBar.text options:NSCaseInsensitiveSearch].location != NSNotFound;
+        }]];
+    } else {
+        self.displayedViews = candidateViews;
+    }
+    
+    [self.tableView reloadData];
+}
+
+- (BOOL)showScopeBar
+{
+    return [self.viewsAtTap count] > 0;
+}
+
+- (void)searchBar:(UISearchBar *)searchBar selectedScopeButtonIndexDidChange:(NSInteger)selectedScope
+{
+    [self updateDisplayedViews];
+    
+    // If the search bar text field is active, don't scroll on selection because we may want to continue typing.
+    // Otherwise, scroll so that the selected cell is visible.
+    UITableViewScrollPosition scrollPosition = self.searchBar.isFirstResponder ? UITableViewScrollPositionNone : UITableViewScrollPositionMiddle;
+    [self trySelectCellForSelectedViewWithScrollPosition:scrollPosition];
+}
+
+- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
+{
+    [self updateDisplayedViews];
+    [self trySelectCellForSelectedViewWithScrollPosition:UITableViewScrollPositionNone];
+}
+
+
+#pragma mark - Table View Data Source
+
+- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
+{
+    return 1;
+}
+
+- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
+{
+    return [self.displayedViews count];
+}
+
+- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
+{
+    static NSString *CellIdentifier = @"Cell";
+    FLEXHierarchyTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
+    if (!cell) {
+        cell = [[FLEXHierarchyTableViewCell alloc] initWithReuseIdentifier:CellIdentifier];
+    }
+    
+    UIView *view = [self.displayedViews objectAtIndex:indexPath.row];
+    NSNumber *depth = [self.depthsForViews objectForKey:[NSValue valueWithNonretainedObject:view]];
+    UIColor *viewColor = [FLEXUtility consistentRandomColorForObject:view];
+    cell.textLabel.text = [FLEXUtility descriptionForView:view includingFrame:NO];
+    cell.detailTextLabel.text = [FLEXUtility detailDescriptionForView:view];
+    cell.viewColor = viewColor;
+    cell.viewDepth = [depth integerValue];
+    if (view.isHidden || view.alpha < 0.01) {
+        cell.textLabel.textColor = [UIColor lightGrayColor];
+        cell.detailTextLabel.textColor = [UIColor lightGrayColor];
+    } else {
+        cell.textLabel.textColor = [UIColor blackColor];
+        cell.detailTextLabel.textColor = [UIColor blackColor];
+    }
+    
+    return cell;
+}
+
+- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
+{
+    self.selectedView = [self.displayedViews objectAtIndex:indexPath.row];
+    [self.delegate hierarchyViewController:self didFinishWithSelectedView:self.selectedView];
+}
+
+- (void)tableView:(UITableView *)tableView accessoryButtonTappedForRowWithIndexPath:(NSIndexPath *)indexPath
+{
+    UIView *drillInView = [self.displayedViews objectAtIndex:indexPath.row];
+    FLEXObjectExplorerViewController *viewExplorer = [FLEXObjectExplorerFactory explorerViewControllerForObject:drillInView];
+    [self.navigationController pushViewController:viewExplorer animated:YES];
+}
+
+
+#pragma mark - Button Actions
+
+- (void)donePressed:(id)sender
+{
+    [self.delegate hierarchyViewController:self didFinishWithSelectedView:self.selectedView];
+}
+
+@end

BIN
Image Originals/close-icon.png


BIN
Image Originals/close-icon@2x.png


BIN
Image Originals/drag-handle.png


BIN
Image Originals/drag-handle@2x.png


BIN
Image Originals/globe-icon.png


BIN
Image Originals/globe-icon@2x.png


BIN
Image Originals/hierarchy-indent-pattern.png


BIN
Image Originals/hierarchy-indent-pattern@2x.png


BIN
Image Originals/list-icon.png


BIN
Image Originals/list-icon@2x.png


BIN
Image Originals/move-icon.png


BIN
Image Originals/move-icon@2x.png


BIN
Image Originals/select-icon.png


BIN
Image Originals/select-icon@2x.png