Browse Source

Merge pull request #46 from Flipboard/network-debugging

Network Debugging
Ryan Olson 11 years ago
parent
commit
ed938aa333
43 changed files with 3436 additions and 166 deletions
  1. 3 9
      Classes/Explorer Toolbar/FLEXExplorerViewController.m
  2. 9 0
      Classes/Explorer Toolbar/FLEXManager.h
  3. 21 0
      Classes/Explorer Toolbar/FLEXManager.m
  4. 7 3
      Classes/Global State Explorers/FLEXFileBrowserTableViewController.m
  5. 11 1
      Classes/Global State Explorers/FLEXGlobalsTableViewController.m
  6. 2 1
      Classes/Global State Explorers/FLEXWebViewController.m
  7. 11 5
      Classes/Global State Explorers/System Log/FLEXSystemLogTableViewController.m
  8. 13 0
      Classes/Network/FLEXNetworkHistoryTableViewController.h
  9. 348 0
      Classes/Network/FLEXNetworkHistoryTableViewController.m
  10. 63 0
      Classes/Network/FLEXNetworkRecorder.h
  11. 255 0
      Classes/Network/FLEXNetworkRecorder.m
  12. 13 0
      Classes/Network/FLEXNetworkSettingsTableViewController.h
  13. 194 0
      Classes/Network/FLEXNetworkSettingsTableViewController.m
  14. 40 0
      Classes/Network/FLEXNetworkTransaction.h
  15. 52 0
      Classes/Network/FLEXNetworkTransaction.m
  16. 17 0
      Classes/Network/FLEXNetworkTransactionDetailTableViewController.h
  17. 438 0
      Classes/Network/FLEXNetworkTransactionDetailTableViewController.m
  18. 21 0
      Classes/Network/FLEXNetworkTransactionTableViewCell.h
  19. 178 0
      Classes/Network/FLEXNetworkTransactionTableViewCell.m
  20. 31 0
      Classes/Network/PonyDebugger/FLEXNetworkObserver.h
  21. 1182 0
      Classes/Network/PonyDebugger/FLEXNetworkObserver.m
  22. 16 0
      Classes/Network/PonyDebugger/LICENSE
  23. 0 15
      Classes/Object Explorers/FLEXDescriptionTableViewCell.h
  24. 0 85
      Classes/Object Explorers/FLEXDescriptionTableViewCell.m
  25. 6 4
      Classes/Object Explorers/FLEXObjectExplorerViewController.m
  26. 1 6
      Classes/Object Explorers/FLEXViewExplorerViewController.m
  27. 17 0
      Classes/Utility/FLEXMultilineTableViewCell.h
  28. 55 0
      Classes/Utility/FLEXMultilineTableViewCell.m
  29. 11 0
      Classes/Utility/FLEXResources.h
  30. 73 0
      Classes/Utility/FLEXResources.m
  31. 6 0
      Classes/Utility/FLEXUtility.h
  32. 137 0
      Classes/Utility/FLEXUtility.m
  33. 1 7
      Classes/View Hierarchy/FLEXHierarchyTableViewCell.m
  34. 1 3
      Classes/View Hierarchy/FLEXHierarchyTableViewController.m
  35. 75 8
      Example/UICatalog.xcodeproj/project.pbxproj
  36. 103 1
      Example/UICatalog/AAPLAppDelegate.m
  37. 3 0
      Example/UICatalog/AAPLMasterViewController.m
  38. 12 16
      Example/UICatalog/Images.xcassets/LaunchImage.launchimage/Contents.json
  39. BIN
      Example/UICatalog/Images.xcassets/LaunchImage.launchimage/Launch.png
  40. BIN
      Example/UICatalog/Images.xcassets/LaunchImage.launchimage/Launch@2x.png
  41. BIN
      Example/UICatalog/Images.xcassets/LaunchImage.launchimage/Launch@2x~568h.png
  42. 2 1
      FLEX.podspec
  43. 8 1
      README.md

+ 3 - 9
Classes/Explorer Toolbar/FLEXExplorerViewController.m

@@ -851,11 +851,8 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
     // If this app doesn't use view controller based status bar management and we're on iOS 7+,
     // If this app doesn't use view controller based status bar management and we're on iOS 7+,
     // make sure the status bar style is UIStatusBarStyleDefault. We don't actully have to check
     // make sure the status bar style is UIStatusBarStyleDefault. We don't actully have to check
     // for view controller based management because the global methods no-op if that is turned on.
     // for view controller based management because the global methods no-op if that is turned on.
-    // Only for iOS 7+
-    if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_6_1) {
-        self.previousStatusBarStyle = [[UIApplication sharedApplication] statusBarStyle];
-        [[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleDefault];
-    }
+    self.previousStatusBarStyle = [[UIApplication sharedApplication] statusBarStyle];
+    [[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleDefault];
     
     
     // Show the view controller.
     // Show the view controller.
     [self presentViewController:viewController animated:animated completion:completion];
     [self presentViewController:viewController animated:animated completion:completion];
@@ -872,10 +869,7 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
     [[self statusWindow] setWindowLevel:UIWindowLevelStatusBar];
     [[self statusWindow] setWindowLevel:UIWindowLevelStatusBar];
     
     
     // Restore the stauts bar style if the app is using global status bar management.
     // Restore the stauts bar style if the app is using global status bar management.
-    // Only for iOS 7+
-    if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_6_1) {
-        [[UIApplication sharedApplication] setStatusBarStyle:self.previousStatusBarStyle];
-    }
+    [[UIApplication sharedApplication] setStatusBarStyle:self.previousStatusBarStyle];
     
     
     [self dismissViewControllerAnimated:animated completion:completion];
     [self dismissViewControllerAnimated:animated completion:completion];
 }
 }

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

@@ -17,6 +17,15 @@
 - (void)showExplorer;
 - (void)showExplorer;
 - (void)hideExplorer;
 - (void)hideExplorer;
 
 
+/// If this property is set to YES, FLEX will swizzle NSURLConnection*Delegate and NSURLSession*Delegate methods
+/// on classes that conform to the protocols. This allows you to view network activity history from the main FLEX menu.
+/// Full responses are kept temporarily in a size limited cache and may be pruged under memory pressure.
+@property (nonatomic, assign, getter=isNetworkDebuggingEnabled) BOOL networkDebuggingEnabled;
+
+/// Defaults to 50 MB if never set. Values set here are presisted across launches of the app.
+/// The response cache uses an NSCache, so it may purge prior to hitting the limit when the app is under memory pressure.
+@property (nonatomic, assign) NSUInteger networkResponseCacheByteLimit;
+
 #pragma mark - Extensions
 #pragma mark - Extensions
 
 
 /// Adds an entry at the bottom of the list of Global State items. Call this method before this view controller is displayed.
 /// Adds an entry at the bottom of the list of Global State items. Call this method before this view controller is displayed.

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

@@ -12,6 +12,8 @@
 #import "FLEXGlobalsTableViewControllerEntry.h"
 #import "FLEXGlobalsTableViewControllerEntry.h"
 #import "FLEXObjectExplorerFactory.h"
 #import "FLEXObjectExplorerFactory.h"
 #import "FLEXObjectExplorerViewController.h"
 #import "FLEXObjectExplorerViewController.h"
+#import "FLEXNetworkObserver.h"
+#import "FLEXNetworkRecorder.h"
 
 
 @interface FLEXManager () <FLEXWindowEventDelegate, FLEXExplorerViewControllerDelegate>
 @interface FLEXManager () <FLEXWindowEventDelegate, FLEXExplorerViewControllerDelegate>
 
 
@@ -81,6 +83,25 @@
     return self.explorerWindow.isHidden;
     return self.explorerWindow.isHidden;
 }
 }
 
 
+- (BOOL)isNetworkDebuggingEnabled
+{
+    return [FLEXNetworkObserver isEnabled];
+}
+
+- (void)setNetworkDebuggingEnabled:(BOOL)networkDebuggingEnabled
+{
+    [FLEXNetworkObserver setEnabled:networkDebuggingEnabled];
+}
+
+- (NSUInteger)networkResponseCacheByteLimit
+{
+    return [[FLEXNetworkRecorder defaultRecorder] responseCacheByteLimit];
+}
+
+- (void)setNetworkResponseCacheByteLimit:(NSUInteger)networkResponseCacheByteLimit
+{
+    [[FLEXNetworkRecorder defaultRecorder] setResponseCacheByteLimit:networkResponseCacheByteLimit];
+}
 
 
 #pragma mark - FLEXWindowEventDelegate
 #pragma mark - FLEXWindowEventDelegate
 
 

+ 7 - 3
Classes/Global State Explorers/FLEXFileBrowserTableViewController.m

@@ -23,7 +23,10 @@
 @property (nonatomic, strong) NSArray *searchPaths;
 @property (nonatomic, strong) NSArray *searchPaths;
 @property (nonatomic, strong) NSNumber *recursiveSize;
 @property (nonatomic, strong) NSNumber *recursiveSize;
 @property (nonatomic, strong) NSNumber *searchPathsSize;
 @property (nonatomic, strong) NSNumber *searchPathsSize;
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
 @property (nonatomic, strong) UISearchDisplayController *searchController;
 @property (nonatomic, strong) UISearchDisplayController *searchController;
+#pragma clang diagnostic pop
 @property (nonatomic) NSOperationQueue *operationQueue;
 @property (nonatomic) NSOperationQueue *operationQueue;
 @property (nonatomic, strong) UIDocumentInteractionController *documentController;
 @property (nonatomic, strong) UIDocumentInteractionController *documentController;
 @property (nonatomic, strong) id<FLEXFileBrowserFileOperationController> fileOperationController;
 @property (nonatomic, strong) id<FLEXFileBrowserFileOperationController> fileOperationController;
@@ -48,7 +51,10 @@
         //add search controller
         //add search controller
         UISearchBar *searchBar = [UISearchBar new];
         UISearchBar *searchBar = [UISearchBar new];
         [searchBar sizeToFit];
         [searchBar sizeToFit];
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
         self.searchController = [[UISearchDisplayController alloc] initWithSearchBar:searchBar contentsController:self];
         self.searchController = [[UISearchDisplayController alloc] initWithSearchBar:searchBar contentsController:self];
+#pragma clang diagnostic pop
         self.searchController.delegate = self;
         self.searchController.delegate = self;
         self.searchController.searchResultsDataSource = self;
         self.searchController.searchResultsDataSource = self;
         self.searchController.searchResultsDelegate = self;
         self.searchController.searchResultsDelegate = self;
@@ -237,9 +243,7 @@
             if ([[subpath pathExtension] isEqual:@"archive"]) {
             if ([[subpath pathExtension] isEqual:@"archive"]) {
                 prettyString = [[NSKeyedUnarchiver unarchiveObjectWithFile:fullPath] description];
                 prettyString = [[NSKeyedUnarchiver unarchiveObjectWithFile:fullPath] description];
             } else if ([[subpath pathExtension] isEqualToString:@"json"]) {
             } else if ([[subpath pathExtension] isEqualToString:@"json"]) {
-                NSData *fileData = [NSData dataWithContentsOfFile:fullPath];
-                id jsonObject = [NSJSONSerialization JSONObjectWithData:fileData options:0 error:NULL];
-                prettyString = [[NSString alloc] initWithData:[NSJSONSerialization dataWithJSONObject:jsonObject options:NSJSONWritingPrettyPrinted error:NULL] encoding:NSUTF8StringEncoding];
+                prettyString = [FLEXUtility prettyJSONStringFromData:[NSData dataWithContentsOfFile:fullPath]];
             } else if ([[subpath pathExtension] isEqualToString:@"plist"]) {
             } else if ([[subpath pathExtension] isEqualToString:@"plist"]) {
                 NSData *fileData = [NSData dataWithContentsOfFile:fullPath];
                 NSData *fileData = [NSData dataWithContentsOfFile:fullPath];
                 prettyString = [[NSPropertyListSerialization propertyListWithData:fileData options:0 format:NULL error:NULL] description];
                 prettyString = [[NSPropertyListSerialization propertyListWithData:fileData options:0 format:NULL error:NULL] description];

+ 11 - 1
Classes/Global State Explorers/FLEXGlobalsTableViewController.m

@@ -17,10 +17,12 @@
 #import "FLEXGlobalsTableViewControllerEntry.h"
 #import "FLEXGlobalsTableViewControllerEntry.h"
 #import "FLEXManager+Private.h"
 #import "FLEXManager+Private.h"
 #import "FLEXSystemLogTableViewController.h"
 #import "FLEXSystemLogTableViewController.h"
+#import "FLEXNetworkHistoryTableViewController.h"
 
 
 static __weak UIWindow *s_applicationWindow = nil;
 static __weak UIWindow *s_applicationWindow = nil;
 
 
 typedef NS_ENUM(NSUInteger, FLEXGlobalsRow) {
 typedef NS_ENUM(NSUInteger, FLEXGlobalsRow) {
+    FLEXGlobalsRowNetworkHistory,
     FLEXGlobalsRowSystemLog,
     FLEXGlobalsRowSystemLog,
     FLEXGlobalsRowLiveObjects,
     FLEXGlobalsRowLiveObjects,
     FLEXGlobalsRowFileBrowser,
     FLEXGlobalsRowFileBrowser,
@@ -178,6 +180,14 @@ typedef NS_ENUM(NSUInteger, FLEXGlobalsRow) {
                 };
                 };
                 break;
                 break;
 
 
+            case FLEXGlobalsRowNetworkHistory:
+                titleFuture = ^{
+                    return @"📡  Network History";
+                };
+                viewControllerFuture = ^{
+                    return [[FLEXNetworkHistoryTableViewController alloc] init];
+                };
+                break;
             case FLEXGlobalsRowCount:
             case FLEXGlobalsRowCount:
                 break;
                 break;
         }
         }
@@ -264,7 +274,7 @@ typedef NS_ENUM(NSUInteger, FLEXGlobalsRow) {
     if (!cell) {
     if (!cell) {
         cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
         cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
         cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
         cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
-        cell.textLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
+        cell.textLabel.font = [FLEXUtility defaultFontOfSize:14.0];
     }
     }
 
 
     cell.textLabel.text = [self titleForRowAtIndexPath:indexPath];
     cell.textLabel.text = [self titleForRowAtIndexPath:indexPath];

+ 2 - 1
Classes/Global State Explorers/FLEXWebViewController.m

@@ -35,7 +35,7 @@
     self = [self initWithNibName:nil bundle:nil];
     self = [self initWithNibName:nil bundle:nil];
     if (self) {
     if (self) {
         self.originalText = text;
         self.originalText = text;
-        NSString *htmlString = [NSString stringWithFormat:@"<pre>%@</pre>", [FLEXUtility stringByEscapingHTMLEntitiesInString:text]];
+        NSString *htmlString = [NSString stringWithFormat:@"<head><meta name='viewport' content='initial-scale=1.0'></head><body><pre>%@</pre></body>", [FLEXUtility stringByEscapingHTMLEntitiesInString:text]];
         [self.webView loadHTMLString:htmlString baseURL:nil];
         [self.webView loadHTMLString:htmlString baseURL:nil];
     }
     }
     return self;
     return self;
@@ -90,6 +90,7 @@
         // For clicked links, push another web view controller onto the navigation stack so that hitting the back button works as expected.
         // For clicked links, push another web view controller onto the navigation stack so that hitting the back button works as expected.
         // Don't allow the current web view do handle the navigation.
         // Don't allow the current web view do handle the navigation.
         FLEXWebViewController *webVC = [[[self class] alloc] initWithURL:[request URL]];
         FLEXWebViewController *webVC = [[[self class] alloc] initWithURL:[request URL]];
+        webVC.title = [[request URL] absoluteString];
         [self.navigationController pushViewController:webVC animated:YES];
         [self.navigationController pushViewController:webVC animated:YES];
     }
     }
     return shouldStart;
     return shouldStart;

+ 11 - 5
Classes/Global State Explorers/System Log/FLEXSystemLogTableViewController.m

@@ -14,7 +14,10 @@
 
 
 @interface FLEXSystemLogTableViewController () <UISearchDisplayDelegate>
 @interface FLEXSystemLogTableViewController () <UISearchDisplayDelegate>
 
 
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
 @property (nonatomic, strong) UISearchDisplayController *searchController;
 @property (nonatomic, strong) UISearchDisplayController *searchController;
+#pragma clang diagnostic pop
 @property (nonatomic, copy) NSArray *logMessages;
 @property (nonatomic, copy) NSArray *logMessages;
 @property (nonatomic, copy) NSArray *filteredLogMessages;
 @property (nonatomic, copy) NSArray *filteredLogMessages;
 @property (nonatomic, strong) NSTimer *logUpdateTimer;
 @property (nonatomic, strong) NSTimer *logUpdateTimer;
@@ -34,7 +37,10 @@
 
 
     UISearchBar *searchBar = [[UISearchBar alloc] init];
     UISearchBar *searchBar = [[UISearchBar alloc] init];
     [searchBar sizeToFit];
     [searchBar sizeToFit];
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
     self.searchController = [[UISearchDisplayController alloc] initWithSearchBar:searchBar contentsController:self];
     self.searchController = [[UISearchDisplayController alloc] initWithSearchBar:searchBar contentsController:self];
+#pragma clang diagnostic pop
     self.searchController.delegate = self;
     self.searchController.delegate = self;
     self.searchController.searchResultsDataSource = self;
     self.searchController.searchResultsDataSource = self;
     self.searchController.searchResultsDelegate = self;
     self.searchController.searchResultsDelegate = self;
@@ -105,7 +111,7 @@
     NSInteger numberOfRows = 0;
     NSInteger numberOfRows = 0;
     if (tableView == self.tableView) {
     if (tableView == self.tableView) {
         numberOfRows = [self.logMessages count];
         numberOfRows = [self.logMessages count];
-    } else if (tableView == self.searchDisplayController.searchResultsTableView) {
+    } else if (tableView == self.searchController.searchResultsTableView) {
         numberOfRows = [self.filteredLogMessages count];
         numberOfRows = [self.filteredLogMessages count];
     }
     }
     return numberOfRows;
     return numberOfRows;
@@ -117,9 +123,9 @@
     if (tableView == self.tableView) {
     if (tableView == self.tableView) {
         cell.logMessage = [self.logMessages objectAtIndex:indexPath.row];
         cell.logMessage = [self.logMessages objectAtIndex:indexPath.row];
         cell.highlightedText = nil;
         cell.highlightedText = nil;
-    } else if (tableView == self.searchDisplayController.searchResultsTableView) {
+    } else if (tableView == self.searchController.searchResultsTableView) {
         cell.logMessage = [self.filteredLogMessages objectAtIndex:indexPath.row];
         cell.logMessage = [self.filteredLogMessages objectAtIndex:indexPath.row];
-        cell.highlightedText = self.searchDisplayController.searchBar.text;
+        cell.highlightedText = self.searchController.searchBar.text;
     }
     }
     if (indexPath.row % 2 == 0) {
     if (indexPath.row % 2 == 0) {
         cell.backgroundColor = [UIColor colorWithWhite:0.95 alpha:1.0];
         cell.backgroundColor = [UIColor colorWithWhite:0.95 alpha:1.0];
@@ -135,7 +141,7 @@
     FLEXSystemLogMessage *logMessage = nil;
     FLEXSystemLogMessage *logMessage = nil;
     if (tableView == self.tableView) {
     if (tableView == self.tableView) {
         logMessage = [self.logMessages objectAtIndex:indexPath.row];
         logMessage = [self.logMessages objectAtIndex:indexPath.row];
-    } else if (tableView == self.searchDisplayController.searchResultsTableView) {
+    } else if (tableView == self.searchController.searchResultsTableView) {
         logMessage = [self.filteredLogMessages objectAtIndex:indexPath.row];
         logMessage = [self.filteredLogMessages objectAtIndex:indexPath.row];
     }
     }
     return [FLEXSystemLogTableViewCell preferredHeightForLogMessage:logMessage inWidth:self.tableView.bounds.size.width];
     return [FLEXSystemLogTableViewCell preferredHeightForLogMessage:logMessage inWidth:self.tableView.bounds.size.width];
@@ -159,7 +165,7 @@
         FLEXSystemLogMessage *logMessage = nil;
         FLEXSystemLogMessage *logMessage = nil;
         if (tableView == self.tableView) {
         if (tableView == self.tableView) {
             logMessage = [self.logMessages objectAtIndex:indexPath.row];
             logMessage = [self.logMessages objectAtIndex:indexPath.row];
-        } else if (tableView == self.searchDisplayController.searchResultsTableView) {
+        } else if (tableView == self.searchController.searchResultsTableView) {
             logMessage = [self.filteredLogMessages objectAtIndex:indexPath.row];
             logMessage = [self.filteredLogMessages objectAtIndex:indexPath.row];
         }
         }
 
 

+ 13 - 0
Classes/Network/FLEXNetworkHistoryTableViewController.h

@@ -0,0 +1,13 @@
+//
+//  FLEXNetworkHistoryTableViewController.h
+//  Flipboard
+//
+//  Created by Ryan Olson on 2/8/15.
+//  Copyright (c) 2015 Flipboard. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+
+@interface FLEXNetworkHistoryTableViewController : UITableViewController
+
+@end

+ 348 - 0
Classes/Network/FLEXNetworkHistoryTableViewController.m

@@ -0,0 +1,348 @@
+//
+//  FLEXNetworkHistoryTableViewController.m
+//  Flipboard
+//
+//  Created by Ryan Olson on 2/8/15.
+//  Copyright (c) 2015 Flipboard. All rights reserved.
+//
+
+#import "FLEXNetworkHistoryTableViewController.h"
+#import "FLEXNetworkTransaction.h"
+#import "FLEXNetworkTransactionTableViewCell.h"
+#import "FLEXNetworkRecorder.h"
+#import "FLEXNetworkTransactionDetailTableViewController.h"
+#import "FLEXNetworkObserver.h"
+#import "FLEXNetworkSettingsTableViewController.h"
+
+@interface FLEXNetworkHistoryTableViewController () <UISearchDisplayDelegate>
+
+/// Backing model
+@property (nonatomic, copy) NSArray *networkTransactions;
+@property (nonatomic, assign) long long bytesReceived;
+@property (nonatomic, copy) NSArray *filteredNetworkTransactions;
+@property (nonatomic, assign) long long filteredBytesReceived;
+
+@property (nonatomic, assign) BOOL rowInsertInProgress;
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
+@property (nonatomic, strong) UISearchDisplayController *searchController;
+#pragma clang diagnostic pop
+
+@end
+
+@implementation FLEXNetworkHistoryTableViewController
+
+- (instancetype)initWithStyle:(UITableViewStyle)style
+{
+    self = [super initWithStyle:style];
+    if (self) {
+        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNewTransactionRecordedNotification:) name:kFLEXNetworkRecorderNewTransactionNotification object:nil];
+        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleTransactionUpdatedNotification:) name:kFLEXNetworkRecorderTransactionUpdatedNotification object:nil];
+        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleTransactionsClearedNotification:) name:kFLEXNetworkRecorderTransactionsClearedNotification object:nil];
+        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleNetworkObserverEnabledStateChangedNotification:) name:kFLEXNetworkObserverEnabledStateChangedNotification object:nil];
+        self.title = @"📡  Network";
+        self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Settings" style:UIBarButtonItemStylePlain target:self action:@selector(settingsButtonTapped:)];
+    }
+    return self;
+}
+
+- (void)dealloc
+{
+    [[NSNotificationCenter defaultCenter] removeObserver:self];
+}
+
+- (void)viewDidLoad
+{
+    [super viewDidLoad];
+
+    [self.tableView registerClass:[FLEXNetworkTransactionTableViewCell class] forCellReuseIdentifier:kFLEXNetworkTransactionCellIdentifier];
+    self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
+    self.tableView.rowHeight = [FLEXNetworkTransactionTableViewCell preferredCellHeight];
+
+    UISearchBar *searchBar = [[UISearchBar alloc] init];
+    [searchBar sizeToFit];
+    self.searchController = [[UISearchDisplayController alloc] initWithSearchBar:searchBar contentsController:self];
+    self.searchController.delegate = self;
+    self.searchController.searchResultsDataSource = self;
+    self.searchController.searchResultsDelegate = self;
+    [self.searchController.searchResultsTableView registerClass:[FLEXNetworkTransactionTableViewCell class] forCellReuseIdentifier:kFLEXNetworkTransactionCellIdentifier];
+    self.searchController.searchResultsTableView.separatorStyle = UITableViewCellSeparatorStyleNone;
+    self.searchController.searchResultsTableView.rowHeight = [FLEXNetworkTransactionTableViewCell preferredCellHeight];
+    self.tableView.tableHeaderView = self.searchController.searchBar;
+
+    [self updateTransactions];
+}
+
+- (void)settingsButtonTapped:(id)sender
+{
+    FLEXNetworkSettingsTableViewController *settingsViewController = [[FLEXNetworkSettingsTableViewController alloc] init];
+    settingsViewController.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(settingsViewControllerDoneTapped:)];
+    settingsViewController.title = @"Network Debugging Settings";
+    UINavigationController *wrapperNavigationController = [[UINavigationController alloc] initWithRootViewController:settingsViewController];
+    [self presentViewController:wrapperNavigationController animated:YES completion:nil];
+}
+
+- (void)settingsViewControllerDoneTapped:(id)sender
+{
+    [self dismissViewControllerAnimated:YES completion:nil];
+}
+
+- (void)updateTransactions
+{
+    self.networkTransactions = [[FLEXNetworkRecorder defaultRecorder] networkTransactions];
+}
+
+- (void)setNetworkTransactions:(NSArray *)networkTransactions
+{
+    if (![_networkTransactions isEqual:networkTransactions]) {
+        _networkTransactions = networkTransactions;
+        [self updateBytesReceived];
+        [self updateFilteredBytesReceived];
+    }
+}
+
+- (void)updateBytesReceived
+{
+    long long bytesReceived = 0;
+    for (FLEXNetworkTransaction *transaction in self.networkTransactions) {
+        bytesReceived += transaction.receivedDataLength;
+    }
+    self.bytesReceived = bytesReceived;
+    [self updateFirstSectionHeaderInTableView:self.tableView];
+}
+
+- (void)setFilteredNetworkTransactions:(NSArray *)filteredNetworkTransactions
+{
+    if (![_filteredNetworkTransactions isEqual:filteredNetworkTransactions]) {
+        _filteredNetworkTransactions = filteredNetworkTransactions;
+        [self updateFilteredBytesReceived];
+    }
+}
+
+- (void)updateFilteredBytesReceived
+{
+    long long filteredBytesReceived = 0;
+    for (FLEXNetworkTransaction *transaction in self.filteredNetworkTransactions) {
+        filteredBytesReceived += transaction.receivedDataLength;
+    }
+    self.filteredBytesReceived = filteredBytesReceived;
+    [self updateFirstSectionHeaderInTableView:self.searchController.searchResultsTableView];
+}
+
+- (void)updateFirstSectionHeaderInTableView:(UITableView *)tableView
+{
+    UIView *view = [tableView headerViewForSection:0];
+    if ([view isKindOfClass:[UITableViewHeaderFooterView class]]) {
+        UITableViewHeaderFooterView *headerView = (UITableViewHeaderFooterView *)view;
+        headerView.textLabel.text = [self headerTextForTableView:tableView];
+        [headerView setNeedsLayout];
+    }
+}
+
+- (NSString *)headerTextForTableView:(UITableView *)tableView
+{
+    NSString *headerText = nil;
+    if ([FLEXNetworkObserver isEnabled]) {
+        long long bytesReceived = 0;
+        NSInteger totalRequests = 0;
+        if (tableView == self.tableView) {
+            bytesReceived = self.bytesReceived;
+            totalRequests = [self.networkTransactions count];
+        } else if (tableView == self.searchController.searchResultsTableView) {
+            bytesReceived = self.filteredBytesReceived;
+            totalRequests = [self.filteredNetworkTransactions count];
+        }
+        NSString *byteCountText = [NSByteCountFormatter stringFromByteCount:bytesReceived countStyle:NSByteCountFormatterCountStyleBinary];
+        NSString *requestsText = totalRequests == 1 ? @"Request" : @"Requests";
+        headerText = [NSString stringWithFormat:@"%ld %@ (%@ received)", (long)totalRequests, requestsText, byteCountText];
+    } else {
+        headerText = @"⚠️  Debugging Disabled (Enable in Settings)";
+    }
+    return headerText;
+}
+
+#pragma mark - Notification Handlers
+
+- (void)handleNewTransactionRecordedNotification:(NSNotification *)notification
+{
+    [self tryUpdateTransactions];
+}
+
+- (void)tryUpdateTransactions
+{
+    // Let the previous row insert animation finish before starting a new one to avoid stomping.
+    // We'll try calling the method again when the insertion completes, and we properly no-op if there haven't been changes.
+    if (self.rowInsertInProgress) {
+        return;
+    }
+
+    NSInteger existingRowCount = [self.networkTransactions count];
+    [self updateTransactions];
+    NSInteger newRowCount = [self.networkTransactions count];
+    NSInteger addedRowCount = newRowCount - existingRowCount;
+
+    if (addedRowCount != 0) {
+        // Insert animation if we're at the top.
+        if (self.tableView.contentOffset.y <= 0.0 && addedRowCount > 0) {
+            [CATransaction begin];
+            
+            self.rowInsertInProgress = YES;
+            [CATransaction setCompletionBlock:^{
+                self.rowInsertInProgress = NO;
+                [self tryUpdateTransactions];
+            }];
+
+            NSMutableArray *indexPathsToReload = [NSMutableArray array];
+            for (NSInteger row = 0; row < addedRowCount; row++) {
+                [indexPathsToReload addObject:[NSIndexPath indexPathForRow:row inSection:0]];
+            }
+            [self.tableView insertRowsAtIndexPaths:indexPathsToReload withRowAnimation:UITableViewRowAnimationAutomatic];
+
+            [CATransaction commit];
+        } else {
+            // Maintain the user's position if they've scrolled down.
+            CGSize existingContentSize = self.tableView.contentSize;
+            [self.tableView reloadData];
+            CGFloat contentHeightChange = self.tableView.contentSize.height - existingContentSize.height;
+            self.tableView.contentOffset = CGPointMake(self.tableView.contentOffset.x, self.tableView.contentOffset.y + contentHeightChange);
+        }
+
+        if (self.searchController.isActive) {
+            [self updateSearchResultsWithSearchString:self.searchController.searchBar.text];
+        }
+    }
+}
+
+- (void)handleTransactionUpdatedNotification:(NSNotification *)notification
+{
+    [self updateBytesReceived];
+    [self updateFilteredBytesReceived];
+
+    FLEXNetworkTransaction *transaction = [notification.userInfo objectForKey:kFLEXNetworkRecorderUserInfoTransactionKey];
+    NSArray *tableViews = @[self.tableView];
+    if (self.searchController.searchResultsTableView) {
+        tableViews = [tableViews arrayByAddingObject:self.searchController.searchResultsTableView];
+    }
+
+    // Update both the main table view and search table view if needed.
+    for (UITableView *tableView in tableViews) {
+        for (FLEXNetworkTransactionTableViewCell *cell in [tableView visibleCells]) {
+            if ([cell.transaction isEqual:transaction]) {
+                NSIndexPath *indexPath = [tableView indexPathForCell:cell];
+                if (indexPath) {
+                    [tableView reloadRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationNone];
+                }
+                break;
+            }
+        }
+        [self updateFirstSectionHeaderInTableView:tableView];
+    }
+}
+
+- (void)handleTransactionsClearedNotification:(NSNotification *)notification
+{
+    [self updateTransactions];
+    [self.tableView reloadData];
+    [self.searchController.searchResultsTableView reloadData];
+}
+
+- (void)handleNetworkObserverEnabledStateChangedNotification:(NSNotification *)notification
+{
+    // Update the header, which displays a warning when network debugging is disabled
+    [self updateFirstSectionHeaderInTableView:self.tableView];
+}
+
+#pragma mark - Table view data source
+
+- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
+{
+    return 1;
+}
+
+- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
+{
+    NSInteger numberOfRows = 0;
+    if (tableView == self.tableView) {
+        numberOfRows = [self.networkTransactions count];
+    } else if (tableView == self.searchController.searchResultsTableView) {
+        numberOfRows = [self.filteredNetworkTransactions count];
+    }
+    return numberOfRows;
+}
+
+- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
+{
+    return [self headerTextForTableView:tableView];
+}
+
+- (void)tableView:(UITableView *)tableView willDisplayHeaderView:(UIView *)view forSection:(NSInteger)section
+{
+    if ([view isKindOfClass:[UITableViewHeaderFooterView class]]) {
+        UITableViewHeaderFooterView *headerView = (UITableViewHeaderFooterView *)view;
+        headerView.textLabel.font = [UIFont fontWithName:@"HelveticaNeue-Medium" size:14.0];
+        headerView.textLabel.textColor = [UIColor whiteColor];
+        headerView.contentView.backgroundColor = [UIColor colorWithWhite:0.5 alpha:1.0];
+    }
+}
+
+- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
+{
+    FLEXNetworkTransactionTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kFLEXNetworkTransactionCellIdentifier forIndexPath:indexPath];
+    cell.transaction = [self transactionAtIndexPath:indexPath inTableView:tableView];
+
+    // Since we insert from the top, assign background colors bottom up to keep them consistent for each transaction.
+    NSInteger totalRows = [tableView numberOfRowsInSection:indexPath.section];
+    if ((totalRows - indexPath.row) % 2 == 0) {
+        cell.backgroundColor = [UIColor colorWithWhite:0.95 alpha:1.0];
+    } else {
+        cell.backgroundColor = [UIColor whiteColor];
+    }
+
+    return cell;
+}
+
+- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
+{
+    FLEXNetworkTransactionDetailTableViewController *detailViewController = [[FLEXNetworkTransactionDetailTableViewController alloc] init];
+    detailViewController.transaction = [self transactionAtIndexPath:indexPath inTableView:tableView];
+    [self.navigationController pushViewController:detailViewController animated:YES];
+}
+
+- (FLEXNetworkTransaction *)transactionAtIndexPath:(NSIndexPath *)indexPath inTableView:(UITableView *)tableView
+{
+    FLEXNetworkTransaction *transaction = nil;
+    if (tableView == self.tableView) {
+        transaction = [self.networkTransactions objectAtIndex:indexPath.row];
+    } else if (tableView == self.searchController.searchResultsTableView) {
+        transaction = [self.filteredNetworkTransactions objectAtIndex:indexPath.row];
+    }
+    return transaction;
+}
+
+#pragma mark - Search display delegate
+
+- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchString:(NSString *)searchString
+{
+    [self updateSearchResultsWithSearchString:searchString];
+
+    // Reload done after the data is filtered asynchronously
+    return NO;
+}
+
+- (void)updateSearchResultsWithSearchString:(NSString *)searchString
+{
+    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
+        NSArray *filteredNetworkTransactions = [self.networkTransactions filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(FLEXNetworkTransaction *transaction, NSDictionary *bindings) {
+            return [[transaction.request.URL absoluteString] rangeOfString:searchString options:NSCaseInsensitiveSearch].length > 0;
+        }]];
+        dispatch_async(dispatch_get_main_queue(), ^{
+            if ([self.searchDisplayController.searchBar.text isEqual:searchString]) {
+                self.filteredNetworkTransactions = filteredNetworkTransactions;
+                [self.searchDisplayController.searchResultsTableView reloadData];
+            }
+        });
+    });
+}
+
+@end

+ 63 - 0
Classes/Network/FLEXNetworkRecorder.h

@@ -0,0 +1,63 @@
+//
+//  FLEXNetworkRecorder.h
+//  Flipboard
+//
+//  Created by Ryan Olson on 2/4/15.
+//  Copyright (c) 2015 Flipboard. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+
+// Notifications posted when the record is updated
+extern NSString *const kFLEXNetworkRecorderNewTransactionNotification;
+extern NSString *const kFLEXNetworkRecorderTransactionUpdatedNotification;
+extern NSString *const kFLEXNetworkRecorderUserInfoTransactionKey;
+extern NSString *const kFLEXNetworkRecorderTransactionsClearedNotification;
+
+@class FLEXNetworkTransaction;
+
+@interface FLEXNetworkRecorder : NSObject
+
+/// In general, it only makes sense to have one recorder for the entire application.
++ (instancetype)defaultRecorder;
+
+/// Defaults to 25 MB if never set. Values set here are presisted across launches of the app.
+@property (nonatomic, assign) NSUInteger responseCacheByteLimit;
+
+/// If NO, the recorder not cache will not cache response for content types with an "image", "video", or "audio" prefix.
+@property (nonatomic, assign) BOOL shouldCacheMediaResponses;
+
+// Accessing recorded network activity
+
+/// Array of FLEXNetworkTransaction objects ordered by start time with the newest first.
+- (NSArray *)networkTransactions;
+
+/// The full response data IFF it hasn't been purged due to memory pressure.
+- (NSData *)cachedResponseBodyForTransaction:(FLEXNetworkTransaction *)transaction;
+
+/// Dumps all network transactions and cached response bodies.
+- (void)clearRecordedActivity;
+
+
+// Recording network activity
+
+/// Call when app is about to send HTTP request.
+- (void)recordRequestWillBeSentWithRequestID:(NSString *)requestID request:(NSURLRequest *)request redirectResponse:(NSURLResponse *)redirectResponse;
+
+/// Call when HTTP response is available.
+- (void)recordResponseReceivedWithRequestID:(NSString *)requestID response:(NSURLResponse *)response;
+
+/// Call when data chunk is received over the network.
+- (void)recordDataReceivedWithRequestID:(NSString *)requestID dataLength:(int64_t)dataLength;
+
+/// Call when HTTP request has finished loading.
+- (void)recordLoadingFinishedWithRequestID:(NSString *)requestID responseBody:(NSData *)responseBody;
+
+/// Call when HTTP request has failed to load.
+- (void)recordLoadingFailedWithRequestID:(NSString *)requestID error:(NSError *)error;
+
+/// Call to set the request mechanism anytime after recordRequestWillBeSent... has been called.
+/// This string can be set to anything useful about the API used to make the request.
+- (void)recordMechanism:(NSString *)mechanism forRequestID:(NSString *)requestID;
+
+@end

+ 255 - 0
Classes/Network/FLEXNetworkRecorder.m

@@ -0,0 +1,255 @@
+//
+//  FLEXNetworkRecorder.m
+//  Flipboard
+//
+//  Created by Ryan Olson on 2/4/15.
+//  Copyright (c) 2015 Flipboard. All rights reserved.
+//
+
+#import "FLEXNetworkRecorder.h"
+#import "FLEXNetworkTransaction.h"
+#import "FLEXUtility.h"
+#import "FLEXResources.h"
+
+NSString *const kFLEXNetworkRecorderNewTransactionNotification = @"kFLEXNetworkRecorderNewTransactionNotification";
+NSString *const kFLEXNetworkRecorderTransactionUpdatedNotification = @"kFLEXNetworkRecorderTransactionUpdatedNotification";
+NSString *const kFLEXNetworkRecorderUserInfoTransactionKey = @"transaction";
+NSString *const kFLEXNetworkRecorderTransactionsClearedNotification = @"kFLEXNetworkRecorderTransactionsClearedNotification";
+
+NSString *const kFLEXNetworkRecorderResponseCacheLimitDefaultsKey = @"com.flex.responseCacheLimit";
+
+@interface FLEXNetworkRecorder ()
+
+@property (nonatomic, strong) NSCache *responseCache;
+@property (nonatomic, strong) NSMutableArray *orderedTransactions;
+@property (nonatomic, strong) NSMutableDictionary *networkTransactionsForRequestIdentifiers;
+@property (nonatomic, strong) dispatch_queue_t queue;
+
+@end
+
+@implementation FLEXNetworkRecorder
+
+- (instancetype)init
+{
+    self = [super init];
+    if (self) {
+        self.responseCache = [[NSCache alloc] init];
+        NSUInteger responseCacheLimit = [[[NSUserDefaults standardUserDefaults] objectForKey:kFLEXNetworkRecorderResponseCacheLimitDefaultsKey] unsignedIntegerValue];
+        if (responseCacheLimit) {
+            [self.responseCache setTotalCostLimit:responseCacheLimit];
+        } else {
+            // Default to 25 MB max. The cache will purge earlier if there is memory pressure.
+            [self.responseCache setTotalCostLimit:25 * 1024 * 1024];
+        }
+        self.orderedTransactions = [NSMutableArray array];
+        self.networkTransactionsForRequestIdentifiers = [NSMutableDictionary dictionary];
+
+        // Serial queue used because we use mutable objects that are not thread safe
+        self.queue = dispatch_queue_create("com.flex.FLEXNetworkRecorder", DISPATCH_QUEUE_SERIAL);
+    }
+    return self;
+}
+
++ (instancetype)defaultRecorder
+{
+    static FLEXNetworkRecorder *defaultRecorder = nil;
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        defaultRecorder = [[[self class] alloc] init];
+    });
+    return defaultRecorder;
+}
+
+#pragma mark - Public Data Access
+
+- (NSUInteger)responseCacheByteLimit
+{
+    return [self.responseCache totalCostLimit];
+}
+
+- (void)setResponseCacheByteLimit:(NSUInteger)responseCacheByteLimit
+{
+    [self.responseCache setTotalCostLimit:responseCacheByteLimit];
+    [[NSUserDefaults standardUserDefaults] setObject:@(responseCacheByteLimit) forKey:kFLEXNetworkRecorderResponseCacheLimitDefaultsKey];
+}
+
+- (NSArray *)networkTransactions
+{
+    __block NSArray *transactions = nil;
+    dispatch_sync(self.queue, ^{
+        transactions = [self.orderedTransactions copy];
+    });
+    return transactions;
+}
+
+- (NSData *)cachedResponseBodyForTransaction:(FLEXNetworkTransaction *)transaction
+{
+    return [self.responseCache objectForKey:transaction.requestID];
+}
+
+- (void)clearRecordedActivity
+{
+    dispatch_async(self.queue, ^{
+        [self.responseCache removeAllObjects];
+        [self.orderedTransactions removeAllObjects];
+        [self.networkTransactionsForRequestIdentifiers removeAllObjects];
+        dispatch_async(dispatch_get_main_queue(), ^{
+            [[NSNotificationCenter defaultCenter] postNotificationName:kFLEXNetworkRecorderTransactionsClearedNotification object:self];
+        });
+    });
+}
+
+#pragma mark - Network Events
+
+- (void)recordRequestWillBeSentWithRequestID:(NSString *)requestID request:(NSURLRequest *)request redirectResponse:(NSURLResponse *)redirectResponse
+{
+    if (redirectResponse) {
+        [self recordResponseReceivedWithRequestID:requestID response:redirectResponse];
+        [self recordLoadingFinishedWithRequestID:requestID responseBody:nil];
+    }
+
+    dispatch_async(self.queue, ^{
+        FLEXNetworkTransaction *transaction = [[FLEXNetworkTransaction alloc] init];
+        transaction.requestID = requestID;
+        transaction.request = request;
+        transaction.startTime = [NSDate date];
+
+        [self.orderedTransactions insertObject:transaction atIndex:0];
+        [self.networkTransactionsForRequestIdentifiers setObject:transaction forKey:requestID];
+        transaction.transactionState = FLEXNetworkTransactionStateAwaitingResponse;
+
+        [self postNewTransactionNotificationWithTransaction:transaction];
+    });
+}
+
+- (void)recordResponseReceivedWithRequestID:(NSString *)requestID response:(NSURLResponse *)response
+{
+    dispatch_async(self.queue, ^{
+        FLEXNetworkTransaction *transaction = [self.networkTransactionsForRequestIdentifiers objectForKey:requestID];
+        if (!transaction) {
+            return;
+        }
+        transaction.response = response;
+        transaction.transactionState = FLEXNetworkTransactionStateReceivingData;
+        transaction.latency = -[transaction.startTime timeIntervalSinceNow];
+
+        [self postUpdateNotificationForTransaction:transaction];
+    });
+}
+
+- (void)recordDataReceivedWithRequestID:(NSString *)requestID dataLength:(int64_t)dataLength
+{
+    dispatch_async(self.queue, ^{
+        FLEXNetworkTransaction *transaction = [self.networkTransactionsForRequestIdentifiers objectForKey:requestID];
+        if (!transaction) {
+            return;
+        }
+        transaction.receivedDataLength += dataLength;
+
+        [self postUpdateNotificationForTransaction:transaction];
+    });
+}
+
+- (void)recordLoadingFinishedWithRequestID:(NSString *)requestID responseBody:(NSData *)responseBody
+{
+    dispatch_async(self.queue, ^{
+        FLEXNetworkTransaction *transaction = [self.networkTransactionsForRequestIdentifiers objectForKey:requestID];
+        if (!transaction) {
+            return;
+        }
+        transaction.transactionState = FLEXNetworkTransactionStateFinished;
+        transaction.duration = -[transaction.startTime timeIntervalSinceNow];
+
+        BOOL shouldCache = [responseBody length] > 0;
+        if (!self.shouldCacheMediaResponses) {
+            NSArray *ignoredMIMETypePrefixes = @[ @"audio", @"image", @"video" ];
+            for (NSString *ignoredPrefix in ignoredMIMETypePrefixes) {
+                shouldCache = shouldCache && ![transaction.response.MIMEType hasPrefix:ignoredPrefix];
+            }
+        }
+        
+        if (shouldCache) {
+            [self.responseCache setObject:responseBody forKey:requestID cost:[responseBody length]];
+        }
+
+        NSString *mimeType = transaction.response.MIMEType;
+        if ([mimeType hasPrefix:@"image/"]) {
+            // Thumbnail image previews on a separate background queue
+            dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
+                NSInteger maxPixelDimension = [[UIScreen mainScreen] scale] * 32.0;
+                transaction.responseThumbnail = [FLEXUtility thumbnailedImageWithMaxPixelDimension:maxPixelDimension fromImageData:responseBody];
+                [self postUpdateNotificationForTransaction:transaction];
+            });
+        } else if ([mimeType isEqual:@"application/json"]) {
+            transaction.responseThumbnail = [FLEXResources jsonIcon];
+        } else if ([mimeType isEqual:@"text/plain"]){
+            transaction.responseThumbnail = [FLEXResources textPlainIcon];
+        } else if ([mimeType isEqual:@"text/html"]) {
+            transaction.responseThumbnail = [FLEXResources htmlIcon];
+        } else if ([mimeType isEqual:@"application/x-plist"]) {
+            transaction.responseThumbnail = [FLEXResources plistIcon];
+        } else if ([mimeType isEqual:@"application/octet-stream"] || [mimeType isEqual:@"application/binary"]) {
+            transaction.responseThumbnail = [FLEXResources binaryIcon];
+        } else if ([mimeType rangeOfString:@"javascript"].length > 0) {
+            transaction.responseThumbnail = [FLEXResources jsIcon];
+        } else if ([mimeType rangeOfString:@"xml"].length > 0) {
+            transaction.responseThumbnail = [FLEXResources xmlIcon];
+        } else if ([mimeType hasPrefix:@"audio"]) {
+            transaction.responseThumbnail = [FLEXResources audioIcon];
+        } else if ([mimeType hasPrefix:@"video"]) {
+            transaction.responseThumbnail = [FLEXResources videoIcon];
+        } else if ([mimeType hasPrefix:@"text"]) {
+            transaction.responseThumbnail = [FLEXResources textIcon];
+        }
+        
+        [self postUpdateNotificationForTransaction:transaction];
+    });
+}
+
+- (void)recordLoadingFailedWithRequestID:(NSString *)requestID error:(NSError *)error
+{
+    dispatch_async(self.queue, ^{
+        FLEXNetworkTransaction *transaction = [self.networkTransactionsForRequestIdentifiers objectForKey:requestID];
+        if (!transaction) {
+            return;
+        }
+        transaction.transactionState = FLEXNetworkTransactionStateFailed;
+        transaction.duration = -[transaction.startTime timeIntervalSinceNow];
+        transaction.error = error;
+
+        [self postUpdateNotificationForTransaction:transaction];
+    });
+}
+
+- (void)recordMechanism:(NSString *)mechanism forRequestID:(NSString *)requestID
+{
+    dispatch_async(self.queue, ^{
+        FLEXNetworkTransaction *transaction = [self.networkTransactionsForRequestIdentifiers objectForKey:requestID];
+        if (!transaction) {
+            return;
+        }
+        transaction.requestMechanism = mechanism;
+
+        [self postUpdateNotificationForTransaction:transaction];
+    });
+}
+
+#pragma mark Notification Posting
+
+- (void)postNewTransactionNotificationWithTransaction:(FLEXNetworkTransaction *)transaction
+{
+    dispatch_async(dispatch_get_main_queue(), ^{
+        NSDictionary *userInfo = @{ kFLEXNetworkRecorderUserInfoTransactionKey : transaction };
+        [[NSNotificationCenter defaultCenter] postNotificationName:kFLEXNetworkRecorderNewTransactionNotification object:self userInfo:userInfo];
+    });
+}
+
+- (void)postUpdateNotificationForTransaction:(FLEXNetworkTransaction *)transaction
+{
+    dispatch_async(dispatch_get_main_queue(), ^{
+        NSDictionary *userInfo = @{ kFLEXNetworkRecorderUserInfoTransactionKey : transaction };
+        [[NSNotificationCenter defaultCenter] postNotificationName:kFLEXNetworkRecorderTransactionUpdatedNotification object:self userInfo:userInfo];
+    });
+}
+
+@end

+ 13 - 0
Classes/Network/FLEXNetworkSettingsTableViewController.h

@@ -0,0 +1,13 @@
+//
+//  FLEXNetworkSettingsTableViewController.h
+//  FLEXInjected
+//
+//  Created by Ryan Olson on 2/20/15.
+//
+//
+
+#import <UIKit/UIKit.h>
+
+@interface FLEXNetworkSettingsTableViewController : UITableViewController
+
+@end

+ 194 - 0
Classes/Network/FLEXNetworkSettingsTableViewController.m

@@ -0,0 +1,194 @@
+//
+//  FLEXNetworkSettingsTableViewController.m
+//  FLEXInjected
+//
+//  Created by Ryan Olson on 2/20/15.
+//
+//
+
+#import "FLEXNetworkSettingsTableViewController.h"
+#import "FLEXNetworkObserver.h"
+#import "FLEXNetworkRecorder.h"
+#import "FLEXUtility.h"
+
+@interface FLEXNetworkSettingsTableViewController () <UIActionSheetDelegate>
+
+@property (nonatomic, copy) NSArray *cells;
+
+@property (nonatomic, strong) UITableViewCell *cacheLimitCell;
+
+@end
+
+@implementation FLEXNetworkSettingsTableViewController
+
+- (instancetype)initWithStyle:(UITableViewStyle)style
+{
+    self = [super initWithStyle:UITableViewStyleGrouped];
+    if (self) {
+
+    }
+    return self;
+}
+
+- (void)viewDidLoad
+{
+    [super viewDidLoad];
+
+    NSMutableArray *mutableCells = [NSMutableArray array];
+
+    UITableViewCell *networkDebuggingCell = [self switchCellWithTitle:@"Network Debugging" toggleAction:@selector(networkDebuggingToggled:) isOn:[FLEXNetworkObserver isEnabled]];
+    [mutableCells addObject:networkDebuggingCell];
+
+    UITableViewCell *enableOnLaunchCell = [self switchCellWithTitle:@"Enable on Launch" toggleAction:@selector(enableOnLaunchToggled:) isOn:[FLEXNetworkObserver shouldEnableOnLaunch]];
+    [mutableCells addObject:enableOnLaunchCell];
+
+    UITableViewCell *cacheMediaResponsesCell = [self switchCellWithTitle:@"Cache Media Responses" toggleAction:@selector(cacheMediaResponsesToggled:) isOn:NO];
+    [mutableCells addObject:cacheMediaResponsesCell];
+
+    NSUInteger currentCacheLimit = [[FLEXNetworkRecorder defaultRecorder] responseCacheByteLimit];
+    const NSUInteger fiftyMega = 50 * 1024 * 1024;
+    NSString *cacheLimitTitle = [self titleForCacheLimitCellWithValue:currentCacheLimit];
+    self.cacheLimitCell = [self sliderCellWithTitle:cacheLimitTitle changedAction:@selector(cacheLimitAdjusted:) minimum:0.0 maximum:fiftyMega initialValue:currentCacheLimit];
+    [mutableCells addObject:self.cacheLimitCell];
+
+    UITableViewCell *clearRecordedRequestsCell = [self buttonCellWithTitle:@"❌  Clear Recorded Requests" touchUpAction:@selector(clearRequestsTapped:) isDestructive:YES];
+    [mutableCells addObject:clearRecordedRequestsCell];
+
+    self.cells = mutableCells;
+}
+
+#pragma mark - Settings Actions
+
+- (void)networkDebuggingToggled:(UISwitch *)sender
+{
+    [FLEXNetworkObserver setEnabled:sender.isOn];
+}
+
+- (void)enableOnLaunchToggled:(UISwitch *)sender
+{
+    [FLEXNetworkObserver setShouldEnableOnLaunch:sender.isOn];
+}
+
+- (void)cacheMediaResponsesToggled:(UISwitch *)sender
+{
+    [[FLEXNetworkRecorder defaultRecorder] setShouldCacheMediaResponses:sender.isOn];
+}
+
+- (void)cacheLimitAdjusted:(UISlider *)sender
+{
+    [[FLEXNetworkRecorder defaultRecorder] setResponseCacheByteLimit:sender.value];
+    self.cacheLimitCell.textLabel.text = [self titleForCacheLimitCellWithValue:sender.value];
+}
+
+- (void)clearRequestsTapped:(UIButton *)sender
+{
+    UIActionSheet *actionSheet = [[UIActionSheet alloc] initWithTitle:nil delegate:self cancelButtonTitle:@"Cancel" destructiveButtonTitle:@"Clear Recorded Requests" otherButtonTitles:nil];
+    [actionSheet showInView:self.view];
+}
+
+#pragma mark - Table view data source
+
+- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
+{
+    return 1;
+}
+
+- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
+{
+    return [self.cells count];
+}
+
+- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 
+{
+    return [self.cells objectAtIndex:indexPath.row];
+}
+
+#pragma mark - UIActionSheetDelegate
+
+- (void)actionSheet:(UIActionSheet *)actionSheet clickedButtonAtIndex:(NSInteger)buttonIndex
+{
+    if (buttonIndex != actionSheet.cancelButtonIndex) {
+        [[FLEXNetworkRecorder defaultRecorder] clearRecordedActivity];
+    }
+}
+
+#pragma mark - Helpers
+
+- (UITableViewCell *)switchCellWithTitle:(NSString *)title toggleAction:(SEL)toggleAction isOn:(BOOL)isOn
+{
+    UITableViewCell *cell = [[UITableViewCell alloc] init];
+    cell.selectionStyle = UITableViewCellSelectionStyleNone;
+    cell.textLabel.text = title;
+    cell.textLabel.font = [[self class] cellTitleFont];
+
+    UISwitch *theSwitch = [[UISwitch alloc] init];
+    theSwitch.on = isOn;
+    [theSwitch addTarget:self action:toggleAction forControlEvents:UIControlEventValueChanged];
+
+    CGFloat switchOriginY = round((cell.contentView.frame.size.height - theSwitch.frame.size.height) / 2.0);
+    CGFloat switchOriginX = CGRectGetMaxX(cell.contentView.frame) - theSwitch.frame.size.width - self.tableView.separatorInset.left;
+    theSwitch.frame = CGRectMake(switchOriginX, switchOriginY, theSwitch.frame.size.width, theSwitch.frame.size.height);
+    theSwitch.autoresizingMask = UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin;
+    [cell.contentView addSubview:theSwitch];
+
+    return cell;
+}
+
+- (UITableViewCell *)buttonCellWithTitle:(NSString *)title touchUpAction:(SEL)action isDestructive:(BOOL)isDestructive
+{
+    UITableViewCell *buttonCell = [[UITableViewCell alloc] init];
+    buttonCell.selectionStyle = UITableViewCellSelectionStyleNone;
+
+    UIButton *actionButton = [UIButton buttonWithType:UIButtonTypeSystem];
+    [actionButton setTitle:title forState:UIControlStateNormal];
+    if (isDestructive) {
+        actionButton.tintColor = [UIColor redColor];
+    }
+    actionButton.titleLabel.font = [[self class] cellTitleFont];;
+    [actionButton addTarget:self action:@selector(clearRequestsTapped:) forControlEvents:UIControlEventTouchUpInside];
+
+    [buttonCell.contentView addSubview:actionButton];
+    actionButton.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
+    actionButton.frame = buttonCell.contentView.frame;
+    actionButton.contentEdgeInsets = UIEdgeInsetsMake(0.0, self.tableView.separatorInset.left, 0.0, self.tableView.separatorInset.left);
+    actionButton.contentHorizontalAlignment = UIControlContentHorizontalAlignmentLeft;
+
+    return buttonCell;
+}
+
+- (NSString *)titleForCacheLimitCellWithValue:(long long)cacheLimit
+{
+    NSInteger limitInMB = round(cacheLimit / (1024 * 1024));
+    return [NSString stringWithFormat:@"Cache Limit (%ld MB)", (long)limitInMB];
+}
+
+- (UITableViewCell *)sliderCellWithTitle:(NSString *)title changedAction:(SEL)changedAction minimum:(CGFloat)minimum maximum:(CGFloat)maximum initialValue:(CGFloat)initialValue
+{
+    UITableViewCell *sliderCell = [[UITableViewCell alloc] init];
+    sliderCell.selectionStyle = UITableViewCellSelectionStyleNone;
+    sliderCell.textLabel.text = title;
+    sliderCell.textLabel.font = [[self class] cellTitleFont];
+
+    UISlider *slider = [[UISlider alloc] init];
+    slider.minimumValue = minimum;
+    slider.maximumValue = maximum;
+    slider.value = initialValue;
+    [slider addTarget:self action:changedAction forControlEvents:UIControlEventValueChanged];
+    [slider sizeToFit];
+
+    CGFloat sliderWidth = round(sliderCell.contentView.frame.size.width * 2.0 / 5.0);
+    CGFloat sliderOriginY = round((sliderCell.contentView.frame.size.height - slider.frame.size.height) / 2.0);
+    CGFloat sliderOriginX = CGRectGetMaxX(sliderCell.contentView.frame) - sliderWidth - self.tableView.separatorInset.left;
+    slider.frame = CGRectMake(sliderOriginX, sliderOriginY, sliderWidth, slider.frame.size.height);
+    slider.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleLeftMargin | UIViewAutoresizingFlexibleTopMargin | UIViewAutoresizingFlexibleBottomMargin;
+    [sliderCell.contentView addSubview:slider];
+
+    return sliderCell;
+}
+
++ (UIFont *)cellTitleFont
+{
+    return [FLEXUtility defaultFontOfSize:14.0];
+}
+
+@end

+ 40 - 0
Classes/Network/FLEXNetworkTransaction.h

@@ -0,0 +1,40 @@
+//
+//  FLEXNetworkTransaction.h
+//  Flipboard
+//
+//  Created by Ryan Olson on 2/8/15.
+//  Copyright (c) 2015 Flipboard. All rights reserved.
+//
+
+#import <Foundation/Foundation.h>
+
+typedef NS_ENUM(NSInteger, FLEXNetworkTransactionState) {
+    FLEXNetworkTransactionStateUnstarted,
+    FLEXNetworkTransactionStateAwaitingResponse,
+    FLEXNetworkTransactionStateReceivingData,
+    FLEXNetworkTransactionStateFinished,
+    FLEXNetworkTransactionStateFailed
+};
+
+@interface FLEXNetworkTransaction : NSObject
+
+@property (nonatomic, copy) NSString *requestID;
+
+@property (nonatomic, strong) NSURLRequest *request;
+@property (nonatomic, strong) NSURLResponse *response;
+@property (nonatomic, copy) NSString *requestMechanism;
+@property (nonatomic, assign) FLEXNetworkTransactionState transactionState;
+@property (nonatomic, strong) NSError *error;
+
+@property (nonatomic, strong) NSDate *startTime;
+@property (nonatomic, assign) NSTimeInterval latency;
+@property (nonatomic, assign) NSTimeInterval duration;
+
+@property (nonatomic, assign) int64_t receivedDataLength;
+
+/// Only applicable for image downloads. A small thumbnail to preview the full response.
+@property (nonatomic, strong) UIImage *responseThumbnail;
+
++ (NSString *)readableStringFromTransactionState:(FLEXNetworkTransactionState)state;
+
+@end

+ 52 - 0
Classes/Network/FLEXNetworkTransaction.m

@@ -0,0 +1,52 @@
+//
+//  FLEXNetworkTransaction.m
+//  Flipboard
+//
+//  Created by Ryan Olson on 2/8/15.
+//  Copyright (c) 2015 Flipboard. All rights reserved.
+//
+
+#import "FLEXNetworkTransaction.h"
+
+@implementation FLEXNetworkTransaction
+
+- (NSString *)description
+{
+    NSString *description = [super description];
+
+    description = [description stringByAppendingFormat:@" id = %@;", self.requestID];
+    description = [description stringByAppendingFormat:@" url = %@;", self.request.URL];
+    description = [description stringByAppendingFormat:@" duration = %f;", self.duration];
+    description = [description stringByAppendingFormat:@" receivedDataLength = %lld", self.receivedDataLength];
+
+    return description;
+}
+
++ (NSString *)readableStringFromTransactionState:(FLEXNetworkTransactionState)state
+{
+    NSString *readableString = nil;
+    switch (state) {
+        case FLEXNetworkTransactionStateUnstarted:
+            readableString = @"Unstarted";
+            break;
+
+        case FLEXNetworkTransactionStateAwaitingResponse:
+            readableString = @"Awaiting Response";
+            break;
+
+        case FLEXNetworkTransactionStateReceivingData:
+            readableString = @"Receiving Data";
+            break;
+
+        case FLEXNetworkTransactionStateFinished:
+            readableString = @"Finished";
+            break;
+
+        case FLEXNetworkTransactionStateFailed:
+            readableString = @"Failed";
+            break;
+    }
+    return readableString;
+}
+
+@end

+ 17 - 0
Classes/Network/FLEXNetworkTransactionDetailTableViewController.h

@@ -0,0 +1,17 @@
+//
+//  FLEXNetworkTransactionDetailTableViewController.h
+//  Flipboard
+//
+//  Created by Ryan Olson on 2/10/15.
+//  Copyright (c) 2015 Flipboard. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+
+@class FLEXNetworkTransaction;
+
+@interface FLEXNetworkTransactionDetailTableViewController : UITableViewController
+
+@property (nonatomic, strong) FLEXNetworkTransaction *transaction;
+
+@end

+ 438 - 0
Classes/Network/FLEXNetworkTransactionDetailTableViewController.m

@@ -0,0 +1,438 @@
+//
+//  FLEXNetworkTransactionDetailTableViewController.m
+//  Flipboard
+//
+//  Created by Ryan Olson on 2/10/15.
+//  Copyright (c) 2015 Flipboard. All rights reserved.
+//
+
+#import "FLEXNetworkTransactionDetailTableViewController.h"
+#import "FLEXNetworkRecorder.h"
+#import "FLEXNetworkTransaction.h"
+#import "FLEXWebViewController.h"
+#import "FLEXImagePreviewViewController.h"
+#import "FLEXMultilineTableViewCell.h"
+#import "FLEXUtility.h"
+
+@interface FLEXNetworkDetailSection : NSObject
+
+@property (nonatomic, copy) NSString *title;
+@property (nonatomic, copy) NSArray *rows;
+
+@end
+
+@implementation FLEXNetworkDetailSection
+
+@end
+
+typedef UIViewController *(^FLEXNetworkDetailRowSelectionFuture)(void);
+
+@interface FLEXNetworkDetailRow : NSObject
+
+@property (nonatomic, copy) NSString *title;
+@property (nonatomic, copy) NSString *detailText;
+@property (nonatomic, copy) FLEXNetworkDetailRowSelectionFuture selectionFuture;
+
+@end
+
+@implementation FLEXNetworkDetailRow
+
+@end
+
+@interface FLEXNetworkTransactionDetailTableViewController ()
+
+@property (nonatomic, copy) NSArray *sections;
+
+@end
+
+@implementation FLEXNetworkTransactionDetailTableViewController
+
+- (instancetype)initWithStyle:(UITableViewStyle)style
+{
+    // Force grouped style
+    self = [super initWithStyle:UITableViewStyleGrouped];
+    if (self) {
+        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleTransactionUpdatedNotification:) name:kFLEXNetworkRecorderTransactionUpdatedNotification object:nil];
+    }
+    return self;
+}
+
+- (void)viewDidLoad
+{
+    [super viewDidLoad];
+
+    [self.tableView registerClass:[FLEXMultilineTableViewCell class] forCellReuseIdentifier:kFLEXMultilineTableViewCellIdentifier];
+}
+
+- (void)setTransaction:(FLEXNetworkTransaction *)transaction
+{
+    if (![_transaction isEqual:transaction]) {
+        _transaction = transaction;
+        self.title = [transaction.request.URL lastPathComponent];
+        [self rebuildTableSections];
+    }
+}
+
+- (void)setSections:(NSArray *)sections
+{
+    if (![_sections isEqual:sections]) {
+        _sections = [sections copy];
+        [self.tableView reloadData];
+    }
+}
+
+- (void)rebuildTableSections
+{
+    NSMutableArray *sections = [NSMutableArray array];
+
+    FLEXNetworkDetailSection *generalSection = [[self class] generalSectionForTransaction:self.transaction];
+    if ([generalSection.rows count] > 0) {
+        [sections addObject:generalSection];
+    }
+    FLEXNetworkDetailSection *requestHeadersSection = [[self class] requestHeadersSectionForTransaction:self.transaction];
+    if ([requestHeadersSection.rows count] > 0) {
+        [sections addObject:requestHeadersSection];
+    }
+    FLEXNetworkDetailSection *queryParametersSection = [[self class] queryParametersSectionForTransaction:self.transaction];
+    if ([queryParametersSection.rows count] > 0) {
+        [sections addObject:queryParametersSection];
+    }
+    FLEXNetworkDetailSection *postBodySection = [[self class] postBodySectionForTransaction:self.transaction];
+    if ([postBodySection.rows count] > 0) {
+        [sections addObject:postBodySection];
+    }
+    FLEXNetworkDetailSection *responseHeadersSection = [[self class] responseHeadersSectionForTransaction:self.transaction];
+    if ([responseHeadersSection.rows count] > 0) {
+        [sections addObject:responseHeadersSection];
+    }
+
+    self.sections = sections;
+}
+
+- (void)handleTransactionUpdatedNotification:(NSNotification *)notification
+{
+    FLEXNetworkTransaction *transaction = [[notification userInfo] objectForKey:kFLEXNetworkRecorderUserInfoTransactionKey];
+    if (transaction == self.transaction) {
+        [self rebuildTableSections];
+    }
+}
+
+#pragma mark - Table view data source
+
+- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
+{
+    return [self.sections count];
+}
+
+- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
+{
+    FLEXNetworkDetailSection *sectionModel = [self.sections objectAtIndex:section];
+    return [sectionModel.rows count];
+}
+
+- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
+{
+    FLEXNetworkDetailSection *sectionModel = [self.sections objectAtIndex:section];
+    return sectionModel.title;
+}
+
+- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
+{
+    FLEXMultilineTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kFLEXMultilineTableViewCellIdentifier forIndexPath:indexPath];
+
+    FLEXNetworkDetailRow *rowModel = [self rowModelAtIndexPath:indexPath];
+
+    cell.textLabel.attributedText = [[self class] attributedTextForRow:rowModel];
+    cell.accessoryType = rowModel.selectionFuture ? UITableViewCellAccessoryDisclosureIndicator : UITableViewCellAccessoryNone;
+    cell.selectionStyle = rowModel.selectionFuture ? UITableViewCellSelectionStyleDefault : UITableViewCellSelectionStyleNone;
+
+    return cell;
+}
+
+- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
+{
+    FLEXNetworkDetailRow *rowModel = [self rowModelAtIndexPath:indexPath];
+
+    UIViewController *viewControllerToPush = nil;
+    if (rowModel.selectionFuture) {
+        viewControllerToPush = rowModel.selectionFuture();
+    }
+
+    if (viewControllerToPush) {
+        [self.navigationController pushViewController:viewControllerToPush animated:YES];
+    }
+
+    [tableView deselectRowAtIndexPath:indexPath animated:YES];
+}
+
+- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
+{
+    FLEXNetworkDetailRow *row = [self rowModelAtIndexPath:indexPath];
+    NSAttributedString *attributedText = [[self class] attributedTextForRow:row];
+    BOOL showsAccessory = row.selectionFuture != nil;
+    return [FLEXMultilineTableViewCell preferredHeightWithAttributedText:attributedText inTableViewWidth:self.tableView.bounds.size.width style:UITableViewStyleGrouped showsAccessory:showsAccessory];
+}
+
+- (FLEXNetworkDetailRow *)rowModelAtIndexPath:(NSIndexPath *)indexPath
+{
+    FLEXNetworkDetailSection *sectionModel = [self.sections objectAtIndex:indexPath.section];
+    return [sectionModel.rows objectAtIndex:indexPath.row];
+}
+
+#pragma mark - View Configuration
+
++ (NSAttributedString *)attributedTextForRow:(FLEXNetworkDetailRow *)row
+{
+    NSDictionary *titleAttributes = @{ NSFontAttributeName : [UIFont fontWithName:@"HelveticaNeue-Medium" size:12.0],
+                                       NSForegroundColorAttributeName : [UIColor colorWithWhite:0.5 alpha:1.0] };
+    NSDictionary *detailAttributes = @{ NSFontAttributeName : [FLEXUtility defaultTableViewCellLabelFont],
+                                        NSForegroundColorAttributeName : [UIColor blackColor] };
+
+    NSString *title = [NSString stringWithFormat:@"%@: ", row.title];
+    NSString *detailText = row.detailText ?: @"";
+    NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] init];
+    [attributedText appendAttributedString:[[NSAttributedString alloc] initWithString:title attributes:titleAttributes]];
+    [attributedText appendAttributedString:[[NSAttributedString alloc] initWithString:detailText attributes:detailAttributes]];
+
+    return attributedText;
+}
+
+#pragma mark - Table Data Generation
+
++ (FLEXNetworkDetailSection *)generalSectionForTransaction:(FLEXNetworkTransaction *)transaction
+{
+    NSMutableArray *rows = [NSMutableArray array];
+
+    FLEXNetworkDetailRow *requestURLRow = [[FLEXNetworkDetailRow alloc] init];
+    requestURLRow.title = @"Request URL";
+    NSURL *url = transaction.request.URL;
+    requestURLRow.detailText = url.absoluteString;
+    requestURLRow.selectionFuture = ^{
+        UIViewController *urlWebViewController = [[FLEXWebViewController alloc] initWithURL:url];
+        urlWebViewController.title = url.absoluteString;
+        return urlWebViewController;
+    };
+    [rows addObject:requestURLRow];
+
+    FLEXNetworkDetailRow *requestMethodRow = [[FLEXNetworkDetailRow alloc] init];
+    requestMethodRow.title = @"Request Method";
+    requestMethodRow.detailText = transaction.request.HTTPMethod;
+    [rows addObject:requestMethodRow];
+
+    if ([transaction.request.HTTPBody length] > 0) {
+        FLEXNetworkDetailRow *postBodySizeRow = [[FLEXNetworkDetailRow alloc] init];
+        postBodySizeRow.title = @"Request Body Size";
+        postBodySizeRow.detailText = [NSByteCountFormatter stringFromByteCount:[transaction.request.HTTPBody length] countStyle:NSByteCountFormatterCountStyleBinary];
+        [rows addObject:postBodySizeRow];
+
+        FLEXNetworkDetailRow *postBodyRow = [[FLEXNetworkDetailRow alloc] init];
+        postBodyRow.title = @"Request Body";
+        postBodyRow.detailText = @"tap to view";
+        postBodyRow.selectionFuture = ^{
+            NSString *contentType = [transaction.request valueForHTTPHeaderField:@"Content-Type"];
+            UIViewController *detailViewController = [self detailViewControllerForMIMEType:contentType data:[self postBodyDataForTransaction:transaction]];
+            if (detailViewController) {
+                detailViewController.title = @"Request Body";
+            } else {
+                NSString *alertMessage = [NSString stringWithFormat:@"FLEX does not have a viewer for request body data with MIME type: %@", [transaction.request valueForHTTPHeaderField:@"Content-Type"]];
+                [[[UIAlertView alloc] initWithTitle:@"Can't View Body Data" message:alertMessage delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show];
+            }
+            return detailViewController;
+        };
+        [rows addObject:postBodyRow];
+    }
+
+    NSString *statusCodeString = [FLEXUtility statusCodeStringFromURLResponse:transaction.response];
+    if ([statusCodeString length] > 0) {
+        FLEXNetworkDetailRow *statusCodeRow = [[FLEXNetworkDetailRow alloc] init];
+        statusCodeRow.title = @"Status Code";
+        statusCodeRow.detailText = statusCodeString;
+        [rows addObject:statusCodeRow];
+    }
+
+    if (transaction.error) {
+        FLEXNetworkDetailRow *errorRow = [[FLEXNetworkDetailRow alloc] init];
+        errorRow.title = @"Error";
+        errorRow.detailText = transaction.error.localizedDescription;
+        [rows addObject:errorRow];
+    }
+
+    FLEXNetworkDetailRow *responseBodyRow = [[FLEXNetworkDetailRow alloc] init];
+    responseBodyRow.title = @"Response Body";
+    NSData *responseData = [[FLEXNetworkRecorder defaultRecorder] cachedResponseBodyForTransaction:transaction];
+    if ([responseData length] > 0) {
+        responseBodyRow.detailText = @"tap to view";
+        // Avoid a long lived strong reference to the response data in case we need to purge it from the cache.
+        __weak NSData *weakResponseData = responseData;
+        responseBodyRow.selectionFuture = ^{
+            UIViewController *responseBodyDetailViewController = nil;
+            NSData *strongResponseData = weakResponseData;
+            if (strongResponseData) {
+                responseBodyDetailViewController = [self detailViewControllerForMIMEType:transaction.response.MIMEType data:strongResponseData];
+                if (!responseBodyDetailViewController) {
+                    NSString *alertMessage = [NSString stringWithFormat:@"FLEX does not have a viewer for responses with MIME type: %@", transaction.response.MIMEType];
+                    [[[UIAlertView alloc] initWithTitle:@"Can't View Response" message:alertMessage delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show];
+                }
+                responseBodyDetailViewController.title = @"Response";
+            } else {
+                NSString *alertMessage = @"The response has been purged from the cache";
+                [[[UIAlertView alloc] initWithTitle:@"Can't View Response" message:alertMessage delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show];
+            }
+            return responseBodyDetailViewController;
+        };
+    } else {
+        BOOL emptyResponse = transaction.receivedDataLength == 0;
+        responseBodyRow.detailText = emptyResponse ? @"empty" : @"not in cache";
+    }
+    [rows addObject:responseBodyRow];
+
+    FLEXNetworkDetailRow *responseSizeRow = [[FLEXNetworkDetailRow alloc] init];
+    responseSizeRow.title = @"Response Size";
+    responseSizeRow.detailText = [NSByteCountFormatter stringFromByteCount:transaction.receivedDataLength countStyle:NSByteCountFormatterCountStyleBinary];
+    [rows addObject:responseSizeRow];
+
+    FLEXNetworkDetailRow *mimeTypeRow = [[FLEXNetworkDetailRow alloc] init];
+    mimeTypeRow.title = @"MIME Type";
+    mimeTypeRow.detailText = transaction.response.MIMEType;
+    [rows addObject:mimeTypeRow];
+
+    FLEXNetworkDetailRow *mechanismRow = [[FLEXNetworkDetailRow alloc] init];
+    mechanismRow.title = @"Mechanism";
+    mechanismRow.detailText = transaction.requestMechanism;
+    [rows addObject:mechanismRow];
+
+    NSDateFormatter *startTimeFormatter = [[NSDateFormatter alloc] init];
+    startTimeFormatter.dateFormat = @"yyyy-MM-dd HH:mm:ss.SSS";
+
+    FLEXNetworkDetailRow *localStartTimeRow = [[FLEXNetworkDetailRow alloc] init];
+    localStartTimeRow.title = [NSString stringWithFormat:@"Start Time (%@)", [[NSTimeZone localTimeZone] abbreviationForDate:transaction.startTime]];
+    localStartTimeRow.detailText = [startTimeFormatter stringFromDate:transaction.startTime];
+    [rows addObject:localStartTimeRow];
+
+    startTimeFormatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"];
+
+    FLEXNetworkDetailRow *utcStartTimeRow = [[FLEXNetworkDetailRow alloc] init];
+    utcStartTimeRow.title = @"Start Time (UTC)";
+    utcStartTimeRow.detailText = [startTimeFormatter stringFromDate:transaction.startTime];
+    [rows addObject:utcStartTimeRow];
+
+    FLEXNetworkDetailRow *unixStartTime = [[FLEXNetworkDetailRow alloc] init];
+    unixStartTime.title = @"Unix Start Time";
+    unixStartTime.detailText = [NSString stringWithFormat:@"%f", [transaction.startTime timeIntervalSince1970]];
+    [rows addObject:unixStartTime];
+
+    FLEXNetworkDetailRow *durationRow = [[FLEXNetworkDetailRow alloc] init];
+    durationRow.title = @"Total Duration";
+    durationRow.detailText = [FLEXUtility stringFromRequestDuration:transaction.duration];
+    [rows addObject:durationRow];
+
+    FLEXNetworkDetailRow *latencyRow = [[FLEXNetworkDetailRow alloc] init];
+    latencyRow.title = @"Latency";
+    latencyRow.detailText = [FLEXUtility stringFromRequestDuration:transaction.latency];
+    [rows addObject:latencyRow];
+
+    FLEXNetworkDetailSection *generalSection = [[FLEXNetworkDetailSection alloc] init];
+    generalSection.title = @"General";
+    generalSection.rows = rows;
+
+    return generalSection;
+}
+
++ (FLEXNetworkDetailSection *)requestHeadersSectionForTransaction:(FLEXNetworkTransaction *)transaction
+{
+    FLEXNetworkDetailSection *requestHeadersSection = [[FLEXNetworkDetailSection alloc] init];
+    requestHeadersSection.title = @"Request Headers";
+    requestHeadersSection.rows = [self networkDetailRowsFromDictionary:transaction.request.allHTTPHeaderFields];
+
+    return requestHeadersSection;
+}
+
++ (FLEXNetworkDetailSection *)postBodySectionForTransaction:(FLEXNetworkTransaction *)transaction
+{
+    FLEXNetworkDetailSection *postBodySection = [[FLEXNetworkDetailSection alloc] init];
+    postBodySection.title = @"Request Body Parameters";
+    if ([transaction.request.HTTPBody length] > 0) {
+        NSString *contentType = [transaction.request valueForHTTPHeaderField:@"Content-Type"];
+        if ([contentType hasPrefix:@"application/x-www-form-urlencoded"]) {
+            NSString *bodyString = [[NSString alloc] initWithData:[self postBodyDataForTransaction:transaction] encoding:NSUTF8StringEncoding];
+            postBodySection.rows = [self networkDetailRowsFromDictionary:[FLEXUtility dictionaryFromQuery:bodyString]];
+        }
+    }
+    return postBodySection;
+}
+
++ (FLEXNetworkDetailSection *)queryParametersSectionForTransaction:(FLEXNetworkTransaction *)transaction
+{
+    NSDictionary *queryDictionary = [FLEXUtility dictionaryFromQuery:transaction.request.URL.query];
+    FLEXNetworkDetailSection *querySection = [[FLEXNetworkDetailSection alloc] init];
+    querySection.title = @"Query Parameters";
+    querySection.rows = [self networkDetailRowsFromDictionary:queryDictionary];
+
+    return querySection;
+}
+
++ (FLEXNetworkDetailSection *)responseHeadersSectionForTransaction:(FLEXNetworkTransaction *)transaction
+{
+    FLEXNetworkDetailSection *responseHeadersSection = [[FLEXNetworkDetailSection alloc] init];
+    responseHeadersSection.title = @"Response Headers";
+    if ([transaction.response isKindOfClass:[NSHTTPURLResponse class]]) {
+        NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)transaction.response;
+        responseHeadersSection.rows = [self networkDetailRowsFromDictionary:httpResponse.allHeaderFields];
+    }
+    return responseHeadersSection;
+}
+
++ (NSArray *)networkDetailRowsFromDictionary:(NSDictionary *)dictionary
+{
+    NSMutableArray *rows = [NSMutableArray arrayWithCapacity:[dictionary count]];
+    NSArray *sortedKeys = [[dictionary allKeys] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)];
+    for (NSString *key in sortedKeys) {
+        NSString *value = [dictionary objectForKey:key];
+        FLEXNetworkDetailRow *row = [[FLEXNetworkDetailRow alloc] init];
+        row.title = key;
+        row.detailText = [value description];
+        [rows addObject:row];
+    }
+    return [rows copy];
+}
+
++ (UIViewController *)detailViewControllerForMIMEType:(NSString *)mimeType data:(NSData *)data
+{
+    // FIXME (RKO): Don't rely on UTF8 string encoding
+    UIViewController *detailViewController = nil;
+    if ([mimeType isEqual:@"application/json"] || [mimeType isEqual:@"application/javascript"] || [mimeType isEqual:@"text/javascript"]) {
+        NSString *prettyJSON = [FLEXUtility prettyJSONStringFromData:data];
+        if ([prettyJSON length] > 0) {
+            detailViewController = [[FLEXWebViewController alloc] initWithText:prettyJSON];
+        }
+    } else if ([mimeType hasPrefix:@"image/"]) {
+        UIImage *image = [UIImage imageWithData:data];
+        detailViewController = [[FLEXImagePreviewViewController alloc] initWithImage:image];
+    } else if ([mimeType isEqual:@"application/x-plist"]) {
+        id propertyList = [NSPropertyListSerialization propertyListWithData:data options:0 format:NULL error:NULL];
+        detailViewController = [[FLEXWebViewController alloc] initWithText:[propertyList description]];
+    }
+
+    // Fall back to trying to show the response as text
+    if (!detailViewController) {
+        NSString *text = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
+        if ([text length] > 0) {
+            detailViewController = [[FLEXWebViewController alloc] initWithText:text];
+        }
+    }
+    return detailViewController;
+}
+
++ (NSData *)postBodyDataForTransaction:(FLEXNetworkTransaction *)transaction
+{
+    NSData *bodyData = transaction.request.HTTPBody;
+    if ([bodyData length] > 0) {
+        NSString *contentEncoding = [transaction.request valueForHTTPHeaderField:@"Content-Encoding"];
+        if ([contentEncoding rangeOfString:@"deflate" options:NSCaseInsensitiveSearch].length > 0 || [contentEncoding rangeOfString:@"gzip" options:NSCaseInsensitiveSearch].length > 0) {
+            bodyData = [FLEXUtility inflatedDataFromCompressedData:bodyData];
+        }
+    }
+    return bodyData;
+}
+
+@end

+ 21 - 0
Classes/Network/FLEXNetworkTransactionTableViewCell.h

@@ -0,0 +1,21 @@
+//
+//  FLEXNetworkTransactionTableViewCell.h
+//  Flipboard
+//
+//  Created by Ryan Olson on 2/8/15.
+//  Copyright (c) 2015 Flipboard. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+
+extern NSString *const kFLEXNetworkTransactionCellIdentifier;
+
+@class FLEXNetworkTransaction;
+
+@interface FLEXNetworkTransactionTableViewCell : UITableViewCell
+
+@property (nonatomic, strong) FLEXNetworkTransaction *transaction;
+
++ (CGFloat)preferredCellHeight;
+
+@end

+ 178 - 0
Classes/Network/FLEXNetworkTransactionTableViewCell.m

@@ -0,0 +1,178 @@
+//
+//  FLEXNetworkTransactionTableViewCell.m
+//  Flipboard
+//
+//  Created by Ryan Olson on 2/8/15.
+//  Copyright (c) 2015 Flipboard. All rights reserved.
+//
+
+#import "FLEXNetworkTransactionTableViewCell.h"
+#import "FLEXNetworkTransaction.h"
+#import "FLEXUtility.h"
+#import "FLEXResources.h"
+
+NSString *const kFLEXNetworkTransactionCellIdentifier = @"kFLEXNetworkTransactionCellIdentifier";
+
+@interface FLEXNetworkTransactionTableViewCell ()
+
+@property (nonatomic, strong) UIImageView *thumbnailImageView;
+@property (nonatomic, strong) UILabel *nameLabel;
+@property (nonatomic, strong) UILabel *pathLabel;
+@property (nonatomic, strong) UILabel *transactionDetailsLabel;
+
+@end
+
+@implementation FLEXNetworkTransactionTableViewCell
+
+- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
+{
+    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
+    if (self) {
+        self.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
+
+        self.nameLabel = [[UILabel alloc] init];
+        self.nameLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
+        [self.contentView addSubview:self.nameLabel];
+
+        self.pathLabel = [[UILabel alloc] init];
+        self.pathLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
+        self.pathLabel.textColor = [UIColor colorWithWhite:0.4 alpha:1.0];
+        [self.contentView addSubview:self.pathLabel];
+
+        self.thumbnailImageView = [[UIImageView alloc] init];
+        self.thumbnailImageView.layer.borderColor = [[UIColor blackColor] CGColor];
+        self.thumbnailImageView.layer.borderWidth = 1.0;
+        self.thumbnailImageView.contentMode = UIViewContentModeScaleAspectFit;
+        [self.contentView addSubview:self.thumbnailImageView];
+
+        self.transactionDetailsLabel = [[UILabel alloc] init];
+        self.transactionDetailsLabel.font = [FLEXUtility defaultFontOfSize:10.0];
+        self.transactionDetailsLabel.textColor = [UIColor colorWithWhite:0.65 alpha:1.0];
+        [self.contentView addSubview:self.transactionDetailsLabel];
+    }
+    return self;
+}
+
+- (void)setTransaction:(FLEXNetworkTransaction *)transaction
+{
+    if (_transaction != transaction) {
+        _transaction = transaction;
+        [self setNeedsLayout];
+    }
+}
+
+- (void)layoutSubviews
+{
+    [super layoutSubviews];
+
+    const CGFloat kVerticalPadding = 8.0;
+    const CGFloat kLeftPadding = 10.0;
+    const CGFloat kImageDimension = 32.0;
+
+    CGFloat thumbnailOriginY = round((self.contentView.bounds.size.height - kImageDimension) / 2.0);
+    self.thumbnailImageView.frame = CGRectMake(kLeftPadding, thumbnailOriginY, kImageDimension, kImageDimension);
+    self.thumbnailImageView.image = self.transaction.responseThumbnail;
+
+    CGFloat textOriginX = CGRectGetMaxX(self.thumbnailImageView.frame) + kLeftPadding;
+    CGFloat availableTextWidth = self.contentView.bounds.size.width - textOriginX;
+
+    self.nameLabel.text = [self nameLabelText];
+    CGSize nameLabelPreferredSize = [self.nameLabel sizeThatFits:CGSizeMake(availableTextWidth, CGFLOAT_MAX)];
+    self.nameLabel.frame = CGRectMake(textOriginX, kVerticalPadding, availableTextWidth, nameLabelPreferredSize.height);
+    self.nameLabel.textColor = self.transaction.error ? [UIColor redColor] : [UIColor blackColor];
+
+    self.pathLabel.text = [self pathLabelText];
+    CGSize pathLabelPreferredSize = [self.pathLabel sizeThatFits:CGSizeMake(availableTextWidth, CGFLOAT_MAX)];
+    CGFloat pathLabelOriginY = ceil((self.contentView.bounds.size.height - pathLabelPreferredSize.height) / 2.0);
+    self.pathLabel.frame = CGRectMake(textOriginX, pathLabelOriginY, availableTextWidth, pathLabelPreferredSize.height);
+
+    self.transactionDetailsLabel.text = [self transactionDetailsLabelText];
+    CGSize transactionLabelPreferredSize = [self.transactionDetailsLabel sizeThatFits:CGSizeMake(availableTextWidth, CGFLOAT_MAX)];
+    CGFloat transactionDetailsOriginX = textOriginX;
+    CGFloat transactionDetailsLabelOriginY = CGRectGetMaxY(self.contentView.bounds) - kVerticalPadding - transactionLabelPreferredSize.height;
+    CGFloat transactionDetailsLabelWidth = self.contentView.bounds.size.width - transactionDetailsOriginX;
+    self.transactionDetailsLabel.frame = CGRectMake(transactionDetailsOriginX, transactionDetailsLabelOriginY, transactionDetailsLabelWidth, transactionLabelPreferredSize.height);
+}
+
+- (NSString *)nameLabelText
+{
+    NSURL *url = self.transaction.request.URL;
+    NSString *name = [url lastPathComponent];
+    if ([name length] == 0) {
+        name = @"/";
+    }
+    NSString *query = [url query];
+    if (query) {
+        name = [name stringByAppendingFormat:@"?%@", query];
+    }
+    return name;
+}
+
+- (NSString *)pathLabelText
+{
+    NSURL *url = self.transaction.request.URL;
+    NSMutableArray *mutablePathComponents = [[url pathComponents] mutableCopy];
+    if ([mutablePathComponents count] > 0) {
+        [mutablePathComponents removeLastObject];
+    }
+    NSString *path = [url host];
+    for (NSString *pathComponent in mutablePathComponents) {
+        path = [path stringByAppendingPathComponent:pathComponent];
+    }
+    return path;
+}
+
+- (NSString *)transactionDetailsLabelText
+{
+    NSMutableArray *detailComponents = [NSMutableArray array];
+
+    NSString *timestamp = [[self class] timestampStringFromRequestDate:self.transaction.startTime];
+    [detailComponents addObject:timestamp];
+
+    // Omit method for GET (assumed as default)
+    NSString *httpMethod = self.transaction.request.HTTPMethod;
+    if (httpMethod) {
+        [detailComponents addObject:httpMethod];
+    }
+
+    if (self.transaction.transactionState == FLEXNetworkTransactionStateFinished || self.transaction.transactionState == FLEXNetworkTransactionStateFailed) {
+        NSString *statusCodeString = [FLEXUtility statusCodeStringFromURLResponse:self.transaction.response];
+        if ([statusCodeString length] > 0) {
+            [detailComponents addObject:statusCodeString];
+        }
+
+        if (self.transaction.receivedDataLength > 0) {
+            NSString *responseSize = [NSByteCountFormatter stringFromByteCount:self.transaction.receivedDataLength countStyle:NSByteCountFormatterCountStyleBinary];
+            [detailComponents addObject:responseSize];
+        }
+
+        NSString *totalDuration = [FLEXUtility stringFromRequestDuration:self.transaction.duration];
+        NSString *latency = [FLEXUtility stringFromRequestDuration:self.transaction.latency];
+        NSString *duration = [NSString stringWithFormat:@"%@ (%@)", totalDuration, latency];
+        [detailComponents addObject:duration];
+    } else {
+        // Unstarted, Awaiting Response, Receiving Data, etc.
+        NSString *state = [FLEXNetworkTransaction readableStringFromTransactionState:self.transaction.transactionState];
+        [detailComponents addObject:state];
+    }
+
+    return [detailComponents componentsJoinedByString:@" ・ "];
+}
+
++ (NSString *)timestampStringFromRequestDate:(NSDate *)date
+{
+    static NSDateFormatter *dateFormatter = nil;
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        dateFormatter = [[NSDateFormatter alloc] init];
+        dateFormatter.dateFormat = @"HH:mm:ss";
+    });
+    return [dateFormatter stringFromDate:date];
+}
+
++ (CGFloat)preferredCellHeight
+{
+    return 65.0;
+}
+
+@end

+ 31 - 0
Classes/Network/PonyDebugger/FLEXNetworkObserver.h

@@ -0,0 +1,31 @@
+//
+//  FLEXNetworkObserver.h
+//  Derived from:
+//
+//  PDAFNetworkDomainController.h
+//  PonyDebugger
+//
+//  Created by Mike Lewis on 2/27/12.
+//
+//  Licensed to Square, Inc. under one or more contributor license agreements.
+//  See the LICENSE file distributed with this work for the terms under
+//  which Square, Inc. licenses this file to you.
+//
+
+extern NSString *const kFLEXNetworkObserverEnabledStateChangedNotification;
+
+/// This class swizzles NSURLConnection and NSURLSession delegate methods to observe events in the URL loading system.
+/// High level network events are sent to the default FLEXNetworkRecorder instance which maintains the request history and caches response bodies.
+@interface FLEXNetworkObserver : NSObject
+
+/// Swizzling occurs when the observer is enabled for the first time.
+/// This reduces the impact of FLEX if network debugging is not desired.
++ (void)setEnabled:(BOOL)enabled;
++ (BOOL)isEnabled;
+
+/// The enable on launch setting is persisted accross launches of the app.
+/// If YES, the observer will automatically enable itself early in the application lifecycle.
++ (void)setShouldEnableOnLaunch:(BOOL)shouldEnableOnLaunch;
++ (BOOL)shouldEnableOnLaunch;
+
+@end

File diff suppressed because it is too large
+ 1182 - 0
Classes/Network/PonyDebugger/FLEXNetworkObserver.m


+ 16 - 0
Classes/Network/PonyDebugger/LICENSE

@@ -0,0 +1,16 @@
+
+PonyDebugger
+Copyright 2012 Square Inc.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+   http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+

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

@@ -1,15 +0,0 @@
-//
-//  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

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

@@ -1,85 +0,0 @@
-//
-//  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);
-    
-    // Size an attributed string to get around deprecation warnings if the deployment target is >= 7 while still supporting deployment tagets back to 6.0.
-    NSAttributedString *attributedText = [[NSAttributedString alloc] initWithString:text attributes:@{NSFontAttributeName: [self labelFont]}];
-    CGSize constrainSize = CGSizeMake(labelWidth, CGFLOAT_MAX);
-    CGFloat preferredLabelHeight = ceil([attributedText boundingRectWithSize:constrainSize options:NSStringDrawingUsesLineFragmentOrigin context:nil].size.height);
-    CGFloat preferredCellHeight = preferredLabelHeight + labelInsets.top + labelInsets.bottom + 1.0;
-    
-    return preferredCellHeight;
-}
-
-@end

+ 6 - 4
Classes/Object Explorers/FLEXObjectExplorerViewController.m

@@ -9,7 +9,7 @@
 #import "FLEXObjectExplorerViewController.h"
 #import "FLEXObjectExplorerViewController.h"
 #import "FLEXUtility.h"
 #import "FLEXUtility.h"
 #import "FLEXRuntimeUtility.h"
 #import "FLEXRuntimeUtility.h"
-#import "FLEXDescriptionTableViewCell.h"
+#import "FLEXMultilineTableViewCell.h"
 #import "FLEXObjectExplorerFactory.h"
 #import "FLEXObjectExplorerFactory.h"
 #import "FLEXPropertyEditorViewController.h"
 #import "FLEXPropertyEditorViewController.h"
 #import "FLEXIvarEditorViewController.h"
 #import "FLEXIvarEditorViewController.h"
@@ -860,11 +860,12 @@ static const NSInteger kFLEXObjectExplorerScopeIncludeInheritanceIndex = 1;
     FLEXObjectExplorerSection explorerSection = [self explorerSectionAtIndex:indexPath.section];
     FLEXObjectExplorerSection explorerSection = [self explorerSectionAtIndex:indexPath.section];
     
     
     BOOL useDescriptionCell = explorerSection == FLEXObjectExplorerSectionDescription;
     BOOL useDescriptionCell = explorerSection == FLEXObjectExplorerSectionDescription;
-    NSString *cellIdentifier = useDescriptionCell ? @"descriptionCell" : @"cell";
+    NSString *cellIdentifier = useDescriptionCell ? kFLEXMultilineTableViewCellIdentifier : @"cell";
     UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
     UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
     if (!cell) {
     if (!cell) {
         if (useDescriptionCell) {
         if (useDescriptionCell) {
-            cell = [[FLEXDescriptionTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];
+            cell = [[FLEXMultilineTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];
+            cell.textLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
         } else {
         } else {
             cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:cellIdentifier];
             cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:cellIdentifier];
             UIFont *cellFont = [FLEXUtility defaultTableViewCellLabelFont];
             UIFont *cellFont = [FLEXUtility defaultTableViewCellLabelFont];
@@ -887,7 +888,8 @@ static const NSInteger kFLEXObjectExplorerScopeIncludeInheritanceIndex = 1;
     CGFloat height = self.tableView.rowHeight;
     CGFloat height = self.tableView.rowHeight;
     if (explorerSection == FLEXObjectExplorerSectionDescription) {
     if (explorerSection == FLEXObjectExplorerSectionDescription) {
         NSString *text = [self titleForRow:indexPath.row inExplorerSection:explorerSection];
         NSString *text = [self titleForRow:indexPath.row inExplorerSection:explorerSection];
-        CGFloat preferredHeight = [FLEXDescriptionTableViewCell preferredHeightWithText:text inTableViewWidth:self.tableView.frame.size.width];
+        NSAttributedString *attributedText = [[NSAttributedString alloc] initWithString:text attributes:@{ NSFontAttributeName : [FLEXUtility defaultTableViewCellLabelFont] }];
+        CGFloat preferredHeight = [FLEXMultilineTableViewCell preferredHeightWithAttributedText:attributedText inTableViewWidth:self.tableView.frame.size.width style:tableView.style showsAccessory:NO];
         height = MAX(height, preferredHeight);
         height = MAX(height, preferredHeight);
     }
     }
     return height;
     return height;

+ 1 - 6
Classes/Object Explorers/FLEXViewExplorerViewController.m

@@ -163,12 +163,7 @@ typedef NS_ENUM(NSUInteger, FLEXViewExplorerRow) {
     if (!CGRectIsEmpty(view.bounds)) {
     if (!CGRectIsEmpty(view.bounds)) {
         CGSize viewSize = view.bounds.size;
         CGSize viewSize = view.bounds.size;
         UIGraphicsBeginImageContextWithOptions(viewSize, NO, 0.0);
         UIGraphicsBeginImageContextWithOptions(viewSize, NO, 0.0);
-        if ([view respondsToSelector:@selector(drawViewHierarchyInRect:afterScreenUpdates:)]) {
-            [view drawViewHierarchyInRect:CGRectMake(0, 0, viewSize.width, viewSize.height) afterScreenUpdates:YES];
-        } else {
-            CGContextRef imageContext = UIGraphicsGetCurrentContext();
-            [view.layer renderInContext:imageContext];
-        }
+        [view drawViewHierarchyInRect:CGRectMake(0, 0, viewSize.width, viewSize.height) afterScreenUpdates:YES];
         UIImage *previewImage = UIGraphicsGetImageFromCurrentImageContext();
         UIImage *previewImage = UIGraphicsGetImageFromCurrentImageContext();
         UIGraphicsEndImageContext();
         UIGraphicsEndImageContext();
         imagePreviewViewController = [[FLEXImagePreviewViewController alloc] initWithImage:previewImage];
         imagePreviewViewController = [[FLEXImagePreviewViewController alloc] initWithImage:previewImage];

+ 17 - 0
Classes/Utility/FLEXMultilineTableViewCell.h

@@ -0,0 +1,17 @@
+//
+//  FLEXMultilineTableViewCell.h
+//  UICatalog
+//
+//  Created by Ryan Olson on 2/13/15.
+//  Copyright (c) 2015 f. All rights reserved.
+//
+
+#import <UIKit/UIKit.h>
+
+extern NSString *const kFLEXMultilineTableViewCellIdentifier;
+
+@interface FLEXMultilineTableViewCell : UITableViewCell
+
++ (CGFloat)preferredHeightWithAttributedText:(NSAttributedString *)attributedText inTableViewWidth:(CGFloat)tableViewWidth style:(UITableViewStyle)style showsAccessory:(BOOL)showsAccessory;
+
+@end

+ 55 - 0
Classes/Utility/FLEXMultilineTableViewCell.m

@@ -0,0 +1,55 @@
+//
+//  FLEXMultilineTableViewCell.m
+//  UICatalog
+//
+//  Created by Ryan Olson on 2/13/15.
+//  Copyright (c) 2015 f. All rights reserved.
+//
+
+#import "FLEXMultilineTableViewCell.h"
+
+NSString *const kFLEXMultilineTableViewCellIdentifier = @"kFLEXMultilineTableViewCellIdentifier";
+
+@implementation FLEXMultilineTableViewCell
+
+- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
+{
+    self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
+    if (self) {
+        self.textLabel.numberOfLines = 0;
+    }
+    return self;
+}
+
+- (void)layoutSubviews
+{
+    [super layoutSubviews];
+
+    self.textLabel.frame = UIEdgeInsetsInsetRect(self.contentView.bounds, [[self class] labelInsets]);
+}
+
++ (UIEdgeInsets)labelInsets
+{
+    return UIEdgeInsetsMake(10.0, 15.0, 10.0, 15.0);
+}
+
++ (CGFloat)preferredHeightWithAttributedText:(NSAttributedString *)attributedText inTableViewWidth:(CGFloat)tableViewWidth style:(UITableViewStyle)style showsAccessory:(BOOL)showsAccessory
+{
+    CGFloat labelWidth = tableViewWidth;
+
+    // Content view inset due to accessory view observed on iOS 8.1 iPhone 6.
+    if (showsAccessory) {
+        labelWidth -= 34.0;
+    }
+
+    UIEdgeInsets labelInsets = [self labelInsets];
+    labelWidth -= (labelInsets.left + labelInsets.right);
+
+    CGSize constrainSize = CGSizeMake(labelWidth, CGFLOAT_MAX);
+    CGFloat preferredLabelHeight = ceil([attributedText boundingRectWithSize:constrainSize options:NSStringDrawingUsesLineFragmentOrigin context:nil].size.height);
+    CGFloat preferredCellHeight = preferredLabelHeight + labelInsets.top + labelInsets.bottom + 1.0;
+
+    return preferredCellHeight;
+}
+
+@end

+ 11 - 0
Classes/Utility/FLEXResources.h

@@ -19,4 +19,15 @@
 + (UIImage *)moveIcon;
 + (UIImage *)moveIcon;
 + (UIImage *)selectIcon;
 + (UIImage *)selectIcon;
 
 
++ (UIImage *)jsonIcon;
++ (UIImage *)textPlainIcon;
++ (UIImage *)htmlIcon;
++ (UIImage *)audioIcon;
++ (UIImage *)jsIcon;
++ (UIImage *)plistIcon;
++ (UIImage *)textIcon;
++ (UIImage *)videoIcon;
++ (UIImage *)xmlIcon;
++ (UIImage *)binaryIcon;
+
 @end
 @end

File diff suppressed because it is too large
+ 73 - 0
Classes/Utility/FLEXResources.m


+ 6 - 0
Classes/Utility/FLEXUtility.h

@@ -30,5 +30,11 @@
 + (UIInterfaceOrientationMask)infoPlistSupportedInterfaceOrientationsMask;
 + (UIInterfaceOrientationMask)infoPlistSupportedInterfaceOrientationsMask;
 + (NSString *)searchBarPlaceholderText;
 + (NSString *)searchBarPlaceholderText;
 + (BOOL)isImagePathExtension:(NSString *)extension;
 + (BOOL)isImagePathExtension:(NSString *)extension;
++ (UIImage *)thumbnailedImageWithMaxPixelDimension:(NSInteger)dimension fromImageData:(NSData *)data;
++ (NSString *)stringFromRequestDuration:(NSTimeInterval)duration;
++ (NSString *)statusCodeStringFromURLResponse:(NSURLResponse *)response;
++ (NSDictionary *)dictionaryFromQuery:(NSString *)query;
++ (NSString *)prettyJSONStringFromData:(NSData *)data;
++ (NSData *)inflatedDataFromCompressedData:(NSData *)compressedData;
 
 
 @end
 @end

+ 137 - 0
Classes/Utility/FLEXUtility.m

@@ -8,6 +8,8 @@
 
 
 #import "FLEXUtility.h"
 #import "FLEXUtility.h"
 #import "FLEXResources.h"
 #import "FLEXResources.h"
+#import <ImageIO/ImageIO.h>
+#import <zlib.h>
 
 
 @implementation FLEXUtility
 @implementation FLEXUtility
 
 
@@ -183,4 +185,139 @@
     return [@[@"jpg", @"jpeg", @"png", @"gif", @"tiff", @"tif"] containsObject:extension];
     return [@[@"jpg", @"jpeg", @"png", @"gif", @"tiff", @"tif"] containsObject:extension];
 }
 }
 
 
++ (UIImage *)thumbnailedImageWithMaxPixelDimension:(NSInteger)dimension fromImageData:(NSData *)data
+{
+    UIImage *thumbnail = nil;
+    CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)data, 0);
+    if (imageSource) {
+        NSDictionary *options = @{ (__bridge id)kCGImageSourceCreateThumbnailWithTransform : @YES,
+                                   (__bridge id)kCGImageSourceCreateThumbnailFromImageAlways : @YES,
+                                   (__bridge id)kCGImageSourceThumbnailMaxPixelSize : @(dimension) };
+
+        CGImageRef scaledImageRef = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, (__bridge CFDictionaryRef)options);
+        if (scaledImageRef) {
+            thumbnail = [UIImage imageWithCGImage:scaledImageRef];
+            CFRelease(scaledImageRef);
+        }
+        CFRelease(imageSource);
+    }
+    return thumbnail;
+}
+
++ (NSString *)stringFromRequestDuration:(NSTimeInterval)duration
+{
+    NSString *string = @"0s";
+    if (duration > 0.0) {
+        if (duration < 1.0) {
+            string = [NSString stringWithFormat:@"%dms", (int)(duration * 1000)];
+        } else if (duration < 10.0) {
+            string = [NSString stringWithFormat:@"%.2fs", duration];
+        } else {
+            string = [NSString stringWithFormat:@"%.1fs", duration];
+        }
+    }
+    return string;
+}
+
++ (NSString *)statusCodeStringFromURLResponse:(NSURLResponse *)response
+{
+    NSString *httpResponseString = nil;
+    if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
+        NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
+        NSString *statusCodeDescription = nil;
+        if (httpResponse.statusCode == 200) {
+            // Prefer OK to the default "no error"
+            statusCodeDescription = @"OK";
+        } else {
+            statusCodeDescription = [NSHTTPURLResponse localizedStringForStatusCode:httpResponse.statusCode];
+        }
+        httpResponseString = [NSString stringWithFormat:@"%ld %@", (long)httpResponse.statusCode, statusCodeDescription];
+    }
+    return httpResponseString;
+}
+
++ (NSDictionary *)dictionaryFromQuery:(NSString *)query
+{
+    NSMutableDictionary *queryDictionary = [NSMutableDictionary dictionary];
+
+    // [a=1, b=2, c=3]
+    NSArray *queryComponents = [query componentsSeparatedByString:@"&"];
+    for (NSString *keyValueString in queryComponents) {
+        // [a, 1]
+        NSArray *components = [keyValueString componentsSeparatedByString:@"="];
+        if ([components count] == 2) {
+            NSString *key = [[components firstObject] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
+            id value = [[components lastObject] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
+
+            // Handle multiple entries under the same key as an array
+            id existingEntry = [queryDictionary objectForKey:key];
+            if (existingEntry) {
+                if ([existingEntry isKindOfClass:[NSArray class]]) {
+                    value = [existingEntry arrayByAddingObject:value];
+                } else {
+                    value = @[existingEntry, value];
+                }
+            }
+            
+            [queryDictionary setObject:value forKey:key];
+        }
+    }
+
+    return queryDictionary;
+}
+
++ (NSString *)prettyJSONStringFromData:(NSData *)data
+{
+    NSString *prettyString = nil;
+    
+    id jsonObject = [NSJSONSerialization JSONObjectWithData:data options:0 error:NULL];
+    if ([NSJSONSerialization isValidJSONObject:jsonObject]) {
+        prettyString = [[NSString alloc] initWithData:[NSJSONSerialization dataWithJSONObject:jsonObject options:NSJSONWritingPrettyPrinted error:NULL] encoding:NSUTF8StringEncoding];
+        // NSJSONSerialization escapes forward slashes. We want pretty json, so run through and unescape the slashes.
+        prettyString = [prettyString stringByReplacingOccurrencesOfString:@"\\/" withString:@"/"];
+    } else {
+        prettyString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
+    }
+
+    return prettyString;
+}
+
+// Thanks to the following links for help with this method
+// http://www.cocoanetics.com/2012/02/decompressing-files-into-memory/
+// https://github.com/nicklockwood/GZIP
++ (NSData *)inflatedDataFromCompressedData:(NSData *)compressedData
+{
+    NSData *inflatedData = nil;
+    NSUInteger compressedDataLength = [compressedData length];
+    if (compressedDataLength > 0) {
+        z_stream stream;
+        stream.zalloc = Z_NULL;
+        stream.zfree = Z_NULL;
+        stream.avail_in = (uInt)compressedDataLength;
+        stream.next_in = (void *)[compressedData bytes];
+        stream.total_out = 0;
+        stream.avail_out = 0;
+
+        NSMutableData *mutableData = [NSMutableData dataWithLength:compressedDataLength * 1.5];
+        if (inflateInit2(&stream, 15 + 32) == Z_OK) {
+            int status = Z_OK;
+            while (status == Z_OK) {
+                if (stream.total_out >= [mutableData length]) {
+                    mutableData.length += compressedDataLength / 2;
+                }
+                stream.next_out = (uint8_t *)[mutableData mutableBytes] + stream.total_out;
+                stream.avail_out = (uInt)([mutableData length] - stream.total_out);
+                status = inflate(&stream, Z_SYNC_FLUSH);
+            }
+            if (inflateEnd(&stream) == Z_OK) {
+                if (status == Z_STREAM_END) {
+                    mutableData.length = stream.total_out;
+                    inflatedData = [mutableData copy];
+                }
+            }
+        }
+    }
+    return inflatedData;
+}
+
 @end
 @end

+ 1 - 7
Classes/View Hierarchy/FLEXHierarchyTableViewCell.m

@@ -37,13 +37,7 @@
         
         
         self.textLabel.font = [UIFont fontWithName:@"HelveticaNeue-Medium" size:14.0];
         self.textLabel.font = [UIFont fontWithName:@"HelveticaNeue-Medium" size:14.0];
         self.detailTextLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
         self.detailTextLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
-        
-        if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_6_1) {
-            // Supported starting with iOS 7
-            self.accessoryType = UITableViewCellAccessoryDetailButton;
-        } else {
-            self.accessoryType = UITableViewCellAccessoryDetailDisclosureButton;
-        }
+        self.accessoryType = UITableViewCellAccessoryDetailButton;
     }
     }
     return self;
     return self;
 }
 }

+ 1 - 3
Classes/View Hierarchy/FLEXHierarchyTableViewController.m

@@ -56,9 +56,7 @@ static const NSInteger kFLEXHierarchyScopeFullHierarchyIndex = 1;
     self.tableView.rowHeight = 50.0;
     self.tableView.rowHeight = 50.0;
     self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
     self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
     // Separator inset clashes with persistent cell selection.
     // Separator inset clashes with persistent cell selection.
-    if ([self.tableView respondsToSelector:@selector(setSeparatorInset:)]) {
-        [self.tableView setSeparatorInset:UIEdgeInsetsZero];
-    }
+    [self.tableView setSeparatorInset:UIEdgeInsetsZero];
     
     
     self.searchBar = [[UISearchBar alloc] init];
     self.searchBar = [[UISearchBar alloc] init];
     self.searchBar.placeholder = [FLEXUtility searchBarPlaceholderText];
     self.searchBar.placeholder = [FLEXUtility searchBarPlaceholderText];

+ 75 - 8
Example/UICatalog.xcodeproj/project.pbxproj

@@ -45,11 +45,17 @@
 		53874F9918F36B1800510922 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53874F9718F36B1800510922 /* Localizable.strings */; };
 		53874F9918F36B1800510922 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 53874F9718F36B1800510922 /* Localizable.strings */; };
 		650855EB1A9007D5006109A1 /* FLEXArgumentInputDateView.m in Sources */ = {isa = PBXBuildFile; fileRef = 650855EA1A9007D5006109A1 /* FLEXArgumentInputDateView.m */; };
 		650855EB1A9007D5006109A1 /* FLEXArgumentInputDateView.m in Sources */ = {isa = PBXBuildFile; fileRef = 650855EA1A9007D5006109A1 /* FLEXArgumentInputDateView.m */; };
 		65F8DC6C1A8F11020076F87B /* FLEXFileBrowserFileOperationController.m in Sources */ = {isa = PBXBuildFile; fileRef = 65F8DC6B1A8F11020076F87B /* FLEXFileBrowserFileOperationController.m */; };
 		65F8DC6C1A8F11020076F87B /* FLEXFileBrowserFileOperationController.m in Sources */ = {isa = PBXBuildFile; fileRef = 65F8DC6B1A8F11020076F87B /* FLEXFileBrowserFileOperationController.m */; };
+		9421B88D1A8BBCB200BA3E46 /* FLEXNetworkHistoryTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 9421B8801A8BBCB200BA3E46 /* FLEXNetworkHistoryTableViewController.m */; };
+		9421B88E1A8BBCB200BA3E46 /* FLEXNetworkRecorder.m in Sources */ = {isa = PBXBuildFile; fileRef = 9421B8821A8BBCB200BA3E46 /* FLEXNetworkRecorder.m */; };
+		9421B88F1A8BBCB200BA3E46 /* FLEXNetworkTransaction.m in Sources */ = {isa = PBXBuildFile; fileRef = 9421B8841A8BBCB200BA3E46 /* FLEXNetworkTransaction.m */; };
+		9421B8901A8BBCB200BA3E46 /* FLEXNetworkTransactionDetailTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 9421B8861A8BBCB200BA3E46 /* FLEXNetworkTransactionDetailTableViewController.m */; };
+		9421B8911A8BBCB200BA3E46 /* FLEXNetworkTransactionTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 9421B8881A8BBCB200BA3E46 /* FLEXNetworkTransactionTableViewCell.m */; };
+		9421B8921A8BBCB200BA3E46 /* FLEXNetworkObserver.m in Sources */ = {isa = PBXBuildFile; fileRef = 9421B88B1A8BBCB200BA3E46 /* FLEXNetworkObserver.m */; };
+		9421B8931A8BBCB200BA3E46 /* LICENSE in Resources */ = {isa = PBXBuildFile; fileRef = 9421B88C1A8BBCB200BA3E46 /* LICENSE */; };
 		943203FE1978F42F00E24DB3 /* AAPLCatalogTableTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 943203FD1978F42F00E24DB3 /* AAPLCatalogTableTableViewController.m */; };
 		943203FE1978F42F00E24DB3 /* AAPLCatalogTableTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 943203FD1978F42F00E24DB3 /* AAPLCatalogTableTableViewController.m */; };
 		944F7489197B458C009AB039 /* FLEXArrayExplorerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 944F7426197B458C009AB039 /* FLEXArrayExplorerViewController.m */; };
 		944F7489197B458C009AB039 /* FLEXArrayExplorerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 944F7426197B458C009AB039 /* FLEXArrayExplorerViewController.m */; };
 		944F748A197B458C009AB039 /* FLEXClassExplorerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 944F7428197B458C009AB039 /* FLEXClassExplorerViewController.m */; };
 		944F748A197B458C009AB039 /* FLEXClassExplorerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 944F7428197B458C009AB039 /* FLEXClassExplorerViewController.m */; };
 		944F748B197B458C009AB039 /* FLEXDefaultsExplorerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 944F742A197B458C009AB039 /* FLEXDefaultsExplorerViewController.m */; };
 		944F748B197B458C009AB039 /* FLEXDefaultsExplorerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 944F742A197B458C009AB039 /* FLEXDefaultsExplorerViewController.m */; };
-		944F748C197B458C009AB039 /* FLEXDescriptionTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 944F742C197B458C009AB039 /* FLEXDescriptionTableViewCell.m */; };
 		944F748D197B458C009AB039 /* FLEXDictionaryExplorerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 944F742E197B458C009AB039 /* FLEXDictionaryExplorerViewController.m */; };
 		944F748D197B458C009AB039 /* FLEXDictionaryExplorerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 944F742E197B458C009AB039 /* FLEXDictionaryExplorerViewController.m */; };
 		944F748E197B458C009AB039 /* FLEXImageExplorerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 944F7430197B458C009AB039 /* FLEXImageExplorerViewController.m */; };
 		944F748E197B458C009AB039 /* FLEXImageExplorerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 944F7430197B458C009AB039 /* FLEXImageExplorerViewController.m */; };
 		944F748F197B458C009AB039 /* FLEXObjectExplorerFactory.m in Sources */ = {isa = PBXBuildFile; fileRef = 944F7432197B458C009AB039 /* FLEXObjectExplorerFactory.m */; };
 		944F748F197B458C009AB039 /* FLEXObjectExplorerFactory.m in Sources */ = {isa = PBXBuildFile; fileRef = 944F7432197B458C009AB039 /* FLEXObjectExplorerFactory.m */; };
@@ -97,6 +103,9 @@
 		946C6ECC1A759928006545C2 /* FLEXSystemLogTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 946C6ECA1A759928006545C2 /* FLEXSystemLogTableViewController.m */; };
 		946C6ECC1A759928006545C2 /* FLEXSystemLogTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 946C6ECA1A759928006545C2 /* FLEXSystemLogTableViewController.m */; };
 		946C6ECF1A7599C4006545C2 /* FLEXSystemLogMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = 946C6ECE1A7599C4006545C2 /* FLEXSystemLogMessage.m */; };
 		946C6ECF1A7599C4006545C2 /* FLEXSystemLogMessage.m in Sources */ = {isa = PBXBuildFile; fileRef = 946C6ECE1A7599C4006545C2 /* FLEXSystemLogMessage.m */; };
 		94C681F31A3E941800E1936D /* FLEXLayerExplorerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 94C681F21A3E941800E1936D /* FLEXLayerExplorerViewController.m */; };
 		94C681F31A3E941800E1936D /* FLEXLayerExplorerViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 94C681F21A3E941800E1936D /* FLEXLayerExplorerViewController.m */; };
+		94CB48391A8EC6000054A905 /* FLEXMultilineTableViewCell.m in Sources */ = {isa = PBXBuildFile; fileRef = 94CB48381A8EC6000054A905 /* FLEXMultilineTableViewCell.m */; };
+		94CB4D431A97183E0054A905 /* libz.dylib in Frameworks */ = {isa = PBXBuildFile; fileRef = 94CB4D421A97183E0054A905 /* libz.dylib */; };
+		94CB4D4F1A9829F80054A905 /* FLEXNetworkSettingsTableViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 94CB4D4E1A9829F80054A905 /* FLEXNetworkSettingsTableViewController.m */; };
 		D03647D919847248007D2A1B /* FLEXGlobalsTableViewControllerEntry.m in Sources */ = {isa = PBXBuildFile; fileRef = D03647D819847248007D2A1B /* FLEXGlobalsTableViewControllerEntry.m */; };
 		D03647D919847248007D2A1B /* FLEXGlobalsTableViewControllerEntry.m in Sources */ = {isa = PBXBuildFile; fileRef = D03647D819847248007D2A1B /* FLEXGlobalsTableViewControllerEntry.m */; };
 /* End PBXBuildFile section */
 /* End PBXBuildFile section */
 
 
@@ -171,6 +180,19 @@
 		650855EA1A9007D5006109A1 /* FLEXArgumentInputDateView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXArgumentInputDateView.m; sourceTree = "<group>"; };
 		650855EA1A9007D5006109A1 /* FLEXArgumentInputDateView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXArgumentInputDateView.m; sourceTree = "<group>"; };
 		65F8DC6A1A8F11020076F87B /* FLEXFileBrowserFileOperationController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXFileBrowserFileOperationController.h; sourceTree = "<group>"; };
 		65F8DC6A1A8F11020076F87B /* FLEXFileBrowserFileOperationController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXFileBrowserFileOperationController.h; sourceTree = "<group>"; };
 		65F8DC6B1A8F11020076F87B /* FLEXFileBrowserFileOperationController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXFileBrowserFileOperationController.m; sourceTree = "<group>"; };
 		65F8DC6B1A8F11020076F87B /* FLEXFileBrowserFileOperationController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXFileBrowserFileOperationController.m; sourceTree = "<group>"; };
+		9421B87F1A8BBCB200BA3E46 /* FLEXNetworkHistoryTableViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXNetworkHistoryTableViewController.h; sourceTree = "<group>"; };
+		9421B8801A8BBCB200BA3E46 /* FLEXNetworkHistoryTableViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXNetworkHistoryTableViewController.m; sourceTree = "<group>"; };
+		9421B8811A8BBCB200BA3E46 /* FLEXNetworkRecorder.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXNetworkRecorder.h; sourceTree = "<group>"; };
+		9421B8821A8BBCB200BA3E46 /* FLEXNetworkRecorder.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXNetworkRecorder.m; sourceTree = "<group>"; };
+		9421B8831A8BBCB200BA3E46 /* FLEXNetworkTransaction.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXNetworkTransaction.h; sourceTree = "<group>"; };
+		9421B8841A8BBCB200BA3E46 /* FLEXNetworkTransaction.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXNetworkTransaction.m; sourceTree = "<group>"; };
+		9421B8851A8BBCB200BA3E46 /* FLEXNetworkTransactionDetailTableViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXNetworkTransactionDetailTableViewController.h; sourceTree = "<group>"; };
+		9421B8861A8BBCB200BA3E46 /* FLEXNetworkTransactionDetailTableViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXNetworkTransactionDetailTableViewController.m; sourceTree = "<group>"; };
+		9421B8871A8BBCB200BA3E46 /* FLEXNetworkTransactionTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXNetworkTransactionTableViewCell.h; sourceTree = "<group>"; };
+		9421B8881A8BBCB200BA3E46 /* FLEXNetworkTransactionTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXNetworkTransactionTableViewCell.m; sourceTree = "<group>"; };
+		9421B88A1A8BBCB200BA3E46 /* FLEXNetworkObserver.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXNetworkObserver.h; sourceTree = "<group>"; };
+		9421B88B1A8BBCB200BA3E46 /* FLEXNetworkObserver.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXNetworkObserver.m; sourceTree = "<group>"; };
+		9421B88C1A8BBCB200BA3E46 /* LICENSE */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE; sourceTree = "<group>"; };
 		943203FC1978F42F00E24DB3 /* AAPLCatalogTableTableViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AAPLCatalogTableTableViewController.h; sourceTree = "<group>"; };
 		943203FC1978F42F00E24DB3 /* AAPLCatalogTableTableViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AAPLCatalogTableTableViewController.h; sourceTree = "<group>"; };
 		943203FD1978F42F00E24DB3 /* AAPLCatalogTableTableViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AAPLCatalogTableTableViewController.m; sourceTree = "<group>"; };
 		943203FD1978F42F00E24DB3 /* AAPLCatalogTableTableViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AAPLCatalogTableTableViewController.m; sourceTree = "<group>"; };
 		944F7425197B458C009AB039 /* FLEXArrayExplorerViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXArrayExplorerViewController.h; sourceTree = "<group>"; };
 		944F7425197B458C009AB039 /* FLEXArrayExplorerViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXArrayExplorerViewController.h; sourceTree = "<group>"; };
@@ -179,8 +201,6 @@
 		944F7428197B458C009AB039 /* FLEXClassExplorerViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXClassExplorerViewController.m; sourceTree = "<group>"; };
 		944F7428197B458C009AB039 /* FLEXClassExplorerViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXClassExplorerViewController.m; sourceTree = "<group>"; };
 		944F7429197B458C009AB039 /* FLEXDefaultsExplorerViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXDefaultsExplorerViewController.h; sourceTree = "<group>"; };
 		944F7429197B458C009AB039 /* FLEXDefaultsExplorerViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXDefaultsExplorerViewController.h; sourceTree = "<group>"; };
 		944F742A197B458C009AB039 /* FLEXDefaultsExplorerViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXDefaultsExplorerViewController.m; sourceTree = "<group>"; };
 		944F742A197B458C009AB039 /* FLEXDefaultsExplorerViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXDefaultsExplorerViewController.m; sourceTree = "<group>"; };
-		944F742B197B458C009AB039 /* FLEXDescriptionTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXDescriptionTableViewCell.h; sourceTree = "<group>"; };
-		944F742C197B458C009AB039 /* FLEXDescriptionTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXDescriptionTableViewCell.m; sourceTree = "<group>"; };
 		944F742D197B458C009AB039 /* FLEXDictionaryExplorerViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXDictionaryExplorerViewController.h; sourceTree = "<group>"; };
 		944F742D197B458C009AB039 /* FLEXDictionaryExplorerViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXDictionaryExplorerViewController.h; sourceTree = "<group>"; };
 		944F742E197B458C009AB039 /* FLEXDictionaryExplorerViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXDictionaryExplorerViewController.m; sourceTree = "<group>"; };
 		944F742E197B458C009AB039 /* FLEXDictionaryExplorerViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXDictionaryExplorerViewController.m; sourceTree = "<group>"; };
 		944F742F197B458C009AB039 /* FLEXImageExplorerViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXImageExplorerViewController.h; sourceTree = "<group>"; };
 		944F742F197B458C009AB039 /* FLEXImageExplorerViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXImageExplorerViewController.h; sourceTree = "<group>"; };
@@ -275,6 +295,11 @@
 		946C6ECE1A7599C4006545C2 /* FLEXSystemLogMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FLEXSystemLogMessage.m; path = "System Log/FLEXSystemLogMessage.m"; sourceTree = "<group>"; };
 		946C6ECE1A7599C4006545C2 /* FLEXSystemLogMessage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = FLEXSystemLogMessage.m; path = "System Log/FLEXSystemLogMessage.m"; sourceTree = "<group>"; };
 		94C681F11A3E941800E1936D /* FLEXLayerExplorerViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXLayerExplorerViewController.h; sourceTree = "<group>"; };
 		94C681F11A3E941800E1936D /* FLEXLayerExplorerViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXLayerExplorerViewController.h; sourceTree = "<group>"; };
 		94C681F21A3E941800E1936D /* FLEXLayerExplorerViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXLayerExplorerViewController.m; sourceTree = "<group>"; };
 		94C681F21A3E941800E1936D /* FLEXLayerExplorerViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXLayerExplorerViewController.m; sourceTree = "<group>"; };
+		94CB48371A8EC6000054A905 /* FLEXMultilineTableViewCell.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXMultilineTableViewCell.h; sourceTree = "<group>"; };
+		94CB48381A8EC6000054A905 /* FLEXMultilineTableViewCell.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXMultilineTableViewCell.m; sourceTree = "<group>"; };
+		94CB4D421A97183E0054A905 /* libz.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; name = libz.dylib; path = usr/lib/libz.dylib; sourceTree = SDKROOT; };
+		94CB4D4D1A9829F80054A905 /* FLEXNetworkSettingsTableViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXNetworkSettingsTableViewController.h; sourceTree = "<group>"; };
+		94CB4D4E1A9829F80054A905 /* FLEXNetworkSettingsTableViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXNetworkSettingsTableViewController.m; sourceTree = "<group>"; };
 		D03647D51984720F007D2A1B /* FLEXManager+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "FLEXManager+Private.h"; sourceTree = "<group>"; };
 		D03647D51984720F007D2A1B /* FLEXManager+Private.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "FLEXManager+Private.h"; sourceTree = "<group>"; };
 		D03647D719847248007D2A1B /* FLEXGlobalsTableViewControllerEntry.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXGlobalsTableViewControllerEntry.h; sourceTree = "<group>"; };
 		D03647D719847248007D2A1B /* FLEXGlobalsTableViewControllerEntry.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FLEXGlobalsTableViewControllerEntry.h; sourceTree = "<group>"; };
 		D03647D819847248007D2A1B /* FLEXGlobalsTableViewControllerEntry.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXGlobalsTableViewControllerEntry.m; sourceTree = "<group>"; };
 		D03647D819847248007D2A1B /* FLEXGlobalsTableViewControllerEntry.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FLEXGlobalsTableViewControllerEntry.m; sourceTree = "<group>"; };
@@ -285,6 +310,7 @@
 			isa = PBXFrameworksBuildPhase;
 			isa = PBXFrameworksBuildPhase;
 			buildActionMask = 2147483647;
 			buildActionMask = 2147483647;
 			files = (
 			files = (
+				94CB4D431A97183E0054A905 /* libz.dylib in Frameworks */,
 				5356824018F3656900BAAD62 /* CoreGraphics.framework in Frameworks */,
 				5356824018F3656900BAAD62 /* CoreGraphics.framework in Frameworks */,
 				5356824218F3656900BAAD62 /* UIKit.framework in Frameworks */,
 				5356824218F3656900BAAD62 /* UIKit.framework in Frameworks */,
 				5356823E18F3656900BAAD62 /* Foundation.framework in Frameworks */,
 				5356823E18F3656900BAAD62 /* Foundation.framework in Frameworks */,
@@ -316,6 +342,7 @@
 		5356823C18F3656900BAAD62 /* Frameworks */ = {
 		5356823C18F3656900BAAD62 /* Frameworks */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
+				94CB4D421A97183E0054A905 /* libz.dylib */,
 				5356823D18F3656900BAAD62 /* Foundation.framework */,
 				5356823D18F3656900BAAD62 /* Foundation.framework */,
 				5356823F18F3656900BAAD62 /* CoreGraphics.framework */,
 				5356823F18F3656900BAAD62 /* CoreGraphics.framework */,
 				5356824118F3656900BAAD62 /* UIKit.framework */,
 				5356824118F3656900BAAD62 /* UIKit.framework */,
@@ -420,6 +447,37 @@
 			name = Application;
 			name = Application;
 			sourceTree = "<group>";
 			sourceTree = "<group>";
 		};
 		};
+		9421B87E1A8BBCB200BA3E46 /* Network */ = {
+			isa = PBXGroup;
+			children = (
+				9421B87F1A8BBCB200BA3E46 /* FLEXNetworkHistoryTableViewController.h */,
+				9421B8801A8BBCB200BA3E46 /* FLEXNetworkHistoryTableViewController.m */,
+				94CB4D4D1A9829F80054A905 /* FLEXNetworkSettingsTableViewController.h */,
+				94CB4D4E1A9829F80054A905 /* FLEXNetworkSettingsTableViewController.m */,
+				9421B8811A8BBCB200BA3E46 /* FLEXNetworkRecorder.h */,
+				9421B8821A8BBCB200BA3E46 /* FLEXNetworkRecorder.m */,
+				9421B8831A8BBCB200BA3E46 /* FLEXNetworkTransaction.h */,
+				9421B8841A8BBCB200BA3E46 /* FLEXNetworkTransaction.m */,
+				9421B8851A8BBCB200BA3E46 /* FLEXNetworkTransactionDetailTableViewController.h */,
+				9421B8861A8BBCB200BA3E46 /* FLEXNetworkTransactionDetailTableViewController.m */,
+				9421B8871A8BBCB200BA3E46 /* FLEXNetworkTransactionTableViewCell.h */,
+				9421B8881A8BBCB200BA3E46 /* FLEXNetworkTransactionTableViewCell.m */,
+				9421B8891A8BBCB200BA3E46 /* PonyDebugger */,
+			);
+			name = Network;
+			path = ../Classes/Network;
+			sourceTree = "<group>";
+		};
+		9421B8891A8BBCB200BA3E46 /* PonyDebugger */ = {
+			isa = PBXGroup;
+			children = (
+				9421B88A1A8BBCB200BA3E46 /* FLEXNetworkObserver.h */,
+				9421B88B1A8BBCB200BA3E46 /* FLEXNetworkObserver.m */,
+				9421B88C1A8BBCB200BA3E46 /* LICENSE */,
+			);
+			path = PonyDebugger;
+			sourceTree = "<group>";
+		};
 		943203FF1978F4B700E24DB3 /* 3rd Party */ = {
 		943203FF1978F4B700E24DB3 /* 3rd Party */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
@@ -431,6 +489,7 @@
 		943204001978F4C100E24DB3 /* FLEX */ = {
 		943204001978F4C100E24DB3 /* FLEX */ = {
 			isa = PBXGroup;
 			isa = PBXGroup;
 			children = (
 			children = (
+				9421B87E1A8BBCB200BA3E46 /* Network */,
 				944F7424197B458C009AB039 /* Object Explorers */,
 				944F7424197B458C009AB039 /* Object Explorers */,
 				944F743B197B458C009AB039 /* Utility */,
 				944F743B197B458C009AB039 /* Utility */,
 				944F7444197B458C009AB039 /* Editing */,
 				944F7444197B458C009AB039 /* Editing */,
@@ -450,8 +509,6 @@
 				944F7428197B458C009AB039 /* FLEXClassExplorerViewController.m */,
 				944F7428197B458C009AB039 /* FLEXClassExplorerViewController.m */,
 				944F7429197B458C009AB039 /* FLEXDefaultsExplorerViewController.h */,
 				944F7429197B458C009AB039 /* FLEXDefaultsExplorerViewController.h */,
 				944F742A197B458C009AB039 /* FLEXDefaultsExplorerViewController.m */,
 				944F742A197B458C009AB039 /* FLEXDefaultsExplorerViewController.m */,
-				944F742B197B458C009AB039 /* FLEXDescriptionTableViewCell.h */,
-				944F742C197B458C009AB039 /* FLEXDescriptionTableViewCell.m */,
 				944F742D197B458C009AB039 /* FLEXDictionaryExplorerViewController.h */,
 				944F742D197B458C009AB039 /* FLEXDictionaryExplorerViewController.h */,
 				944F742E197B458C009AB039 /* FLEXDictionaryExplorerViewController.m */,
 				944F742E197B458C009AB039 /* FLEXDictionaryExplorerViewController.m */,
 				944F742F197B458C009AB039 /* FLEXImageExplorerViewController.h */,
 				944F742F197B458C009AB039 /* FLEXImageExplorerViewController.h */,
@@ -485,6 +542,8 @@
 				944F7441197B458C009AB039 /* FLEXRuntimeUtility.m */,
 				944F7441197B458C009AB039 /* FLEXRuntimeUtility.m */,
 				944F7442197B458C009AB039 /* FLEXUtility.h */,
 				944F7442197B458C009AB039 /* FLEXUtility.h */,
 				944F7443197B458C009AB039 /* FLEXUtility.m */,
 				944F7443197B458C009AB039 /* FLEXUtility.m */,
+				94CB48371A8EC6000054A905 /* FLEXMultilineTableViewCell.h */,
+				94CB48381A8EC6000054A905 /* FLEXMultilineTableViewCell.m */,
 			);
 			);
 			name = Utility;
 			name = Utility;
 			path = ../Classes/Utility;
 			path = ../Classes/Utility;
@@ -687,6 +746,7 @@
 			isa = PBXResourcesBuildPhase;
 			isa = PBXResourcesBuildPhase;
 			buildActionMask = 2147483647;
 			buildActionMask = 2147483647;
 			files = (
 			files = (
+				9421B8931A8BBCB200BA3E46 /* LICENSE in Resources */,
 				5356825418F3656900BAAD62 /* Main_iPad.storyboard in Resources */,
 				5356825418F3656900BAAD62 /* Main_iPad.storyboard in Resources */,
 				53874F9918F36B1800510922 /* Localizable.strings in Resources */,
 				53874F9918F36B1800510922 /* Localizable.strings in Resources */,
 				5356825918F3656900BAAD62 /* Images.xcassets in Resources */,
 				5356825918F3656900BAAD62 /* Images.xcassets in Resources */,
@@ -714,8 +774,11 @@
 				944F74B1197B458C009AB039 /* FLEXInstancesTableViewController.m in Sources */,
 				944F74B1197B458C009AB039 /* FLEXInstancesTableViewController.m in Sources */,
 				944F7497197B458C009AB039 /* FLEXUtility.m in Sources */,
 				944F7497197B458C009AB039 /* FLEXUtility.m in Sources */,
 				535682B018F3670300BAAD62 /* AAPLDefaultToolbarViewController.m in Sources */,
 				535682B018F3670300BAAD62 /* AAPLDefaultToolbarViewController.m in Sources */,
+				94CB48391A8EC6000054A905 /* FLEXMultilineTableViewCell.m in Sources */,
 				535682BD18F3670300BAAD62 /* AAPLTintedToolbarViewController.m in Sources */,
 				535682BD18F3670300BAAD62 /* AAPLTintedToolbarViewController.m in Sources */,
 				943203FE1978F42F00E24DB3 /* AAPLCatalogTableTableViewController.m in Sources */,
 				943203FE1978F42F00E24DB3 /* AAPLCatalogTableTableViewController.m in Sources */,
+				9421B8921A8BBCB200BA3E46 /* FLEXNetworkObserver.m in Sources */,
+				9421B88F1A8BBCB200BA3E46 /* FLEXNetworkTransaction.m in Sources */,
 				944F748A197B458C009AB039 /* FLEXClassExplorerViewController.m in Sources */,
 				944F748A197B458C009AB039 /* FLEXClassExplorerViewController.m in Sources */,
 				535682B318F3670300BAAD62 /* AAPLPageControlViewController.m in Sources */,
 				535682B318F3670300BAAD62 /* AAPLPageControlViewController.m in Sources */,
 				535682BA18F3670300BAAD62 /* AAPLSwitchViewController.m in Sources */,
 				535682BA18F3670300BAAD62 /* AAPLSwitchViewController.m in Sources */,
@@ -728,11 +791,13 @@
 				944F74B0197B458C009AB039 /* FLEXGlobalsTableViewController.m in Sources */,
 				944F74B0197B458C009AB039 /* FLEXGlobalsTableViewController.m in Sources */,
 				944F748B197B458C009AB039 /* FLEXDefaultsExplorerViewController.m in Sources */,
 				944F748B197B458C009AB039 /* FLEXDefaultsExplorerViewController.m in Sources */,
 				944F749E197B458C009AB039 /* FLEXArgumentInputStructView.m in Sources */,
 				944F749E197B458C009AB039 /* FLEXArgumentInputStructView.m in Sources */,
+				94CB4D4F1A9829F80054A905 /* FLEXNetworkSettingsTableViewController.m in Sources */,
 				944F74B3197B458C009AB039 /* FLEXLiveObjectsTableViewController.m in Sources */,
 				944F74B3197B458C009AB039 /* FLEXLiveObjectsTableViewController.m in Sources */,
 				944F748E197B458C009AB039 /* FLEXImageExplorerViewController.m in Sources */,
 				944F748E197B458C009AB039 /* FLEXImageExplorerViewController.m in Sources */,
 				944F74B6197B458C009AB039 /* FLEXHierarchyTableViewController.m in Sources */,
 				944F74B6197B458C009AB039 /* FLEXHierarchyTableViewController.m in Sources */,
 				65F8DC6C1A8F11020076F87B /* FLEXFileBrowserFileOperationController.m in Sources */,
 				65F8DC6C1A8F11020076F87B /* FLEXFileBrowserFileOperationController.m in Sources */,
 				535682BC18F3670300BAAD62 /* AAPLTextViewController.m in Sources */,
 				535682BC18F3670300BAAD62 /* AAPLTextViewController.m in Sources */,
+				9421B8901A8BBCB200BA3E46 /* FLEXNetworkTransactionDetailTableViewController.m in Sources */,
 				944F74B4197B458C009AB039 /* FLEXWebViewController.m in Sources */,
 				944F74B4197B458C009AB039 /* FLEXWebViewController.m in Sources */,
 				944F74A5197B458C009AB039 /* FLEXFieldEditorViewController.m in Sources */,
 				944F74A5197B458C009AB039 /* FLEXFieldEditorViewController.m in Sources */,
 				944F7489197B458C009AB039 /* FLEXArrayExplorerViewController.m in Sources */,
 				944F7489197B458C009AB039 /* FLEXArrayExplorerViewController.m in Sources */,
@@ -752,6 +817,7 @@
 				944F74B2197B458C009AB039 /* FLEXLibrariesTableViewController.m in Sources */,
 				944F74B2197B458C009AB039 /* FLEXLibrariesTableViewController.m in Sources */,
 				535682BF18F3670300BAAD62 /* UIColor+AAPLApplicationSpecific.m in Sources */,
 				535682BF18F3670300BAAD62 /* UIColor+AAPLApplicationSpecific.m in Sources */,
 				535682B718F3670300BAAD62 /* AAPLSliderViewController.m in Sources */,
 				535682B718F3670300BAAD62 /* AAPLSliderViewController.m in Sources */,
+				9421B88E1A8BBCB200BA3E46 /* FLEXNetworkRecorder.m in Sources */,
 				944F74AC197B458C009AB039 /* FLEXToolbarItem.m in Sources */,
 				944F74AC197B458C009AB039 /* FLEXToolbarItem.m in Sources */,
 				944F74B7197B458C009AB039 /* FLEXImagePreviewViewController.m in Sources */,
 				944F74B7197B458C009AB039 /* FLEXImagePreviewViewController.m in Sources */,
 				650855EB1A9007D5006109A1 /* FLEXArgumentInputDateView.m in Sources */,
 				650855EB1A9007D5006109A1 /* FLEXArgumentInputDateView.m in Sources */,
@@ -762,6 +828,8 @@
 				944F7494197B458C009AB039 /* FLEXHeapEnumerator.m in Sources */,
 				944F7494197B458C009AB039 /* FLEXHeapEnumerator.m in Sources */,
 				535682B918F3670300BAAD62 /* AAPLStepperViewController.m in Sources */,
 				535682B918F3670300BAAD62 /* AAPLStepperViewController.m in Sources */,
 				535682AA18F3670300BAAD62 /* AAPLAppDelegate.m in Sources */,
 				535682AA18F3670300BAAD62 /* AAPLAppDelegate.m in Sources */,
+				9421B88D1A8BBCB200BA3E46 /* FLEXNetworkHistoryTableViewController.m in Sources */,
+				9421B8911A8BBCB200BA3E46 /* FLEXNetworkTransactionTableViewCell.m in Sources */,
 				535682B518F3670300BAAD62 /* AAPLProgressViewController.m in Sources */,
 				535682B518F3670300BAAD62 /* AAPLProgressViewController.m in Sources */,
 				535682AD18F3670300BAAD62 /* AAPLCustomToolbarViewController.m in Sources */,
 				535682AD18F3670300BAAD62 /* AAPLCustomToolbarViewController.m in Sources */,
 				944F748F197B458C009AB039 /* FLEXObjectExplorerFactory.m in Sources */,
 				944F748F197B458C009AB039 /* FLEXObjectExplorerFactory.m in Sources */,
@@ -782,7 +850,6 @@
 				944F7495197B458C009AB039 /* FLEXResources.m in Sources */,
 				944F7495197B458C009AB039 /* FLEXResources.m in Sources */,
 				535682A918F3670300BAAD62 /* AAPLAlertViewController.m in Sources */,
 				535682A918F3670300BAAD62 /* AAPLAlertViewController.m in Sources */,
 				944F74A6197B458C009AB039 /* FLEXIvarEditorViewController.m in Sources */,
 				944F74A6197B458C009AB039 /* FLEXIvarEditorViewController.m in Sources */,
-				944F748C197B458C009AB039 /* FLEXDescriptionTableViewCell.m in Sources */,
 				0149BFE2198FAFC700B90A1B /* FLEXFileBrowserSearchOperation.m in Sources */,
 				0149BFE2198FAFC700B90A1B /* FLEXFileBrowserSearchOperation.m in Sources */,
 				535682B218F3670300BAAD62 /* AAPLMasterViewController.m in Sources */,
 				535682B218F3670300BAAD62 /* AAPLMasterViewController.m in Sources */,
 			);
 			);
@@ -850,7 +917,7 @@
 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
 				GCC_WARN_UNUSED_FUNCTION = YES;
 				GCC_WARN_UNUSED_FUNCTION = YES;
 				GCC_WARN_UNUSED_VARIABLE = YES;
 				GCC_WARN_UNUSED_VARIABLE = YES;
-				IPHONEOS_DEPLOYMENT_TARGET = 6.0;
+				IPHONEOS_DEPLOYMENT_TARGET = 7.0;
 				ONLY_ACTIVE_ARCH = YES;
 				ONLY_ACTIVE_ARCH = YES;
 				SDKROOT = iphoneos;
 				SDKROOT = iphoneos;
 				TARGETED_DEVICE_FAMILY = "1,2";
 				TARGETED_DEVICE_FAMILY = "1,2";
@@ -883,7 +950,7 @@
 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
 				GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
 				GCC_WARN_UNUSED_FUNCTION = YES;
 				GCC_WARN_UNUSED_FUNCTION = YES;
 				GCC_WARN_UNUSED_VARIABLE = YES;
 				GCC_WARN_UNUSED_VARIABLE = YES;
-				IPHONEOS_DEPLOYMENT_TARGET = 6.0;
+				IPHONEOS_DEPLOYMENT_TARGET = 7.0;
 				SDKROOT = iphoneos;
 				SDKROOT = iphoneos;
 				TARGETED_DEVICE_FAMILY = "1,2";
 				TARGETED_DEVICE_FAMILY = "1,2";
 				VALIDATE_PRODUCT = YES;
 				VALIDATE_PRODUCT = YES;

+ 103 - 1
Example/UICatalog/AAPLAppDelegate.m

@@ -47,9 +47,14 @@
 
 
 #import "AAPLAppDelegate.h"
 #import "AAPLAppDelegate.h"
 
 
-@interface AAPLAppDelegate ()
+#if DEBUG
+#import "FLEXManager.h"
+#endif
+
+@interface AAPLAppDelegate () <NSURLConnectionDataDelegate, NSURLSessionDataDelegate>
 
 
 @property (nonatomic, strong) NSTimer *repeatingLogExampleTimer;
 @property (nonatomic, strong) NSTimer *repeatingLogExampleTimer;
+@property (nonatomic, strong) NSMutableArray *connections;
 
 
 @end
 @end
 
 
@@ -57,7 +62,11 @@
 
 
 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
 - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
 {
 {
+#if DEBUG
+    [[FLEXManager sharedManager] setNetworkDebuggingEnabled:YES];
+    [self sendExampleNetworkRequests];
     self.repeatingLogExampleTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(sendExampleLogMessage) userInfo:nil repeats:YES];
     self.repeatingLogExampleTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(sendExampleLogMessage) userInfo:nil repeats:YES];
+#endif
     return YES;
     return YES;
 }
 }
 
 
@@ -71,4 +80,97 @@
     }
     }
 }
 }
 
 
+#pragma mark - Networking Example
+
+- (void)sendExampleNetworkRequests
+{
+    // Async NSURLConnection
+    [NSURLConnection sendAsynchronousRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"https://api.github.com/repos/Flipboard/FLEX/issues"]] queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
+
+    }];
+
+    // Sync NSURLConnection
+    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
+        [NSURLConnection sendSynchronousRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://lorempixel.com/320/480/"]] returningResponse:NULL error:NULL];
+    });
+
+    // NSURLSession
+    NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
+    config.timeoutIntervalForRequest = 10.0;
+    NSURLSession *mySession = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:[NSOperationQueue mainQueue]];
+
+    NSMutableArray *pendingTasks = [NSMutableArray array];
+
+    // NSURLSessionDataTask with delegate
+    [pendingTasks addObject:[mySession dataTaskWithURL:[NSURL URLWithString:@"http://cdn.flipboard.com/serviceIcons/v2/social-icon-flipboard-96.png"]]];
+
+    // NSURLSessionDownloadTask with delegate
+    [pendingTasks addObject:[mySession downloadTaskWithURL:[NSURL URLWithString:@"https://assets-cdn.github.com/images/icons/emoji/unicode/1f44d.png?v5"]]];
+
+    // Async NSURLSessionDownloadTask
+    [pendingTasks addObject:[[NSURLSession sharedSession] downloadTaskWithURL:[NSURL URLWithString:@"http://lorempixel.com/1024/1024/"] completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
+
+    }]];
+
+    // Async NSURLSessionDataTask
+    [pendingTasks addObject:[[NSURLSession sharedSession] dataTaskWithURL:[NSURL URLWithString:@"https://api.github.com/emojis"] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
+
+    }]];
+
+    // Async NSURLSessionUploadTask
+    NSMutableURLRequest *uploadRequest = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"https://google.com/"]];
+    uploadRequest.HTTPMethod = @"POST";
+    NSData *data = [@"q=test" dataUsingEncoding:NSUTF8StringEncoding];
+    [pendingTasks addObject:[mySession uploadTaskWithRequest:uploadRequest fromData:data completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
+
+    }]];
+
+    // Remaining requests made through NSURLConnection with a delegate
+    NSArray *requestURLStrings = @[ @"http://lorempixel.com/400/400/",
+                                    @"http://google.com",
+                                    @"http://search.cocoapods.org/api/pods?query=FLEX&amount=1",
+                                    @"https://api.github.com/users/Flipboard/repos",
+                                    @"http://info.cern.ch/hypertext/WWW/TheProject.html",
+                                    @"https://api.github.com/repos/Flipboard/FLEX/issues",
+                                    @"https://cloud.githubusercontent.com/assets/516562/3971767/e4e21f58-27d6-11e4-9b07-4d1fe82b80ca.png",
+                                    @"http://hipsterjesus.com/api?paras=1&type=hipster-centric&html=false",
+                                    @"http://lorempixel.com/750/1334/" ];
+
+    NSTimeInterval delayTime = 10.0;
+    const NSTimeInterval stagger = 1.0;
+
+    // Send off the NSURLSessionTasks (staggered)
+    for (NSURLSessionTask *task in pendingTasks) {
+        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayTime * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
+            [task resume];
+        });
+        delayTime += stagger;
+    }
+
+    // Begin the NSURLConnection requests (staggered)
+    self.connections = [NSMutableArray array];
+    for (NSString *urlString in requestURLStrings) {
+        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayTime * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
+            NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:urlString]];
+            [self.connections addObject:[[NSURLConnection alloc] initWithRequest:request delegate:self]];
+        });
+        delayTime += stagger;
+    }
+}
+
+- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler
+{
+    completionHandler(NSURLSessionResponseAllow);
+}
+
+- (void)connectionDidFinishLoading:(NSURLConnection *)connection
+{
+    [self.connections removeObject:connection];
+}
+
+- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error
+{
+    [self.connections removeObject:connection];
+}
+
 @end
 @end

+ 3 - 0
Example/UICatalog/AAPLMasterViewController.m

@@ -74,7 +74,10 @@
 // "More" bar button item is correctly transferred to the destination detail view controller's navigation item. We are only concerned about
 // "More" bar button item is correctly transferred to the destination detail view controller's navigation item. We are only concerned about
 // this change when the application is in portrait mode since this is the only time that the "More" bar button item will be visible.
 // this change when the application is in portrait mode since this is the only time that the "More" bar button item will be visible.
 - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
 - (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wdeprecated-declarations"
     if (UIInterfaceOrientationIsPortrait(self.interfaceOrientation)) {
     if (UIInterfaceOrientationIsPortrait(self.interfaceOrientation)) {
+#pragma clang diagnostic pop
         UINavigationController *newDetailViewController = [segue destinationViewController];
         UINavigationController *newDetailViewController = [segue destinationViewController];
         UINavigationController *oldDetailViewController = [self.splitViewController.viewControllers lastObject];
         UINavigationController *oldDetailViewController = [self.splitViewController.viewControllers lastObject];
         
         

+ 12 - 16
Example/UICatalog/Images.xcassets/LaunchImage.launchimage/Contents.json

@@ -32,42 +32,38 @@
       "idiom" : "iphone",
       "idiom" : "iphone",
       "extent" : "full-screen",
       "extent" : "full-screen",
       "minimum-system-version" : "7.0",
       "minimum-system-version" : "7.0",
+      "filename" : "Launch@2x.png",
       "scale" : "2x"
       "scale" : "2x"
     },
     },
     {
     {
-      "orientation" : "portrait",
+      "extent" : "full-screen",
       "idiom" : "iphone",
       "idiom" : "iphone",
       "subtype" : "retina4",
       "subtype" : "retina4",
-      "extent" : "full-screen",
+      "filename" : "Launch@2x~568h.png",
       "minimum-system-version" : "7.0",
       "minimum-system-version" : "7.0",
+      "orientation" : "portrait",
       "scale" : "2x"
       "scale" : "2x"
     },
     },
     {
     {
       "orientation" : "portrait",
       "orientation" : "portrait",
-      "idiom" : "ipad",
-      "extent" : "full-screen",
-      "minimum-system-version" : "7.0",
-      "scale" : "1x"
-    },
-    {
-      "orientation" : "landscape",
-      "idiom" : "ipad",
+      "idiom" : "iphone",
       "extent" : "full-screen",
       "extent" : "full-screen",
-      "minimum-system-version" : "7.0",
+      "filename" : "Launch.png",
       "scale" : "1x"
       "scale" : "1x"
     },
     },
     {
     {
       "orientation" : "portrait",
       "orientation" : "portrait",
-      "idiom" : "ipad",
+      "idiom" : "iphone",
       "extent" : "full-screen",
       "extent" : "full-screen",
-      "minimum-system-version" : "7.0",
+      "filename" : "Launch@2x.png",
       "scale" : "2x"
       "scale" : "2x"
     },
     },
     {
     {
-      "orientation" : "landscape",
-      "idiom" : "ipad",
+      "orientation" : "portrait",
+      "idiom" : "iphone",
       "extent" : "full-screen",
       "extent" : "full-screen",
-      "minimum-system-version" : "7.0",
+      "filename" : "Launch@2x~568h.png",
+      "subtype" : "retina4",
       "scale" : "2x"
       "scale" : "2x"
     }
     }
   ],
   ],

BIN
Example/UICatalog/Images.xcassets/LaunchImage.launchimage/Launch.png


BIN
Example/UICatalog/Images.xcassets/LaunchImage.launchimage/Launch@2x.png


BIN
Example/UICatalog/Images.xcassets/LaunchImage.launchimage/Launch@2x~568h.png


+ 2 - 1
FLEX.podspec

@@ -27,9 +27,10 @@ Pod::Spec.new do |spec|
   spec.license          = { :type => "BSD", :file => "LICENSE" }
   spec.license          = { :type => "BSD", :file => "LICENSE" }
   spec.author           = { "Ryan Olson" => "ryanolsonk@gmail.com" }
   spec.author           = { "Ryan Olson" => "ryanolsonk@gmail.com" }
   spec.social_media_url = "https://twitter.com/ryanolsonk"
   spec.social_media_url = "https://twitter.com/ryanolsonk"
-  spec.platform         = :ios, "6.0"
+  spec.platform         = :ios, "7.0"
   spec.source           = { :git => "https://github.com/Flipboard/FLEX.git", :tag => "#{spec.version}" }
   spec.source           = { :git => "https://github.com/Flipboard/FLEX.git", :tag => "#{spec.version}" }
   spec.source_files     = "Classes/**/*.{h,m}"
   spec.source_files     = "Classes/**/*.{h,m}"
   spec.frameworks       = "CoreGraphics"
   spec.frameworks       = "CoreGraphics"
+  spec.libraries        = "z"
   spec.requires_arc     = true
   spec.requires_arc     = true
 end
 end

+ 8 - 1
README.md

@@ -9,6 +9,7 @@ FLEX (Flipboard Explorer) is a set of in-app debugging and exploration tools for
 - See the properties and ivars on any object.
 - See the properties and ivars on any object.
 - Dynamically modify many properties and ivars.
 - Dynamically modify many properties and ivars.
 - Dynamically call instance and class methods.
 - Dynamically call instance and class methods.
+- Observe detailed network request history with timing, headers, and full responses.
 - View system log messages (e.g. from `NSLog`).
 - View system log messages (e.g. from `NSLog`).
 - Access any live object via a scan of the heap.
 - Access any live object via a scan of the heap.
 - View the file system within your app's sandbox.
 - View the file system within your app's sandbox.
@@ -53,6 +54,11 @@ Once a view is selected, you can tap on the info bar below the toolbar to presen
 
 
 ![View Modification](http://engineering.flipboard.com/assets/flex/advanced-view-editing.gif)
 ![View Modification](http://engineering.flipboard.com/assets/flex/advanced-view-editing.gif)
 
 
+### Network History
+When enabled, network debugging allows you to view all requests made using NSURLConnection or NSURLSession. Settings allow you to adjust what kind of response bodies get cached and the maximum size limit of the response cache. You can choose to have network debugging enabled automatically on app launch. This setting is persisted accross launches.
+
+![Network History](http://engineering.flipboard.com/assets/flex/network-history.gif)
+
 ### All Objects on the Heap
 ### All Objects on the Heap
 FLEX queries malloc for all the live allocated memory blocks and searches for ones that look like objects. You can see everything from here.
 FLEX queries malloc for all the live allocated memory blocks and searches for ones that look like objects. You can see everything from here.
 
 
@@ -118,6 +124,8 @@ FLEX builds on ideas and inspiration from open source tools that came before it.
 - [heap_find.cpp](https://www.opensource.apple.com/source/lldb/lldb-179.1/examples/darwin/heap_find/heap/heap_find.cpp): an example of enumerating malloc blocks for finding objects on the heap.
 - [heap_find.cpp](https://www.opensource.apple.com/source/lldb/lldb-179.1/examples/darwin/heap_find/heap/heap_find.cpp): an example of enumerating malloc blocks for finding objects on the heap.
 - [Gist](https://gist.github.com/samdmarshall/17f4e66b5e2e579fd396) from [@samdmarshall](https://github.com/samdmarshall): another example of enumerating malloc blocks.
 - [Gist](https://gist.github.com/samdmarshall/17f4e66b5e2e579fd396) from [@samdmarshall](https://github.com/samdmarshall): another example of enumerating malloc blocks.
 - [Non-pointer isa](http://www.sealiesoftware.com/blog/archive/2013/09/24/objc_explain_Non-pointer_isa.html): an explanation of changes to the isa field on iOS for ARM64 and mention of the useful `objc_debug_isa_class_mask` variable.
 - [Non-pointer isa](http://www.sealiesoftware.com/blog/archive/2013/09/24/objc_explain_Non-pointer_isa.html): an explanation of changes to the isa field on iOS for ARM64 and mention of the useful `objc_debug_isa_class_mask` variable.
+- [GZIP](https://github.com/nicklockwood/GZIP): A library for compressing/decompressing data on iOS using libz.
+
 
 
 
 
 ## Contributing
 ## Contributing
@@ -126,7 +134,6 @@ We welcome pull requests for bug fixes, new features, and improvements to FLEX.
 
 
 ## TODO
 ## TODO
 - Swift runtime introspection (swift classes, swift objects on the heap, etc.)
 - Swift runtime introspection (swift classes, swift objects on the heap, etc.)
-- Network request logging
 - Improved file type detection and display in the file browser
 - Improved file type detection and display in the file browser
 - Add new NSUserDefault key/value pairs on the fly
 - Add new NSUserDefault key/value pairs on the fly