Compare commits
261 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8aece0a266 | |||
| 81b27b6918 | |||
| 727943c4b3 | |||
| 9f2c032157 | |||
| d6a5b1af8d | |||
| dda9dd5beb | |||
| 888887f09a | |||
| b70a1a2f48 | |||
| 54730c368c | |||
| 21672e6f8d | |||
| 4ffc992872 | |||
| 8eea2ec652 | |||
| 3df01ee7bb | |||
| d0ad6e4319 | |||
| 37aec6dacc | |||
| cdc5aae4b7 | |||
| fd2b89fd24 | |||
| f1683e54c3 | |||
| c66dd2e7d3 | |||
| 5a5b921bbf | |||
| 30cc65bd9d | |||
| 29a45aa02d | |||
| 08b25ea8d3 | |||
| 7ffcb83563 | |||
| b0b64c1ba9 | |||
| bc5dfa02ec | |||
| c69f5c220a | |||
| 7f28d430d0 | |||
| 30dc024903 | |||
| 6403053989 | |||
| ba352c15e8 | |||
| e9e084e6f1 | |||
| 26e92c2bd6 | |||
| 41d761f822 | |||
| 832a03bf27 | |||
| ebf5254629 | |||
| e199b529c8 | |||
| 06edea64ae | |||
| 6d3afe36d1 | |||
| b22d2b57a2 | |||
| b453086936 | |||
| 103489c566 | |||
| 3dd27557ea | |||
| a64188dd5e | |||
| 44e428655a | |||
| 96d8b425d5 | |||
| 7df172afac | |||
| 9bb44925c8 | |||
| 320aeb815b | |||
| ab4b678498 | |||
| 52bf2071a5 | |||
| f0bb931a64 | |||
| 30f1fecc54 | |||
| 85a424a824 | |||
| 6405bf40e3 | |||
| b79fd26ca4 | |||
| caadcce7f1 | |||
| c250200d03 | |||
| 51c05087e7 | |||
| 7a2e65f292 | |||
| 0489c09ba3 | |||
| 70038d244d | |||
| a9e0dedd31 | |||
| f6ad51219d | |||
| a42af79040 | |||
| 792634527a | |||
| 5627219c56 | |||
| 4e81d4b476 | |||
| 001d58cd89 | |||
| 03c96d8fdb | |||
| b5423192fb | |||
| 1efb40a07e | |||
| 6c6023dc84 | |||
| a6dc4b010c | |||
| dd87da4134 | |||
| 627ff6cbe2 | |||
| 9cc8435cae | |||
| 3d977450ca | |||
| f7c482ceed | |||
| c38b90ee60 | |||
| 70491431fa | |||
| 6bc055911e | |||
| 9b1e13b963 | |||
| 74a73893d4 | |||
| 4a3ab17851 | |||
| 5b8efe71a7 | |||
| b7f2d9bcbe | |||
| f4efc6dbbf | |||
| efab760253 | |||
| 48826e2160 | |||
| 0b4e231814 | |||
| 6f2d811338 | |||
| 9c9ce5e2e1 | |||
| 08b4559b26 | |||
| 913ad5e2c6 | |||
| 7649dc616c | |||
| efabb29a52 | |||
| e3612e31d7 | |||
| 2575d2eaee | |||
| f041002e73 | |||
| 0da49c1eb6 | |||
| a3a84b0cd7 | |||
| 33be034e2b | |||
| f590263d9f | |||
| a1c378a9d5 | |||
| 617db9c48a | |||
| b05f78c388 | |||
| d9ecb2359b | |||
| 5f9a61c755 | |||
| 3dd178c029 | |||
| ed938aa333 | |||
| 717ed18077 | |||
| 1f43569a4d | |||
| 991fd6559d | |||
| 9d98993838 | |||
| 3eae2ce457 | |||
| f24edcdba9 | |||
| a2c7110b07 | |||
| 300b68ae83 | |||
| ea53ac9bfb | |||
| 6f9c0917eb | |||
| b66857f0da | |||
| 52a51b6fd4 | |||
| 06ff0152bb | |||
| ea1ecbc73a | |||
| 33d2667c99 | |||
| b333f83470 | |||
| fd54e4cae0 | |||
| b1d6ac4d52 | |||
| bd9a062329 | |||
| 56fffab825 | |||
| e73eafccdb | |||
| f7d23bc8a2 | |||
| e531190616 | |||
| a88b5f5019 | |||
| 538fe5e17b | |||
| 62a350059b | |||
| 02657d7766 | |||
| f13e39450c | |||
| 1544f2bf4b | |||
| cd41de37e9 | |||
| b4c9cbd6a6 | |||
| c5668f42b2 | |||
| 99e8f112b2 | |||
| 57f4519c3f | |||
| 671f2eeb77 | |||
| ab31f8202c | |||
| ffd532b630 | |||
| f911a91000 | |||
| fed8c35f2f | |||
| aafc23a5ec | |||
| eee45744d0 | |||
| 51493322c3 | |||
| baeb0c9a14 | |||
| 141cdf121d | |||
| cdb9235a83 | |||
| cae0e0ef98 | |||
| ec98b0cba6 | |||
| d468254b21 | |||
| 462e0ed548 | |||
| cbfc49bd0c | |||
| 176652010c | |||
| 2e516615bd | |||
| 6d1db9535f | |||
| 09bc0377d8 | |||
| 197dcccb4e | |||
| e082fdeddd | |||
| e4e22b8cd8 | |||
| 11bb3574a8 | |||
| 9ed5e08089 | |||
| 01abdf05ed | |||
| 65e6e86378 | |||
| 2b3677e503 | |||
| 84dcbb4847 | |||
| cca6415c2a | |||
| 73f3d52bb3 | |||
| 4e167f02fb | |||
| ec76385bcf | |||
| 387a72d93e | |||
| b8d8f29c97 | |||
| 8e7dfe9adb | |||
| 6471a96b19 | |||
| 3b66d9b53e | |||
| 3385493b6a | |||
| 993b61ecb6 | |||
| ea5cea6d77 | |||
| 47db763ea6 | |||
| 054cfd196a | |||
| 4d9271c7ed | |||
| 719c641692 | |||
| 95eaab3486 | |||
| b4d03fc9e9 | |||
| 3b5ed964b2 | |||
| 67a91b5283 | |||
| 580a4d8a1c | |||
| ad0b130dc4 | |||
| 5ecdb953c7 | |||
| c7132f5890 | |||
| 6d680e4df7 | |||
| 18e2edb15f | |||
| f4106e4be5 | |||
| a53f744035 | |||
| 61bcf5354a | |||
| dd686e80da | |||
| 794d00d82f | |||
| e9801242f5 | |||
| 11411a20b7 | |||
| db089f4fb9 | |||
| 34721971c5 | |||
| 26f80349f6 | |||
| 190006dcc0 | |||
| 1d7d41a350 | |||
| d74bc56487 | |||
| fa4eeea424 | |||
| 3c8ac29ebe | |||
| 18487eeac5 | |||
| b040530219 | |||
| 7fb22825ae | |||
| 61fe88fd7f | |||
| 60226cc62b | |||
| fad33ddf7a | |||
| 630eec59ee | |||
| 4e096a2cd7 | |||
| 28241518bf | |||
| e1c2c9c6fc | |||
| ef96471937 | |||
| 4b9385bee0 | |||
| 9d68ff5d15 | |||
| a644f89bbd | |||
| ed9b721cd5 | |||
| 2b1f28c2fd | |||
| 6d563856a6 | |||
| 491ba78729 | |||
| d978d574ad | |||
| a2cd7d81ed | |||
| e0366afcc2 | |||
| 1653d86552 | |||
| 05399839a3 | |||
| 49afafc50a | |||
| 063ffe97c1 | |||
| 35938ecbee | |||
| 9999f6b1d3 | |||
| 93d759e743 | |||
| d989001073 | |||
| ad231a5bb7 | |||
| b67d18f569 | |||
| f65263d027 | |||
| 3870f7c7cf | |||
| 88c893cc44 | |||
| 53a95845f4 | |||
| 8f6b8b2dae | |||
| fefcd125b5 | |||
| 5fb6684b31 | |||
| 7a93e9b4dd | |||
| a04cdf789a | |||
| 9d1489adbe | |||
| 10c7b7b420 | |||
| 8bf66eb664 | |||
| 43eedfafc5 | |||
| c6b2e97835 | |||
| c6b0a74873 |
@@ -0,0 +1,47 @@
|
||||
---
|
||||
BasedOnStyle: WebKit
|
||||
AccessModifierOffset: -2
|
||||
AlignEscapedNewlinesLeft: false
|
||||
AlignTrailingComments: true
|
||||
AllowAllParametersOfDeclarationOnNextLine: false
|
||||
AllowShortIfStatementsOnASingleLine: false
|
||||
AllowShortLoopsOnASingleLine: false
|
||||
AlwaysBreakBeforeMultilineStrings: false
|
||||
AlwaysBreakTemplateDeclarations: false
|
||||
BinPackParameters: true
|
||||
BreakBeforeBinaryOperators: false
|
||||
BreakBeforeBraces: Linux
|
||||
BreakBeforeTernaryOperators: true
|
||||
BreakConstructorInitializersBeforeComma: false
|
||||
ColumnLimit: 0
|
||||
ConstructorInitializerAllOnOneLineOrOnePerLine: true
|
||||
ConstructorInitializerIndentWidth: 4
|
||||
ContinuationIndentWidth: 4
|
||||
Cpp11BracedListStyle: true
|
||||
DerivePointerBinding: false
|
||||
ExperimentalAutoDetectBinPacking: false
|
||||
IndentCaseLabels: true
|
||||
IndentFunctionDeclarationAfterType: false
|
||||
IndentWidth: 4
|
||||
MaxEmptyLinesToKeep: 3
|
||||
NamespaceIndentation: None
|
||||
ObjCSpaceBeforeProtocolList: true
|
||||
PenaltyBreakBeforeFirstCallParameter: 19
|
||||
PenaltyBreakComment: 60
|
||||
PenaltyBreakFirstLessLess: 120
|
||||
PenaltyBreakString: 1000
|
||||
PenaltyExcessCharacter: 1000000
|
||||
PenaltyReturnTypeOnItsOwnLine: 60
|
||||
PointerBindsToType: false
|
||||
SpaceAfterControlStatementKeyword: true
|
||||
SpaceBeforeAssignmentOperators: true
|
||||
SpaceInEmptyParentheses: false
|
||||
SpacesBeforeTrailingComments: 1
|
||||
SpacesInAngles: false
|
||||
SpacesInCStyleCastParentheses: false
|
||||
SpacesInParentheses: false
|
||||
Standard: Auto
|
||||
TabWidth: 4
|
||||
UseTab: Never
|
||||
|
||||
...
|
||||
@@ -0,0 +1,8 @@
|
||||
language: objective-c
|
||||
xcode_workspace: FLEX.xcworkspace
|
||||
matrix:
|
||||
include:
|
||||
- xcode_scheme: UICatalog
|
||||
xcode_sdk: iphonesimulator
|
||||
- xcode_scheme: FLEX
|
||||
xcode_sdk: iphonesimulator
|
||||
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// FLEXArgumentInputDataView.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Daniel Rodriguez Troitino on 2/14/15.
|
||||
// Copyright (c) 2015 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputView.h"
|
||||
|
||||
@interface FLEXArgumentInputDateView : FLEXArgumentInputView
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,63 @@
|
||||
//
|
||||
// FLEXArgumentInputDataView.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Daniel Rodriguez Troitino on 2/14/15.
|
||||
// Copyright (c) 2015 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXArgumentInputDateView.h"
|
||||
#import "FLEXRuntimeUtility.h"
|
||||
|
||||
@interface FLEXArgumentInputDateView ()
|
||||
|
||||
@property (nonatomic, strong) UIDatePicker *datePicker;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXArgumentInputDateView
|
||||
|
||||
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
|
||||
{
|
||||
self = [super initWithArgumentTypeEncoding:typeEncoding];
|
||||
if (self) {
|
||||
self.datePicker = [[UIDatePicker alloc] init];
|
||||
self.datePicker.datePickerMode = UIDatePickerModeDateAndTime;
|
||||
// Using UTC, because that's what the NSDate description prints
|
||||
self.datePicker.calendar = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierGregorian];
|
||||
self.datePicker.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"];
|
||||
[self addSubview:self.datePicker];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setInputValue:(id)inputValue
|
||||
{
|
||||
if ([inputValue isKindOfClass:[NSDate class]]) {
|
||||
self.datePicker.date = inputValue;
|
||||
}
|
||||
}
|
||||
|
||||
- (id)inputValue
|
||||
{
|
||||
return self.datePicker.date;
|
||||
}
|
||||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
[super layoutSubviews];
|
||||
self.datePicker.frame = self.bounds;
|
||||
}
|
||||
|
||||
- (CGSize)sizeThatFits:(CGSize)size
|
||||
{
|
||||
CGFloat height = [self.datePicker sizeThatFits:size].height;
|
||||
return CGSizeMake(size.width, height);
|
||||
}
|
||||
|
||||
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
|
||||
{
|
||||
return (type && (strcmp(type, FLEXEncodeClass(NSDate)) == 0)) || [value isKindOfClass:[NSDate class]];
|
||||
}
|
||||
|
||||
@end
|
||||
+3
-3
@@ -31,7 +31,7 @@
|
||||
inputView.targetSize = FLEXArgumentInputViewSizeSmall;
|
||||
|
||||
if (fieldIndex < [customTitles count]) {
|
||||
inputView.title = [customTitles objectAtIndex:fieldIndex];
|
||||
inputView.title = customTitles[fieldIndex];
|
||||
} else {
|
||||
inputView.title = [NSString stringWithFormat:@"%@ field %lu (%@)", structName, (unsigned long)fieldIndex, prettyTypeEncoding];
|
||||
}
|
||||
@@ -72,7 +72,7 @@
|
||||
[FLEXRuntimeUtility enumerateTypesInStructEncoding:structTypeEncoding usingBlock:^(NSString *structName, const char *fieldTypeEncoding, NSString *prettyTypeEncoding, NSUInteger fieldIndex, NSUInteger fieldOffset) {
|
||||
|
||||
void *fieldPointer = unboxedValue + fieldOffset;
|
||||
FLEXArgumentInputView *inputView = [self.argumentInputViews objectAtIndex:fieldIndex];
|
||||
FLEXArgumentInputView *inputView = self.argumentInputViews[fieldIndex];
|
||||
|
||||
if (fieldTypeEncoding[0] == @encode(id)[0] || fieldTypeEncoding[0] == @encode(Class)[0]) {
|
||||
inputView.inputValue = (__bridge id)fieldPointer;
|
||||
@@ -102,7 +102,7 @@
|
||||
[FLEXRuntimeUtility enumerateTypesInStructEncoding:structTypeEncoding usingBlock:^(NSString *structName, const char *fieldTypeEncoding, NSString *prettyTypeEncoding, NSUInteger fieldIndex, NSUInteger fieldOffset) {
|
||||
|
||||
void *fieldPointer = unboxedStruct + fieldOffset;
|
||||
FLEXArgumentInputView *inputView = [self.argumentInputViews objectAtIndex:fieldIndex];
|
||||
FLEXArgumentInputView *inputView = self.argumentInputViews[fieldIndex];
|
||||
|
||||
if (fieldTypeEncoding[0] == @encode(id)[0] || fieldTypeEncoding[0] == @encode(Class)[0]) {
|
||||
// Object fields
|
||||
+4
-1
@@ -12,9 +12,12 @@
|
||||
|
||||
@interface FLEXArgumentInputViewFactory : NSObject
|
||||
|
||||
/// The main factory method for making argument input view subclasses that are the best fit for the type.
|
||||
/// Forwards to argumentInputViewForTypeEncoding:currentValue: with a nil currentValue.
|
||||
+ (FLEXArgumentInputView *)argumentInputViewForTypeEncoding:(const char *)typeEncoding;
|
||||
|
||||
/// The main factory method for making argument input view subclasses that are the best fit for the type.
|
||||
+ (FLEXArgumentInputView *)argumentInputViewForTypeEncoding:(const char *)typeEncoding currentValue:(id)currentValue;
|
||||
|
||||
/// A way to check if we should try editing a filed given its type encoding and value.
|
||||
/// Useful when deciding whether to edit or explore a property, ivar, or NSUserDefaults value.
|
||||
+ (BOOL)canEditFieldWithTypeEncoding:(const char *)typeEncoding currentValue:(id)currentValue;
|
||||
+9
-1
@@ -16,12 +16,18 @@
|
||||
#import "FLEXArgumentInputStringView.h"
|
||||
#import "FLEXArgumentInputFontView.h"
|
||||
#import "FLEXArgumentInputColorView.h"
|
||||
#import "FLEXArgumentInputDateView.h"
|
||||
|
||||
@implementation FLEXArgumentInputViewFactory
|
||||
|
||||
+ (FLEXArgumentInputView *)argumentInputViewForTypeEncoding:(const char *)typeEncoding
|
||||
{
|
||||
Class subclass = [self argumentInputViewSubclassForTypeEncoding:typeEncoding currentValue:nil];
|
||||
return [self argumentInputViewForTypeEncoding:typeEncoding currentValue:nil];
|
||||
}
|
||||
|
||||
+ (FLEXArgumentInputView *)argumentInputViewForTypeEncoding:(const char *)typeEncoding currentValue:(id)currentValue
|
||||
{
|
||||
Class subclass = [self argumentInputViewSubclassForTypeEncoding:typeEncoding currentValue:currentValue];
|
||||
if (!subclass) {
|
||||
// Fall back to a FLEXArgumentInputNotSupportedView if we can't find a subclass that fits the type encoding.
|
||||
// The unsupported view shows "nil" and does not allow user input.
|
||||
@@ -47,6 +53,8 @@
|
||||
argumentInputViewSubclass = [FLEXArgumentInputStructView class];
|
||||
} else if ([FLEXArgumentInputSwitchView supportsObjCType:typeEncoding withCurrentValue:currentValue]) {
|
||||
argumentInputViewSubclass = [FLEXArgumentInputSwitchView class];
|
||||
} else if ([FLEXArgumentInputDateView supportsObjCType:typeEncoding withCurrentValue:currentValue]) {
|
||||
argumentInputViewSubclass = [FLEXArgumentInputDateView class];
|
||||
} else if ([FLEXArgumentInputNumberView supportsObjCType:typeEncoding withCurrentValue:currentValue]) {
|
||||
argumentInputViewSubclass = [FLEXArgumentInputNumberView class];
|
||||
} else if ([FLEXArgumentInputJSONObjectView supportsObjCType:typeEncoding withCurrentValue:currentValue]) {
|
||||
@@ -41,10 +41,11 @@
|
||||
[super viewDidLoad];
|
||||
|
||||
self.fieldEditorView.fieldDescription = self.key;
|
||||
|
||||
FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:@encode(id)];
|
||||
|
||||
id currentValue = [self.defaults objectForKey:self.key];
|
||||
FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:@encode(id) currentValue:currentValue];
|
||||
inputView.backgroundColor = self.view.backgroundColor;
|
||||
inputView.inputValue = [self.defaults objectForKey:self.key];
|
||||
inputView.inputValue = currentValue;
|
||||
self.fieldEditorView.argumentInputViews = @[inputView];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
//
|
||||
// FLEXManager.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 4/4/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface FLEXManager : NSObject
|
||||
|
||||
+ (instancetype)sharedManager;
|
||||
|
||||
@property (nonatomic, readonly) BOOL isHidden;
|
||||
|
||||
- (void)showExplorer;
|
||||
- (void)hideExplorer;
|
||||
|
||||
#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.
|
||||
/// @param entryName The string to be displayed in the cell.
|
||||
/// @param objectFutureBlock When you tap on the row, information about the object returned by this block will be displayed.
|
||||
/// Passing a block that returns an object allows you to display information about an object whose actual pointer may change at runtime (e.g. +currentUser)
|
||||
/// @note This method must be called from the main thread.
|
||||
/// The objectFutureBlock will be invoked from the main thread and may return nil.
|
||||
/// @note The passed block will be copied and retain for the duration of the application, you may want to use __weak references.
|
||||
- (void)registerGlobalEntryWithName:(NSString *)entryName objectFutureBlock:(id(^)(void))objectFutureBlock;
|
||||
|
||||
@end
|
||||
@@ -1,122 +0,0 @@
|
||||
//
|
||||
// FLEXManager.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 4/4/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXManager.h"
|
||||
#import "FLEXExplorerViewController.h"
|
||||
#import "FLEXWindow.h"
|
||||
#import "FLEXGlobalsTableViewControllerEntry.h"
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
#import "FLEXObjectExplorerViewController.h"
|
||||
|
||||
@interface FLEXManager () <FLEXWindowEventDelegate, FLEXExplorerViewControllerDelegate>
|
||||
|
||||
@property (nonatomic, strong) FLEXWindow *explorerWindow;
|
||||
@property (nonatomic, strong) FLEXExplorerViewController *explorerViewController;
|
||||
|
||||
@property (nonatomic, readonly, strong) NSMutableArray *userGlobalEntries;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXManager
|
||||
|
||||
+ (instancetype)sharedManager
|
||||
{
|
||||
static FLEXManager *sharedManager = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
sharedManager = [[[self class] alloc] init];
|
||||
});
|
||||
return sharedManager;
|
||||
}
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_userGlobalEntries = [[NSMutableArray alloc] init];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (FLEXWindow *)explorerWindow
|
||||
{
|
||||
NSAssert([NSThread isMainThread], @"You must use %@ from the main thread only.", NSStringFromClass([self class]));
|
||||
|
||||
if (!_explorerWindow) {
|
||||
_explorerWindow = [[FLEXWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
|
||||
_explorerWindow.eventDelegate = self;
|
||||
|
||||
_explorerWindow.rootViewController = self.explorerViewController;
|
||||
[_explorerWindow addSubview:self.explorerViewController.view];
|
||||
}
|
||||
|
||||
return _explorerWindow;
|
||||
}
|
||||
|
||||
- (FLEXExplorerViewController *)explorerViewController
|
||||
{
|
||||
if (!_explorerViewController) {
|
||||
_explorerViewController = [[FLEXExplorerViewController alloc] init];
|
||||
_explorerViewController.delegate = self;
|
||||
}
|
||||
|
||||
return _explorerViewController;
|
||||
}
|
||||
|
||||
- (void)showExplorer
|
||||
{
|
||||
self.explorerWindow.hidden = NO;
|
||||
}
|
||||
|
||||
- (void)hideExplorer
|
||||
{
|
||||
self.explorerWindow.hidden = YES;
|
||||
}
|
||||
|
||||
- (BOOL)isHidden
|
||||
{
|
||||
return self.explorerWindow.isHidden;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - FLEXWindowEventDelegate
|
||||
|
||||
- (BOOL)shouldHandleTouchAtPoint:(CGPoint)pointInWindow
|
||||
{
|
||||
// Ask the explorer view controller
|
||||
return [self.explorerViewController shouldReceiveTouchAtWindowPoint:pointInWindow];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - FLEXExplorerViewControllerDelegate
|
||||
|
||||
- (void)explorerViewControllerDidFinish:(FLEXExplorerViewController *)explorerViewController
|
||||
{
|
||||
[self hideExplorer];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Extensions
|
||||
|
||||
- (void)registerGlobalEntryWithName:(NSString *)entryName objectFutureBlock:(id (^)(void))objectFutureBlock
|
||||
{
|
||||
NSParameterAssert(entryName);
|
||||
NSParameterAssert(objectFutureBlock);
|
||||
NSAssert([NSThread isMainThread], @"This method must be called from the main thread.");
|
||||
|
||||
entryName = entryName.copy;
|
||||
FLEXGlobalsTableViewControllerEntry *entry = [FLEXGlobalsTableViewControllerEntry entryWithNameFuture:^NSString *{
|
||||
return entryName;
|
||||
} viewControllerFuture:^UIViewController *{
|
||||
return [FLEXObjectExplorerFactory explorerViewControllerForObject:objectFutureBlock()];
|
||||
}];
|
||||
|
||||
[self.userGlobalEntries addObject:entry];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -1,36 +0,0 @@
|
||||
//
|
||||
// FLEXWindow.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 4/13/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXWindow.h"
|
||||
|
||||
@implementation FLEXWindow
|
||||
|
||||
- (id)initWithFrame:(CGRect)frame
|
||||
{
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
self.backgroundColor = [UIColor clearColor];
|
||||
// Some apps have windows at UIWindowLevelStatusBar + n.
|
||||
// If we make the window level too high, we block out UIAlertViews.
|
||||
// There's a balance between staying above the app's windows and staying below alerts.
|
||||
// UIWindowLevelStatusBar + 100 seems to hit that balance.
|
||||
self.windowLevel = UIWindowLevelStatusBar + 100.0;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
|
||||
{
|
||||
BOOL pointInside = NO;
|
||||
if ([self.eventDelegate shouldHandleTouchAtPoint:point]) {
|
||||
pointInside = [super pointInside:point withEvent:event];
|
||||
}
|
||||
return pointInside;
|
||||
}
|
||||
|
||||
@end
|
||||
+1
-1
@@ -47,7 +47,7 @@
|
||||
[self.dragHandle addSubview:self.dragHandleImageView];
|
||||
|
||||
UIImage *globalsIcon = [FLEXResources globeIcon];
|
||||
self.globalsItem = [FLEXToolbarItem toolbarItemWithTitle:@"globals" image:globalsIcon];
|
||||
self.globalsItem = [FLEXToolbarItem toolbarItemWithTitle:@"menu" image:globalsIcon];
|
||||
[self addSubview:self.globalsItem];
|
||||
[toolbarItems addObject:self.globalsItem];
|
||||
|
||||
+12
@@ -15,6 +15,18 @@
|
||||
@property (nonatomic, weak) id <FLEXExplorerViewControllerDelegate> delegate;
|
||||
|
||||
- (BOOL)shouldReceiveTouchAtWindowPoint:(CGPoint)pointInWindowCoordinates;
|
||||
- (BOOL)wantsWindowToBecomeKey;
|
||||
|
||||
// Keyboard shortcut helpers
|
||||
|
||||
- (void)toggleSelectTool;
|
||||
- (void)toggleMoveTool;
|
||||
- (void)toggleViewsTool;
|
||||
- (void)toggleMenuTool;
|
||||
- (void)handleDownArrowKeyPressed;
|
||||
- (void)handleUpArrowKeyPressed;
|
||||
- (void)handleRightArrowKeyPressed;
|
||||
- (void)handleLeftArrowKeyPressed;
|
||||
|
||||
@end
|
||||
|
||||
+172
-116
@@ -14,6 +14,7 @@
|
||||
#import "FLEXGlobalsTableViewController.h"
|
||||
#import "FLEXObjectExplorerViewController.h"
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
#import "FLEXNetworkHistoryTableViewController.h"
|
||||
|
||||
typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
FLEXExplorerModeDefault,
|
||||
@@ -120,75 +121,27 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Status Bar Wrangling for iOS 7
|
||||
|
||||
// Try to get the preferred status bar properties from the app's root view controller (not us).
|
||||
// In general, our window shouldn't be the key window when this view controller is asked about the status bar.
|
||||
// However, we guard against infinite recursion and provide a reasonable default for status bar behavior in case our window is the keyWindow.
|
||||
|
||||
- (UIViewController *)viewControllerForStatusBarAndOrientationProperties
|
||||
{
|
||||
UIViewController *viewControllerToAsk = [[[UIApplication sharedApplication] keyWindow] rootViewController];
|
||||
|
||||
// On iPhone, modal view controllers get asked
|
||||
if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPhone) {
|
||||
while (viewControllerToAsk.presentedViewController) {
|
||||
viewControllerToAsk = viewControllerToAsk.presentedViewController;
|
||||
}
|
||||
}
|
||||
|
||||
return viewControllerToAsk;
|
||||
}
|
||||
|
||||
- (UIStatusBarStyle)preferredStatusBarStyle
|
||||
{
|
||||
UIViewController *viewControllerToAsk = [self viewControllerForStatusBarAndOrientationProperties];
|
||||
UIStatusBarStyle preferredStyle = UIStatusBarStyleDefault;
|
||||
if (viewControllerToAsk && viewControllerToAsk != self) {
|
||||
// We might need to foward to a child
|
||||
UIViewController *childViewControllerToAsk = [viewControllerToAsk childViewControllerForStatusBarStyle];
|
||||
if (childViewControllerToAsk) {
|
||||
preferredStyle = [childViewControllerToAsk preferredStatusBarStyle];
|
||||
} else {
|
||||
preferredStyle = [viewControllerToAsk preferredStatusBarStyle];
|
||||
}
|
||||
}
|
||||
return preferredStyle;
|
||||
}
|
||||
|
||||
- (UIStatusBarAnimation)preferredStatusBarUpdateAnimation
|
||||
{
|
||||
UIViewController *viewControllerToAsk = [self viewControllerForStatusBarAndOrientationProperties];
|
||||
UIStatusBarAnimation preferredAnimation = UIStatusBarAnimationFade;
|
||||
if (viewControllerToAsk && viewControllerToAsk != self) {
|
||||
preferredAnimation = [viewControllerToAsk preferredStatusBarUpdateAnimation];
|
||||
}
|
||||
return preferredAnimation;
|
||||
}
|
||||
|
||||
- (BOOL)prefersStatusBarHidden
|
||||
{
|
||||
UIViewController *viewControllerToAsk = [self viewControllerForStatusBarAndOrientationProperties];
|
||||
BOOL prefersHidden = NO;
|
||||
if (viewControllerToAsk && viewControllerToAsk != self) {
|
||||
// Again, we might need to forward to a child
|
||||
UIViewController *childViewControllerToAsk = [viewControllerToAsk childViewControllerForStatusBarHidden];
|
||||
if (childViewControllerToAsk) {
|
||||
prefersHidden = [childViewControllerToAsk prefersStatusBarHidden];
|
||||
} else {
|
||||
prefersHidden = [viewControllerToAsk prefersStatusBarHidden];
|
||||
}
|
||||
}
|
||||
return prefersHidden;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Rotation
|
||||
|
||||
- (NSUInteger)supportedInterfaceOrientations
|
||||
- (UIViewController *)viewControllerForRotationAndOrientation
|
||||
{
|
||||
UIViewController *viewControllerToAsk = [self viewControllerForStatusBarAndOrientationProperties];
|
||||
NSUInteger supportedOrientations = [FLEXUtility infoPlistSupportedInterfaceOrientationsMask];
|
||||
UIWindow *window = self.previousKeyWindow ?: [[UIApplication sharedApplication] keyWindow];
|
||||
UIViewController *viewController = window.rootViewController;
|
||||
NSString *viewControllerSelectorString = [@[@"_vie", @"wContro", @"llerFor", @"Supported", @"Interface", @"Orientations"] componentsJoinedByString:@""];
|
||||
SEL viewControllerSelector = NSSelectorFromString(viewControllerSelectorString);
|
||||
if ([viewController respondsToSelector:viewControllerSelector]) {
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
|
||||
viewController = [viewController performSelector:viewControllerSelector];
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
return viewController;
|
||||
}
|
||||
|
||||
- (UIInterfaceOrientationMask)supportedInterfaceOrientations
|
||||
{
|
||||
UIViewController *viewControllerToAsk = [self viewControllerForRotationAndOrientation];
|
||||
UIInterfaceOrientationMask supportedOrientations = [FLEXUtility infoPlistSupportedInterfaceOrientationsMask];
|
||||
if (viewControllerToAsk && viewControllerToAsk != self) {
|
||||
supportedOrientations = [viewControllerToAsk supportedInterfaceOrientations];
|
||||
}
|
||||
@@ -204,7 +157,7 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
|
||||
- (BOOL)shouldAutorotate
|
||||
{
|
||||
UIViewController *viewControllerToAsk = [self viewControllerForStatusBarAndOrientationProperties];
|
||||
UIViewController *viewControllerToAsk = [self viewControllerForRotationAndOrientation];
|
||||
BOOL shouldAutorotate = YES;
|
||||
if (viewControllerToAsk && viewControllerToAsk != self) {
|
||||
shouldAutorotate = [viewControllerToAsk shouldAutorotate];
|
||||
@@ -378,9 +331,9 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
{
|
||||
NSUInteger indexOfView = [self.viewsAtTapPoint indexOfObject:object];
|
||||
if (indexOfView != NSNotFound) {
|
||||
UIView *view = [self.viewsAtTapPoint objectAtIndex:indexOfView];
|
||||
UIView *view = self.viewsAtTapPoint[indexOfView];
|
||||
NSValue *key = [NSValue valueWithNonretainedObject:view];
|
||||
UIView *outline = [self.outlineViewsForVisibleViews objectForKey:key];
|
||||
UIView *outline = self.outlineViewsForVisibleViews[key];
|
||||
if (outline) {
|
||||
outline.frame = [self frameInLocalCoordinatesForView:view];
|
||||
}
|
||||
@@ -415,21 +368,12 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
|
||||
- (void)selectButtonTapped:(FLEXToolbarItem *)sender
|
||||
{
|
||||
if (self.currentMode == FLEXExplorerModeSelect) {
|
||||
self.currentMode = FLEXExplorerModeDefault;
|
||||
} else {
|
||||
self.currentMode = FLEXExplorerModeSelect;
|
||||
}
|
||||
[self toggleSelectTool];
|
||||
}
|
||||
|
||||
- (void)hierarchyButtonTapped:(FLEXToolbarItem *)sender
|
||||
{
|
||||
NSArray *allViews = [self allViewsInHierarchy];
|
||||
NSDictionary *depthsForViews = [self hierarchyDepthsForViews:allViews];
|
||||
FLEXHierarchyTableViewController *hierarchyTVC = [[FLEXHierarchyTableViewController alloc] initWithViews:allViews viewsAtTap:self.viewsAtTapPoint selectedView:self.selectedView depths:depthsForViews];
|
||||
hierarchyTVC.delegate = self;
|
||||
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:hierarchyTVC];
|
||||
[self makeKeyAndPresentViewController:navigationController animated:YES completion:nil];
|
||||
[self toggleViewsTool];
|
||||
}
|
||||
|
||||
- (NSArray *)allViewsInHierarchy
|
||||
@@ -447,20 +391,23 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
|
||||
- (NSArray *)allWindows
|
||||
{
|
||||
NSMutableArray *windows = [[[UIApplication sharedApplication] windows] mutableCopy];
|
||||
UIWindow *statusWindow = [self statusWindow];
|
||||
if (statusWindow) {
|
||||
// The windows are ordered back to front, so default to inserting the status bar at the end.
|
||||
// However, it there are windows at status bar level, insert the status bar before them.
|
||||
NSInteger insertionIndex = [windows count];
|
||||
for (UIWindow *window in windows) {
|
||||
if (window.windowLevel >= UIWindowLevelStatusBar) {
|
||||
insertionIndex = [windows indexOfObject:window];
|
||||
break;
|
||||
}
|
||||
}
|
||||
[windows insertObject:statusWindow atIndex:insertionIndex];
|
||||
}
|
||||
BOOL includeInternalWindows = YES;
|
||||
BOOL onlyVisibleWindows = NO;
|
||||
|
||||
NSArray *allWindowsComponents = @[@"al", @"lWindo", @"wsIncl", @"udingInt", @"ernalWin", @"dows:o", @"nlyVisi", @"bleWin", @"dows:"];
|
||||
SEL allWindowsSelector = NSSelectorFromString([allWindowsComponents componentsJoinedByString:@""]);
|
||||
|
||||
NSMethodSignature *methodSignature = [[UIWindow class] methodSignatureForSelector:allWindowsSelector];
|
||||
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
|
||||
|
||||
invocation.target = [UIWindow class];
|
||||
invocation.selector = allWindowsSelector;
|
||||
[invocation setArgument:&includeInternalWindows atIndex:2];
|
||||
[invocation setArgument:&onlyVisibleWindows atIndex:3];
|
||||
[invocation invoke];
|
||||
|
||||
__unsafe_unretained NSArray *windows = nil;
|
||||
[invocation getReturnValue:&windows];
|
||||
return windows;
|
||||
}
|
||||
|
||||
@@ -472,20 +419,12 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
|
||||
- (void)moveButtonTapped:(FLEXToolbarItem *)sender
|
||||
{
|
||||
if (self.currentMode == FLEXExplorerModeMove) {
|
||||
self.currentMode = FLEXExplorerModeDefault;
|
||||
} else {
|
||||
self.currentMode = FLEXExplorerModeMove;
|
||||
}
|
||||
[self toggleMoveTool];
|
||||
}
|
||||
|
||||
- (void)globalsButtonTapped:(FLEXToolbarItem *)sender
|
||||
{
|
||||
FLEXGlobalsTableViewController *globalsViewController = [[FLEXGlobalsTableViewController alloc] init];
|
||||
globalsViewController.delegate = self;
|
||||
[FLEXGlobalsTableViewController setApplicationWindow:[[UIApplication sharedApplication] keyWindow]];
|
||||
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:globalsViewController];
|
||||
[self makeKeyAndPresentViewController:navigationController animated:YES completion:nil];
|
||||
[self toggleMenuTool];
|
||||
}
|
||||
|
||||
- (void)closeButtonTapped:(FLEXToolbarItem *)sender
|
||||
@@ -592,7 +531,11 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
{
|
||||
// Only if we're in selection mode
|
||||
if (self.currentMode == FLEXExplorerModeSelect && tapGR.state == UIGestureRecognizerStateRecognized) {
|
||||
[self updateOutlineViewsForSelectionPoint:[tapGR locationInView:nil]];
|
||||
// Note that [tapGR locationInView:nil] is broken in iOS 8, so we have to do a two step conversion to window coordinates.
|
||||
// Thanks to @lascorbe for finding this: https://github.com/Flipboard/FLEX/pull/31
|
||||
CGPoint tapPointInView = [tapGR locationInView:self.view];
|
||||
CGPoint tapPointInWindow = [self.view convertPoint:tapPointInView toView:nil];
|
||||
[self updateOutlineViewsForSelectionPoint:tapPointInWindow];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -842,11 +785,8 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
// 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
|
||||
// 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.
|
||||
[self presentViewController:viewController animated:animated completion:completion];
|
||||
@@ -854,21 +794,137 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
|
||||
- (void)resignKeyAndDismissViewControllerAnimated:(BOOL)animated completion:(void (^)(void))completion
|
||||
{
|
||||
[self.previousKeyWindow makeKeyWindow];
|
||||
|
||||
UIWindow *previousKeyWindow = self.previousKeyWindow;
|
||||
self.previousKeyWindow = nil;
|
||||
[previousKeyWindow makeKeyWindow];
|
||||
[[previousKeyWindow rootViewController] setNeedsStatusBarAppearanceUpdate];
|
||||
|
||||
// Restore the status bar window's normal window level.
|
||||
// We want it above FLEX while a modal is presented for scroll to top, but below FLEX otherwise for exploration.
|
||||
[[self statusWindow] setWindowLevel:UIWindowLevelStatusBar];
|
||||
|
||||
// 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];
|
||||
}
|
||||
|
||||
- (BOOL)wantsWindowToBecomeKey
|
||||
{
|
||||
return self.previousKeyWindow != nil;
|
||||
}
|
||||
|
||||
#pragma mark - Keyboard Shortcut Helpers
|
||||
|
||||
- (void)toggleSelectTool
|
||||
{
|
||||
if (self.currentMode == FLEXExplorerModeSelect) {
|
||||
self.currentMode = FLEXExplorerModeDefault;
|
||||
} else {
|
||||
self.currentMode = FLEXExplorerModeSelect;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)toggleMoveTool
|
||||
{
|
||||
if (self.currentMode == FLEXExplorerModeMove) {
|
||||
self.currentMode = FLEXExplorerModeDefault;
|
||||
} else {
|
||||
self.currentMode = FLEXExplorerModeMove;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)toggleViewsTool
|
||||
{
|
||||
BOOL viewsModalShown = [[self presentedViewController] isKindOfClass:[UINavigationController class]];
|
||||
viewsModalShown = viewsModalShown && [[[(UINavigationController *)[self presentedViewController] viewControllers] firstObject] isKindOfClass:[FLEXHierarchyTableViewController class]];
|
||||
if (viewsModalShown) {
|
||||
[self resignKeyAndDismissViewControllerAnimated:YES completion:nil];
|
||||
} else {
|
||||
void (^presentBlock)() = ^{
|
||||
NSArray *allViews = [self allViewsInHierarchy];
|
||||
NSDictionary *depthsForViews = [self hierarchyDepthsForViews:allViews];
|
||||
FLEXHierarchyTableViewController *hierarchyTVC = [[FLEXHierarchyTableViewController alloc] initWithViews:allViews viewsAtTap:self.viewsAtTapPoint selectedView:self.selectedView depths:depthsForViews];
|
||||
hierarchyTVC.delegate = self;
|
||||
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:hierarchyTVC];
|
||||
[self makeKeyAndPresentViewController:navigationController animated:YES completion:nil];
|
||||
};
|
||||
|
||||
if (self.presentedViewController) {
|
||||
[self resignKeyAndDismissViewControllerAnimated:NO completion:presentBlock];
|
||||
} else {
|
||||
presentBlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)toggleMenuTool
|
||||
{
|
||||
BOOL menuModalShown = [[self presentedViewController] isKindOfClass:[UINavigationController class]];
|
||||
menuModalShown = menuModalShown && [[[(UINavigationController *)[self presentedViewController] viewControllers] firstObject] isKindOfClass:[FLEXGlobalsTableViewController class]];
|
||||
if (menuModalShown) {
|
||||
[self resignKeyAndDismissViewControllerAnimated:YES completion:nil];
|
||||
} else {
|
||||
void (^presentBlock)() = ^{
|
||||
FLEXGlobalsTableViewController *globalsViewController = [[FLEXGlobalsTableViewController alloc] init];
|
||||
globalsViewController.delegate = self;
|
||||
[FLEXGlobalsTableViewController setApplicationWindow:[[UIApplication sharedApplication] keyWindow]];
|
||||
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:globalsViewController];
|
||||
[self makeKeyAndPresentViewController:navigationController animated:YES completion:nil];
|
||||
};
|
||||
|
||||
if (self.presentedViewController) {
|
||||
[self resignKeyAndDismissViewControllerAnimated:NO completion:presentBlock];
|
||||
} else {
|
||||
presentBlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handleDownArrowKeyPressed
|
||||
{
|
||||
if (self.currentMode == FLEXExplorerModeMove) {
|
||||
CGRect frame = self.selectedView.frame;
|
||||
frame.origin.y += 1.0 / [[UIScreen mainScreen] scale];
|
||||
self.selectedView.frame = frame;
|
||||
} else if (self.currentMode == FLEXExplorerModeSelect && [self.viewsAtTapPoint count] > 0) {
|
||||
NSInteger selectedViewIndex = [self.viewsAtTapPoint indexOfObject:self.selectedView];
|
||||
if (selectedViewIndex > 0) {
|
||||
self.selectedView = [self.viewsAtTapPoint objectAtIndex:selectedViewIndex - 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handleUpArrowKeyPressed
|
||||
{
|
||||
if (self.currentMode == FLEXExplorerModeMove) {
|
||||
CGRect frame = self.selectedView.frame;
|
||||
frame.origin.y -= 1.0 / [[UIScreen mainScreen] scale];
|
||||
self.selectedView.frame = frame;
|
||||
} else if (self.currentMode == FLEXExplorerModeSelect && [self.viewsAtTapPoint count] > 0) {
|
||||
NSInteger selectedViewIndex = [self.viewsAtTapPoint indexOfObject:self.selectedView];
|
||||
if (selectedViewIndex < [self.viewsAtTapPoint count] - 1) {
|
||||
self.selectedView = [self.viewsAtTapPoint objectAtIndex:selectedViewIndex + 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handleRightArrowKeyPressed
|
||||
{
|
||||
if (self.currentMode == FLEXExplorerModeMove) {
|
||||
CGRect frame = self.selectedView.frame;
|
||||
frame.origin.x += 1.0 / [[UIScreen mainScreen] scale];
|
||||
self.selectedView.frame = frame;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handleLeftArrowKeyPressed
|
||||
{
|
||||
if (self.currentMode == FLEXExplorerModeMove) {
|
||||
CGRect frame = self.selectedView.frame;
|
||||
frame.origin.x -= 1.0 / [[UIScreen mainScreen] scale];
|
||||
self.selectedView.frame = frame;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,70 @@
|
||||
//
|
||||
// FLEXManager.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 4/4/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface FLEXManager : NSObject
|
||||
|
||||
+ (instancetype)sharedManager;
|
||||
|
||||
@property (nonatomic, readonly) BOOL isHidden;
|
||||
|
||||
- (void)showExplorer;
|
||||
- (void)hideExplorer;
|
||||
- (void)toggleExplorer;
|
||||
|
||||
#pragma mark - Network Debugging
|
||||
|
||||
/// 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 25 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 - Keyboard Shortcuts
|
||||
|
||||
/// Simulator keyboard shortcuts are enabled by default.
|
||||
/// The shortcuts will not fire when there is an active text field, text view, or other responder accepting key input.
|
||||
/// You can disable keyboard shortcuts if you have existing keyboard shortcuts that conflict with FLEX, or if you like doing things the hard way ;)
|
||||
/// Keyboard shortcuts are always disabled (and support is compiled out) in non-simulator builds
|
||||
@property (nonatomic, assign) BOOL simulatorShortcutsEnabled;
|
||||
|
||||
/// Adds an action to run when the specified key & modifier combination is pressed
|
||||
/// @param key A single character string matching a key on the keyboard
|
||||
/// @param modifiers Modifier keys such as shift, command, or alt/option
|
||||
/// @param action The block to run on the main thread when the key & modifier combination is recognized.
|
||||
/// @param description Shown the the keyboard shortcut help menu, which is accessed via the '?' key.
|
||||
/// @note The action block will be retained for the duration of the application. You may want to use weak references.
|
||||
/// @note FLEX registers several default keyboard shortcuts. Use the '?' key to see a list of shortcuts.
|
||||
- (void)registerSimulatorShortcutWithKey:(NSString *)key modifiers:(UIKeyModifierFlags)modifiers action:(dispatch_block_t)action description:(NSString *)description;
|
||||
|
||||
#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.
|
||||
/// @param entryName The string to be displayed in the cell.
|
||||
/// @param objectFutureBlock When you tap on the row, information about the object returned by this block will be displayed.
|
||||
/// Passing a block that returns an object allows you to display information about an object whose actual pointer may change at runtime (e.g. +currentUser)
|
||||
/// @note This method must be called from the main thread.
|
||||
/// The objectFutureBlock will be invoked from the main thread and may return nil.
|
||||
/// @note The passed block will be copied and retain for the duration of the application, you may want to use __weak references.
|
||||
- (void)registerGlobalEntryWithName:(NSString *)entryName objectFutureBlock:(id (^)(void))objectFutureBlock;
|
||||
|
||||
/// Adds an entry at the bottom of the list of Global State items. Call this method before this view controller is displayed.
|
||||
/// @param entryName The string to be displayed in the cell.
|
||||
/// @param viewControllerFutureBlock When you tap on the row, view controller returned by this block will be pushed on the navigation controller stack.
|
||||
/// @note This method must be called from the main thread.
|
||||
/// The viewControllerFutureBlock will be invoked from the main thread and may not return nil.
|
||||
/// @note The passed block will be copied and retain for the duration of the application, you may want to use __weak references.
|
||||
- (void)registerGlobalEntryWithName:(NSString *)entryName
|
||||
viewControllerFutureBlock:(UIViewController * (^)(void))viewControllerFutureBlock;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,360 @@
|
||||
//
|
||||
// FLEXManager.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 4/4/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXManager.h"
|
||||
#import "FLEXExplorerViewController.h"
|
||||
#import "FLEXWindow.h"
|
||||
#import "FLEXGlobalsTableViewControllerEntry.h"
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
#import "FLEXObjectExplorerViewController.h"
|
||||
#import "FLEXNetworkObserver.h"
|
||||
#import "FLEXNetworkRecorder.h"
|
||||
#import "FLEXKeyboardShortcutManager.h"
|
||||
#import "FLEXFileBrowserTableViewController.h"
|
||||
#import "FLEXNetworkHistoryTableViewController.h"
|
||||
#import "FLEXKeyboardHelpViewController.h"
|
||||
|
||||
@interface FLEXManager () <FLEXWindowEventDelegate, FLEXExplorerViewControllerDelegate>
|
||||
|
||||
@property (nonatomic, strong) FLEXWindow *explorerWindow;
|
||||
@property (nonatomic, strong) FLEXExplorerViewController *explorerViewController;
|
||||
|
||||
@property (nonatomic, readonly, strong) NSMutableArray *userGlobalEntries;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXManager
|
||||
|
||||
+ (instancetype)sharedManager
|
||||
{
|
||||
static FLEXManager *sharedManager = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
sharedManager = [[[self class] alloc] init];
|
||||
});
|
||||
return sharedManager;
|
||||
}
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_userGlobalEntries = [[NSMutableArray alloc] init];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (FLEXWindow *)explorerWindow
|
||||
{
|
||||
NSAssert([NSThread isMainThread], @"You must use %@ from the main thread only.", NSStringFromClass([self class]));
|
||||
|
||||
if (!_explorerWindow) {
|
||||
_explorerWindow = [[FLEXWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];
|
||||
_explorerWindow.eventDelegate = self;
|
||||
_explorerWindow.rootViewController = self.explorerViewController;
|
||||
}
|
||||
|
||||
return _explorerWindow;
|
||||
}
|
||||
|
||||
- (FLEXExplorerViewController *)explorerViewController
|
||||
{
|
||||
if (!_explorerViewController) {
|
||||
_explorerViewController = [[FLEXExplorerViewController alloc] init];
|
||||
_explorerViewController.delegate = self;
|
||||
}
|
||||
|
||||
return _explorerViewController;
|
||||
}
|
||||
|
||||
- (void)showExplorer
|
||||
{
|
||||
self.explorerWindow.hidden = NO;
|
||||
}
|
||||
|
||||
- (void)hideExplorer
|
||||
{
|
||||
self.explorerWindow.hidden = YES;
|
||||
}
|
||||
|
||||
- (void)toggleExplorer {
|
||||
if (self.explorerWindow.isHidden) {
|
||||
[self showExplorer];
|
||||
} else {
|
||||
[self hideExplorer];
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)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
|
||||
|
||||
- (BOOL)shouldHandleTouchAtPoint:(CGPoint)pointInWindow
|
||||
{
|
||||
// Ask the explorer view controller
|
||||
return [self.explorerViewController shouldReceiveTouchAtWindowPoint:pointInWindow];
|
||||
}
|
||||
|
||||
- (BOOL)canBecomeKeyWindow
|
||||
{
|
||||
// Only when the explorer view controller wants it because it needs to accept key input & affect the status bar.
|
||||
return [self.explorerViewController wantsWindowToBecomeKey];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - FLEXExplorerViewControllerDelegate
|
||||
|
||||
- (void)explorerViewControllerDidFinish:(FLEXExplorerViewController *)explorerViewController
|
||||
{
|
||||
[self hideExplorer];
|
||||
}
|
||||
|
||||
#pragma mark - Simulator Shortcuts
|
||||
|
||||
- (void)registerSimulatorShortcutWithKey:(NSString *)key modifiers:(UIKeyModifierFlags)modifiers action:(dispatch_block_t)action description:(NSString *)description
|
||||
{
|
||||
# if TARGET_OS_SIMULATOR
|
||||
[[FLEXKeyboardShortcutManager sharedManager] registerSimulatorShortcutWithKey:key modifiers:modifiers action:action description:description];
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)setSimulatorShortcutsEnabled:(BOOL)simulatorShortcutsEnabled
|
||||
{
|
||||
# if TARGET_OS_SIMULATOR
|
||||
[[FLEXKeyboardShortcutManager sharedManager] setEnabled:simulatorShortcutsEnabled];
|
||||
#endif
|
||||
}
|
||||
|
||||
- (BOOL)simulatorShortcutsEnabled
|
||||
{
|
||||
# if TARGET_OS_SIMULATOR
|
||||
return [[FLEXKeyboardShortcutManager sharedManager] isEnabled];
|
||||
#else
|
||||
return NO;
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)registerDefaultSimulatorShortcuts
|
||||
{
|
||||
[self registerSimulatorShortcutWithKey:@"f" modifiers:0 action:^{
|
||||
[self toggleExplorer];
|
||||
} description:@"Toggle FLEX toolbar"];
|
||||
|
||||
[self registerSimulatorShortcutWithKey:@"g" modifiers:0 action:^{
|
||||
[self showExplorerIfNeeded];
|
||||
[self.explorerViewController toggleMenuTool];
|
||||
} description:@"Toggle FLEX globals menu"];
|
||||
|
||||
[self registerSimulatorShortcutWithKey:@"v" modifiers:0 action:^{
|
||||
[self showExplorerIfNeeded];
|
||||
[self.explorerViewController toggleViewsTool];
|
||||
} description:@"Toggle view hierarchy menu"];
|
||||
|
||||
[self registerSimulatorShortcutWithKey:@"s" modifiers:0 action:^{
|
||||
[self showExplorerIfNeeded];
|
||||
[self.explorerViewController toggleSelectTool];
|
||||
} description:@"Toggle select tool"];
|
||||
|
||||
[self registerSimulatorShortcutWithKey:@"m" modifiers:0 action:^{
|
||||
[self showExplorerIfNeeded];
|
||||
[self.explorerViewController toggleMoveTool];
|
||||
} description:@"Toggle move tool"];
|
||||
|
||||
[self registerSimulatorShortcutWithKey:@"n" modifiers:0 action:^{
|
||||
[self toggleTopViewControllerOfClass:[FLEXNetworkHistoryTableViewController class]];
|
||||
} description:@"Toggle network history view"];
|
||||
|
||||
[self registerSimulatorShortcutWithKey:UIKeyInputDownArrow modifiers:0 action:^{
|
||||
if ([self isHidden]) {
|
||||
[self tryScrollDown];
|
||||
} else {
|
||||
[self.explorerViewController handleDownArrowKeyPressed];
|
||||
}
|
||||
} description:@"Cycle view selection\n\t\tMove view down\n\t\tScroll down"];
|
||||
|
||||
[self registerSimulatorShortcutWithKey:UIKeyInputUpArrow modifiers:0 action:^{
|
||||
if ([self isHidden]) {
|
||||
[self tryScrollUp];
|
||||
} else {
|
||||
[self.explorerViewController handleUpArrowKeyPressed];
|
||||
}
|
||||
} description:@"Cycle view selection\n\t\tMove view up\n\t\tScroll up"];
|
||||
|
||||
[self registerSimulatorShortcutWithKey:UIKeyInputRightArrow modifiers:0 action:^{
|
||||
if (![self isHidden]) {
|
||||
[self.explorerViewController handleRightArrowKeyPressed];
|
||||
}
|
||||
} description:@"Move selected view right"];
|
||||
|
||||
[self registerSimulatorShortcutWithKey:UIKeyInputLeftArrow modifiers:0 action:^{
|
||||
if ([self isHidden]) {
|
||||
[self tryGoBack];
|
||||
} else {
|
||||
[self.explorerViewController handleLeftArrowKeyPressed];
|
||||
}
|
||||
} description:@"Move selected view left"];
|
||||
|
||||
[self registerSimulatorShortcutWithKey:@"?" modifiers:0 action:^{
|
||||
[self toggleTopViewControllerOfClass:[FLEXKeyboardHelpViewController class]];
|
||||
} description:@"Toggle (this) help menu"];
|
||||
|
||||
[self registerSimulatorShortcutWithKey:UIKeyInputEscape modifiers:0 action:^{
|
||||
[[[self topViewController] presentingViewController] dismissViewControllerAnimated:YES completion:nil];
|
||||
} description:@"End editing text\n\t\tDismiss top view controller"];
|
||||
|
||||
[self registerSimulatorShortcutWithKey:@"o" modifiers:UIKeyModifierCommand|UIKeyModifierShift action:^{
|
||||
[self toggleTopViewControllerOfClass:[FLEXFileBrowserTableViewController class]];
|
||||
} description:@"Toggle file browser menu"];
|
||||
}
|
||||
|
||||
+ (void)load
|
||||
{
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[[[self class] sharedManager] registerDefaultSimulatorShortcuts];
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark - Extensions
|
||||
|
||||
- (void)registerGlobalEntryWithName:(NSString *)entryName objectFutureBlock:(id (^)(void))objectFutureBlock
|
||||
{
|
||||
NSParameterAssert(entryName);
|
||||
NSParameterAssert(objectFutureBlock);
|
||||
NSAssert([NSThread isMainThread], @"This method must be called from the main thread.");
|
||||
|
||||
entryName = entryName.copy;
|
||||
FLEXGlobalsTableViewControllerEntry *entry = [FLEXGlobalsTableViewControllerEntry entryWithNameFuture:^NSString *{
|
||||
return entryName;
|
||||
} viewControllerFuture:^UIViewController *{
|
||||
return [FLEXObjectExplorerFactory explorerViewControllerForObject:objectFutureBlock()];
|
||||
}];
|
||||
|
||||
[self.userGlobalEntries addObject:entry];
|
||||
}
|
||||
|
||||
- (void)registerGlobalEntryWithName:(NSString *)entryName viewControllerFutureBlock:(UIViewController * (^)(void))viewControllerFutureBlock
|
||||
{
|
||||
NSParameterAssert(entryName);
|
||||
NSParameterAssert(viewControllerFutureBlock);
|
||||
NSAssert([NSThread isMainThread], @"This method must be called from the main thread.");
|
||||
|
||||
entryName = entryName.copy;
|
||||
FLEXGlobalsTableViewControllerEntry *entry = [FLEXGlobalsTableViewControllerEntry entryWithNameFuture:^NSString *{
|
||||
return entryName;
|
||||
} viewControllerFuture:^UIViewController *{
|
||||
UIViewController *viewController = viewControllerFutureBlock();
|
||||
NSCAssert(viewController, @"'%@' entry returned nil viewController. viewControllerFutureBlock should never return nil.", entryName);
|
||||
return viewController;
|
||||
}];
|
||||
|
||||
[self.userGlobalEntries addObject:entry];
|
||||
}
|
||||
|
||||
- (void)tryScrollDown
|
||||
{
|
||||
UIScrollView *firstScrollView = [self firstScrollView];
|
||||
CGPoint contentOffset = [firstScrollView contentOffset];
|
||||
CGFloat distance = floor(firstScrollView.frame.size.height / 2.0);
|
||||
CGFloat maxContentOffsetY = firstScrollView.contentSize.height + firstScrollView.contentInset.bottom - firstScrollView.frame.size.height;
|
||||
distance = MIN(maxContentOffsetY - firstScrollView.contentOffset.y, distance);
|
||||
contentOffset.y += distance;
|
||||
[firstScrollView setContentOffset:contentOffset animated:YES];
|
||||
}
|
||||
|
||||
- (void)tryScrollUp
|
||||
{
|
||||
UIScrollView *firstScrollView = [self firstScrollView];
|
||||
CGPoint contentOffset = [firstScrollView contentOffset];
|
||||
CGFloat distance = floor(firstScrollView.frame.size.height / 2.0);
|
||||
CGFloat minContentOffsetY = -firstScrollView.contentInset.top;
|
||||
distance = MIN(firstScrollView.contentOffset.y - minContentOffsetY, distance);
|
||||
contentOffset.y -= distance;
|
||||
[firstScrollView setContentOffset:contentOffset animated:YES];
|
||||
}
|
||||
|
||||
- (UIScrollView *)firstScrollView
|
||||
{
|
||||
NSMutableArray *views = [[[[UIApplication sharedApplication] keyWindow] subviews] mutableCopy];
|
||||
UIScrollView *scrollView = nil;
|
||||
while ([views count] > 0) {
|
||||
UIView *view = [views firstObject];
|
||||
[views removeObjectAtIndex:0];
|
||||
if ([view isKindOfClass:[UIScrollView class]]) {
|
||||
scrollView = (UIScrollView *)view;
|
||||
break;
|
||||
} else {
|
||||
[views addObjectsFromArray:[view subviews]];
|
||||
}
|
||||
}
|
||||
return scrollView;
|
||||
}
|
||||
|
||||
- (void)tryGoBack
|
||||
{
|
||||
UINavigationController *navigationController = nil;
|
||||
UIViewController *topViewController = [self topViewController];
|
||||
if ([topViewController isKindOfClass:[UINavigationController class]]) {
|
||||
navigationController = (UINavigationController *)topViewController;
|
||||
} else {
|
||||
navigationController = topViewController.navigationController;
|
||||
}
|
||||
[navigationController popViewControllerAnimated:YES];
|
||||
}
|
||||
|
||||
- (UIViewController *)topViewController
|
||||
{
|
||||
UIViewController *topViewController = [[[UIApplication sharedApplication] keyWindow] rootViewController];
|
||||
while ([topViewController presentedViewController]) {
|
||||
topViewController = [topViewController presentedViewController];
|
||||
}
|
||||
return topViewController;
|
||||
}
|
||||
|
||||
- (void)toggleTopViewControllerOfClass:(Class)class
|
||||
{
|
||||
UIViewController *topViewController = [self topViewController];
|
||||
if ([topViewController isKindOfClass:[UINavigationController class]] && [[[(UINavigationController *)topViewController viewControllers] firstObject] isKindOfClass:[class class]]) {
|
||||
[[topViewController presentingViewController] dismissViewControllerAnimated:YES completion:nil];
|
||||
} else {
|
||||
id viewController = [[class alloc] init];
|
||||
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:viewController];
|
||||
[topViewController presentViewController:navigationController animated:YES completion:nil];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)showExplorerIfNeeded
|
||||
{
|
||||
if ([self isHidden]) {
|
||||
[self showExplorer];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -19,5 +19,6 @@
|
||||
@protocol FLEXWindowEventDelegate <NSObject>
|
||||
|
||||
- (BOOL)shouldHandleTouchAtPoint:(CGPoint)pointInWindow;
|
||||
- (BOOL)canBecomeKeyWindow;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,67 @@
|
||||
//
|
||||
// FLEXWindow.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 4/13/14.
|
||||
// Copyright (c) 2014 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXWindow.h"
|
||||
#import <objc/runtime.h>
|
||||
|
||||
@implementation FLEXWindow
|
||||
|
||||
- (id)initWithFrame:(CGRect)frame
|
||||
{
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
self.backgroundColor = [UIColor clearColor];
|
||||
// Some apps have windows at UIWindowLevelStatusBar + n.
|
||||
// If we make the window level too high, we block out UIAlertViews.
|
||||
// There's a balance between staying above the app's windows and staying below alerts.
|
||||
// UIWindowLevelStatusBar + 100 seems to hit that balance.
|
||||
self.windowLevel = UIWindowLevelStatusBar + 100.0;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
|
||||
{
|
||||
BOOL pointInside = NO;
|
||||
if ([self.eventDelegate shouldHandleTouchAtPoint:point]) {
|
||||
pointInside = [super pointInside:point withEvent:event];
|
||||
}
|
||||
return pointInside;
|
||||
}
|
||||
|
||||
- (BOOL)shouldAffectStatusBarAppearance
|
||||
{
|
||||
return [self isKeyWindow];
|
||||
}
|
||||
|
||||
- (BOOL)canBecomeKeyWindow
|
||||
{
|
||||
return [self.eventDelegate canBecomeKeyWindow];
|
||||
}
|
||||
|
||||
+ (void)initialize
|
||||
{
|
||||
// This adds a method (superclass override) at runtime which gives us the status bar behavior we want.
|
||||
// The FLEX window is intended to be an overlay that generally doesn't affect the app underneath.
|
||||
// Most of the time, we want the app's main window(s) to be in control of status bar behavior.
|
||||
// Done at runtime with an obfuscated selector because it is private API. But you shoudn't ship this to the App Store anyways...
|
||||
NSString *canAffectSelectorString = [@[@"_can", @"Affect", @"Status", @"Bar", @"Appearance"] componentsJoinedByString:@""];
|
||||
SEL canAffectSelector = NSSelectorFromString(canAffectSelectorString);
|
||||
Method shouldAffectMethod = class_getInstanceMethod(self, @selector(shouldAffectStatusBarAppearance));
|
||||
IMP canAffectImplementation = method_getImplementation(shouldAffectMethod);
|
||||
class_addMethod(self, canAffectSelector, canAffectImplementation, method_getTypeEncoding(shouldAffectMethod));
|
||||
|
||||
// One more...
|
||||
NSString *canBecomeKeySelectorString = [NSString stringWithFormat:@"_%@", NSStringFromSelector(@selector(canBecomeKeyWindow))];
|
||||
SEL canBecomeKeySelector = NSSelectorFromString(canBecomeKeySelectorString);
|
||||
Method canBecomeKeyMethod = class_getInstanceMethod(self, @selector(canBecomeKeyWindow));
|
||||
IMP canBecomeKeyImplementation = method_getImplementation(canBecomeKeyMethod);
|
||||
class_addMethod(self, canBecomeKeySelector, canBecomeKeyImplementation, method_getTypeEncoding(canBecomeKeyMethod));
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// FLEX.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Eric Horacek on 7/18/15.
|
||||
// Copyright (c) 2015 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
//! Project version number for FLEX.
|
||||
FOUNDATION_EXPORT double FLEXVersionNumber;
|
||||
|
||||
//! Project version string for FLEX.
|
||||
FOUNDATION_EXPORT const unsigned char FLEXVersionString[];
|
||||
|
||||
#import <FLEX/FLEXManager.h>
|
||||
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// PTDatabaseManager.h
|
||||
// Derived from:
|
||||
//
|
||||
// FMDatabase.h
|
||||
// FMDB( https://github.com/ccgus/fmdb )
|
||||
//
|
||||
// Created by Peng Tao on 15/11/23.
|
||||
//
|
||||
// Licensed to Flying Meat Inc. under one or more contributor license agreements.
|
||||
// See the LICENSE file distributed with this work for the terms under
|
||||
// which Flying Meat Inc. licenses this file to you.
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface FLEXDatabaseManager : NSObject
|
||||
|
||||
|
||||
- (instancetype)initWithPath:(NSString*)aPath;
|
||||
|
||||
- (BOOL)open;
|
||||
- (NSArray *)queryAllTables;
|
||||
- (NSArray *)queryAllColumnsWithTableName:(NSString *)tableName;
|
||||
- (NSArray *)queryAllDataWithTableName:(NSString *)tableName;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,195 @@
|
||||
//
|
||||
// PTDatabaseManager.m
|
||||
// PTDatabaseReader
|
||||
//
|
||||
// Created by Peng Tao on 15/11/23.
|
||||
// Copyright © 2015年 Peng Tao. All rights reserved.
|
||||
//
|
||||
|
||||
|
||||
|
||||
#import "FLEXDatabaseManager.h"
|
||||
#import <sqlite3.h>
|
||||
|
||||
|
||||
static NSString *const QUERY_TABLENAMES_SQL = @"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name";
|
||||
|
||||
|
||||
@implementation FLEXDatabaseManager
|
||||
{
|
||||
sqlite3* _db;
|
||||
NSString* _databasePath;
|
||||
}
|
||||
|
||||
|
||||
- (instancetype)initWithPath:(NSString*)aPath
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
if (self) {
|
||||
_databasePath = [aPath copy];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
- (BOOL)open {
|
||||
if (_db) {
|
||||
return YES;
|
||||
}
|
||||
int err = sqlite3_open([_databasePath UTF8String], &_db);
|
||||
if(err != SQLITE_OK) {
|
||||
NSLog(@"error opening!: %d", err);
|
||||
return NO;
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)close {
|
||||
if (!_db) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
int rc;
|
||||
BOOL retry;
|
||||
BOOL triedFinalizingOpenStatements = NO;
|
||||
|
||||
do {
|
||||
retry = NO;
|
||||
rc = sqlite3_close(_db);
|
||||
if (SQLITE_BUSY == rc || SQLITE_LOCKED == rc) {
|
||||
if (!triedFinalizingOpenStatements) {
|
||||
triedFinalizingOpenStatements = YES;
|
||||
sqlite3_stmt *pStmt;
|
||||
while ((pStmt = sqlite3_next_stmt(_db, nil)) !=0) {
|
||||
NSLog(@"Closing leaked statement");
|
||||
sqlite3_finalize(pStmt);
|
||||
retry = YES;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (SQLITE_OK != rc) {
|
||||
NSLog(@"error closing!: %d", rc);
|
||||
}
|
||||
}
|
||||
while (retry);
|
||||
|
||||
_db = nil;
|
||||
return YES;
|
||||
}
|
||||
|
||||
|
||||
- (NSArray *)queryAllTables
|
||||
{
|
||||
return [self executeQuery:QUERY_TABLENAMES_SQL];
|
||||
}
|
||||
|
||||
- (NSArray *)queryAllColumnsWithTableName:(NSString *)tableName
|
||||
{
|
||||
NSString *sql = [NSString stringWithFormat:@"PRAGMA table_info('%@')",tableName];
|
||||
NSArray *resultArray = [self executeQuery:sql];
|
||||
NSMutableArray *array = [NSMutableArray array];
|
||||
for (NSDictionary *dict in resultArray) {
|
||||
[array addObject:dict[@"name"]];
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
- (NSArray *)queryAllDataWithTableName:(NSString *)tableName
|
||||
{
|
||||
NSString *sql = [NSString stringWithFormat:@"SELECT * FROM %@",tableName];
|
||||
return [self executeQuery:sql];
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark - Private
|
||||
|
||||
- (NSArray *)executeQuery:(NSString *)sql
|
||||
{
|
||||
[self open];
|
||||
NSMutableArray *resultArray = [NSMutableArray array];
|
||||
sqlite3_stmt *pstmt;
|
||||
if (sqlite3_prepare_v2(_db, [sql UTF8String], -1, &pstmt, 0) == SQLITE_OK) {
|
||||
while (sqlite3_step(pstmt) == SQLITE_ROW) {
|
||||
NSUInteger num_cols = (NSUInteger)sqlite3_data_count(pstmt);
|
||||
if (num_cols > 0) {
|
||||
NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:num_cols];
|
||||
|
||||
int columnCount = sqlite3_column_count(pstmt);
|
||||
|
||||
int columnIdx = 0;
|
||||
for (columnIdx = 0; columnIdx < columnCount; columnIdx++) {
|
||||
|
||||
NSString *columnName = [NSString stringWithUTF8String:sqlite3_column_name(pstmt, columnIdx)];
|
||||
id objectValue = [self objectForColumnIndex:columnIdx stmt:pstmt];
|
||||
[dict setObject:objectValue forKey:columnName];
|
||||
}
|
||||
[resultArray addObject:dict];
|
||||
}
|
||||
}
|
||||
}
|
||||
[self close];
|
||||
return resultArray;
|
||||
}
|
||||
|
||||
|
||||
- (id)objectForColumnIndex:(int)columnIdx stmt:(sqlite3_stmt*)stmt {
|
||||
int columnType = sqlite3_column_type(stmt, columnIdx);
|
||||
|
||||
id returnValue = nil;
|
||||
|
||||
if (columnType == SQLITE_INTEGER) {
|
||||
returnValue = [NSNumber numberWithLongLong:sqlite3_column_int64(stmt, columnIdx)];
|
||||
}
|
||||
else if (columnType == SQLITE_FLOAT) {
|
||||
returnValue = [NSNumber numberWithDouble:sqlite3_column_double(stmt, columnIdx)];
|
||||
}
|
||||
else if (columnType == SQLITE_BLOB) {
|
||||
returnValue = [self dataForColumnIndex:columnIdx stmt:stmt];
|
||||
}
|
||||
else {
|
||||
//default to a string for everything else
|
||||
returnValue = [self stringForColumnIndex:columnIdx stmt:stmt];
|
||||
}
|
||||
|
||||
if (returnValue == nil) {
|
||||
returnValue = [NSNull null];
|
||||
}
|
||||
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
- (NSString *)stringForColumnIndex:(int)columnIdx stmt:(sqlite3_stmt *)stmt {
|
||||
|
||||
if (sqlite3_column_type(stmt, columnIdx) == SQLITE_NULL || (columnIdx < 0)) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
const char *c = (const char *)sqlite3_column_text(stmt, columnIdx);
|
||||
|
||||
if (!c) {
|
||||
// null row.
|
||||
return nil;
|
||||
}
|
||||
|
||||
return [NSString stringWithUTF8String:c];
|
||||
}
|
||||
|
||||
- (NSData *)dataForColumnIndex:(int)columnIdx stmt:(sqlite3_stmt *)stmt{
|
||||
|
||||
if (sqlite3_column_type(stmt, columnIdx) == SQLITE_NULL || (columnIdx < 0)) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
const char *dataBuffer = sqlite3_column_blob(stmt, columnIdx);
|
||||
int dataSize = sqlite3_column_bytes(stmt, columnIdx);
|
||||
|
||||
if (dataBuffer == NULL) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return [NSData dataWithBytes:(const void *)dataBuffer length:(NSUInteger)dataSize];
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,48 @@
|
||||
//
|
||||
// PTMultiColumnTableView.h
|
||||
// PTMultiColumnTableViewDemo
|
||||
//
|
||||
// Created by Peng Tao on 15/11/16.
|
||||
// Copyright © 2015年 Peng Tao. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "FLEXTableColumnHeader.h"
|
||||
|
||||
@class FLEXMultiColumnTableView;
|
||||
|
||||
@protocol FLEXMultiColumnTableViewDelegate <NSObject>
|
||||
|
||||
@required
|
||||
- (void)multiColumnTableView:(FLEXMultiColumnTableView *)tableView didTapLabelWithText:(NSString *)text;
|
||||
- (void)multiColumnTableView:(FLEXMultiColumnTableView *)tableView didTapHeaderWithText:(NSString *)text sortType:(FLEXTableColumnHeaderSortType)sortType;
|
||||
|
||||
@end
|
||||
|
||||
@protocol FLEXMultiColumnTableViewDataSource <NSObject>
|
||||
|
||||
@required
|
||||
|
||||
- (NSInteger)numberOfColumnsInTableView:(FLEXMultiColumnTableView *)tableView;
|
||||
- (NSInteger)numberOfRowsInTableView:(FLEXMultiColumnTableView *)tableView;
|
||||
- (NSString *)columnNameInColumn:(NSInteger)column;
|
||||
- (NSString *)rowNameInRow:(NSInteger)row;
|
||||
- (NSString *)contentAtColumn:(NSInteger)column row:(NSInteger)row;
|
||||
- (NSArray *)contentAtRow:(NSInteger)row;
|
||||
|
||||
- (CGFloat)multiColumnTableView:(FLEXMultiColumnTableView *)tableView widthForContentCellInColumn:(NSInteger)column;
|
||||
- (CGFloat)multiColumnTableView:(FLEXMultiColumnTableView *)tableView heightForContentCellInRow:(NSInteger)row;
|
||||
- (CGFloat)heightForTopHeaderInTableView:(FLEXMultiColumnTableView *)tableView;
|
||||
- (CGFloat)widthForLeftHeaderInTableView:(FLEXMultiColumnTableView *)tableView;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@interface FLEXMultiColumnTableView : UIView
|
||||
|
||||
@property (nonatomic, weak) id<FLEXMultiColumnTableViewDataSource>dataSource;
|
||||
@property (nonatomic, weak) id<FLEXMultiColumnTableViewDelegate>delegate;
|
||||
|
||||
- (void)reloadData;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,341 @@
|
||||
//
|
||||
// PTMultiColumnTableView.m
|
||||
// PTMultiColumnTableViewDemo
|
||||
//
|
||||
// Created by Peng Tao on 15/11/16.
|
||||
// Copyright © 2015年 Peng Tao. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXMultiColumnTableView.h"
|
||||
#import "FLEXTableContentCell.h"
|
||||
#import "FLEXTableLeftCell.h"
|
||||
|
||||
@interface FLEXMultiColumnTableView ()
|
||||
<UITableViewDataSource, UITableViewDelegate,UIScrollViewDelegate, FLEXTableContentCellDelegate>
|
||||
|
||||
@property (nonatomic, strong) UIScrollView *contentScrollView;
|
||||
@property (nonatomic, strong) UIScrollView *headerScrollView;
|
||||
@property (nonatomic, strong) UITableView *leftTableView;
|
||||
@property (nonatomic, strong) UITableView *contentTableView;
|
||||
@property (nonatomic, strong) UIView *leftHeader;
|
||||
|
||||
@property (nonatomic, strong) NSDictionary *sortStatusDict;
|
||||
@property (nonatomic, strong) NSArray *rowData;
|
||||
@end
|
||||
|
||||
static const CGFloat kColumnMargin = 1;
|
||||
|
||||
@implementation FLEXMultiColumnTableView
|
||||
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame
|
||||
{
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
[self loadUI];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)didMoveToSuperview
|
||||
{
|
||||
[super didMoveToSuperview];
|
||||
[self reloadData];
|
||||
}
|
||||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
[super layoutSubviews];
|
||||
|
||||
CGFloat width = self.frame.size.width;
|
||||
CGFloat height = self.frame.size.height;
|
||||
CGFloat topheaderHeight = [self topHeaderHeight];
|
||||
CGFloat leftHeaderWidth = [self leftHeaderWidth];
|
||||
|
||||
CGFloat contentWidth = 0.0;
|
||||
NSInteger rowsCount = [self numberOfColumns];
|
||||
for (int i = 0; i < rowsCount; i++) {
|
||||
contentWidth += [self contentWidthForColumn:i];
|
||||
}
|
||||
|
||||
self.leftTableView.frame = CGRectMake(0, topheaderHeight, leftHeaderWidth, height - topheaderHeight);
|
||||
self.headerScrollView.frame = CGRectMake(leftHeaderWidth, 0, width - leftHeaderWidth, topheaderHeight);
|
||||
self.headerScrollView.contentSize = CGSizeMake( self.contentTableView.frame.size.width, self.headerScrollView.frame.size.height);
|
||||
self.contentTableView.frame = CGRectMake(0, 0, contentWidth + [self numberOfColumns] * [self columnMargin] , height - topheaderHeight);
|
||||
self.contentScrollView.frame = CGRectMake(leftHeaderWidth, topheaderHeight, width - leftHeaderWidth, height - topheaderHeight);
|
||||
self.contentScrollView.contentSize = self.contentTableView.frame.size;
|
||||
self.leftHeader.frame = CGRectMake(0, 0, [self leftHeaderWidth], [self topHeaderHeight]);
|
||||
}
|
||||
|
||||
|
||||
- (void)loadUI
|
||||
{
|
||||
[self loadHeaderScrollView];
|
||||
[self loadContentScrollView];
|
||||
[self loadLeftView];
|
||||
}
|
||||
|
||||
- (void)reloadData
|
||||
{
|
||||
[self loadLeftViewData];
|
||||
[self loadContentData];
|
||||
[self loadHeaderData];
|
||||
}
|
||||
|
||||
#pragma mark - UI
|
||||
|
||||
- (void)loadHeaderScrollView
|
||||
{
|
||||
UIScrollView *headerScrollView = [[UIScrollView alloc] init];
|
||||
headerScrollView.delegate = self;
|
||||
self.headerScrollView = headerScrollView;
|
||||
self.headerScrollView.backgroundColor = [UIColor colorWithWhite:0.803 alpha:0.850];
|
||||
|
||||
[self addSubview:headerScrollView];
|
||||
}
|
||||
|
||||
- (void)loadContentScrollView
|
||||
{
|
||||
|
||||
UIScrollView *scrollView = [[UIScrollView alloc] init];
|
||||
scrollView.bounces = NO;
|
||||
scrollView.delegate = self;
|
||||
|
||||
UITableView *tableView = [[UITableView alloc] init];
|
||||
tableView.delegate = self;
|
||||
tableView.dataSource = self;
|
||||
tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
|
||||
|
||||
[self addSubview:scrollView];
|
||||
[scrollView addSubview:tableView];
|
||||
|
||||
self.contentScrollView = scrollView;
|
||||
self.contentTableView = tableView;
|
||||
|
||||
}
|
||||
|
||||
- (void)loadLeftView
|
||||
{
|
||||
UITableView *leftTableView = [[UITableView alloc] init];
|
||||
leftTableView.delegate = self;
|
||||
leftTableView.dataSource = self;
|
||||
leftTableView.separatorStyle = UITableViewCellSeparatorStyleNone;
|
||||
self.leftTableView = leftTableView;
|
||||
[self addSubview:leftTableView];
|
||||
|
||||
UIView *leftHeader = [[UIView alloc] init];
|
||||
leftHeader.backgroundColor = [UIColor colorWithWhite:0.950 alpha:0.668];
|
||||
self.leftHeader = leftHeader;
|
||||
[self addSubview:leftHeader];
|
||||
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Data
|
||||
|
||||
- (void)loadHeaderData
|
||||
{
|
||||
NSArray *subviews = self.headerScrollView.subviews;
|
||||
|
||||
for (UIView *subview in subviews) {
|
||||
[subview removeFromSuperview];
|
||||
}
|
||||
CGFloat x = 0.0;
|
||||
CGFloat w = 0.0;
|
||||
for (int i = 0; i < [self numberOfColumns] ; i++) {
|
||||
w = [self contentWidthForColumn:i] + [self columnMargin];
|
||||
|
||||
FLEXTableColumnHeader *cell = [[FLEXTableColumnHeader alloc] initWithFrame:CGRectMake(x, 0, w, [self topHeaderHeight] - 1)];
|
||||
cell.label.text = [self columnTitleForColumn:i];
|
||||
[self.headerScrollView addSubview:cell];
|
||||
|
||||
FLEXTableColumnHeaderSortType type = [self.sortStatusDict[[self columnTitleForColumn:i]] integerValue];
|
||||
[cell changeSortStatusWithType:type];
|
||||
|
||||
UITapGestureRecognizer *gesture = [[UITapGestureRecognizer alloc] initWithTarget:self
|
||||
action:@selector(contentHeaderTap:)];
|
||||
[cell addGestureRecognizer:gesture];
|
||||
cell.userInteractionEnabled = YES;
|
||||
|
||||
x = x + w;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)contentHeaderTap:(UIGestureRecognizer *)gesture
|
||||
{
|
||||
FLEXTableColumnHeader *header = (FLEXTableColumnHeader *)gesture.view;
|
||||
NSString *string = header.label.text;
|
||||
FLEXTableColumnHeaderSortType currentType = [self.sortStatusDict[string] integerValue];
|
||||
FLEXTableColumnHeaderSortType newType ;
|
||||
|
||||
switch (currentType) {
|
||||
case FLEXTableColumnHeaderSortTypeNone:
|
||||
newType = FLEXTableColumnHeaderSortTypeAsc;
|
||||
break;
|
||||
case FLEXTableColumnHeaderSortTypeAsc:
|
||||
newType = FLEXTableColumnHeaderSortTypeDesc;
|
||||
break;
|
||||
case FLEXTableColumnHeaderSortTypeDesc:
|
||||
newType = FLEXTableColumnHeaderSortTypeAsc;
|
||||
break;
|
||||
}
|
||||
|
||||
self.sortStatusDict = @{header.label.text : @(newType)};
|
||||
[header changeSortStatusWithType:newType];
|
||||
[self.delegate multiColumnTableView:self didTapHeaderWithText:string sortType:newType];
|
||||
|
||||
}
|
||||
|
||||
- (void)loadContentData
|
||||
{
|
||||
[self.contentTableView reloadData];
|
||||
}
|
||||
|
||||
- (void)loadLeftViewData
|
||||
{
|
||||
[self.leftTableView reloadData];
|
||||
}
|
||||
|
||||
- (UITableViewCell *)tableView:(UITableView *)tableView
|
||||
cellForRowAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
UIColor *backgroundColor = [UIColor whiteColor];
|
||||
if (indexPath.row % 2 != 0) {
|
||||
backgroundColor = [UIColor colorWithWhite:0.950 alpha:0.750];
|
||||
}
|
||||
|
||||
if (tableView != self.leftTableView) {
|
||||
self.rowData = [self.dataSource contentAtRow:indexPath.row];
|
||||
FLEXTableContentCell *cell = [FLEXTableContentCell cellWithTableView:tableView
|
||||
columnNumber:[self numberOfColumns]];
|
||||
cell.contentView.backgroundColor = backgroundColor;
|
||||
cell.delegate = self;
|
||||
|
||||
for (int i = 0 ; i < cell.labels.count; i++) {
|
||||
|
||||
UILabel *label = cell.labels[i];
|
||||
label.textColor = [UIColor blackColor];
|
||||
|
||||
NSString *content = [NSString stringWithFormat:@"%@",self.rowData[i]];
|
||||
if ([content isEqualToString:@"<null>"]) {
|
||||
label.textColor = [UIColor lightGrayColor];
|
||||
content = @"NULL";
|
||||
}
|
||||
label.text = content;
|
||||
label.backgroundColor = backgroundColor;
|
||||
}
|
||||
return cell;
|
||||
}
|
||||
else {
|
||||
FLEXTableLeftCell *cell = [FLEXTableLeftCell cellWithTableView:tableView];
|
||||
cell.contentView.backgroundColor = backgroundColor;
|
||||
cell.titlelabel.text = [self rowTitleForRow:indexPath.row];
|
||||
return cell;
|
||||
}
|
||||
}
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
|
||||
{
|
||||
return [self.dataSource numberOfRowsInTableView:self];
|
||||
}
|
||||
|
||||
|
||||
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
return [self.dataSource multiColumnTableView:self heightForContentCellInRow:indexPath.row];
|
||||
}
|
||||
|
||||
|
||||
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
|
||||
{
|
||||
if (scrollView == self.contentScrollView) {
|
||||
self.headerScrollView.contentOffset = scrollView.contentOffset;
|
||||
}
|
||||
else if (scrollView == self.headerScrollView) {
|
||||
self.contentScrollView.contentOffset = scrollView.contentOffset;
|
||||
}
|
||||
else if (scrollView == self.leftTableView) {
|
||||
self.contentTableView.contentOffset = scrollView.contentOffset;
|
||||
}
|
||||
else if (scrollView == self.contentTableView) {
|
||||
self.leftTableView.contentOffset = scrollView.contentOffset;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark UITableView Delegate
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
if (tableView == self.leftTableView) {
|
||||
[self.contentTableView selectRowAtIndexPath:indexPath
|
||||
animated:NO
|
||||
scrollPosition:UITableViewScrollPositionNone];
|
||||
}
|
||||
else if (tableView == self.contentTableView) {
|
||||
[self.leftTableView selectRowAtIndexPath:indexPath
|
||||
animated:NO
|
||||
scrollPosition:UITableViewScrollPositionNone];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark DataSource Accessor
|
||||
|
||||
- (NSInteger)numberOfrows
|
||||
{
|
||||
return [self.dataSource numberOfRowsInTableView:self];
|
||||
}
|
||||
|
||||
- (NSInteger)numberOfColumns
|
||||
{
|
||||
return [self.dataSource numberOfColumnsInTableView:self];
|
||||
}
|
||||
|
||||
- (NSString *)columnTitleForColumn:(NSInteger)column
|
||||
{
|
||||
return [self.dataSource columnNameInColumn:column];
|
||||
}
|
||||
|
||||
- (NSString *)rowTitleForRow:(NSInteger)row
|
||||
{
|
||||
return [self.dataSource rowNameInRow:row];
|
||||
}
|
||||
|
||||
- (NSString *)contentAtColumn:(NSInteger)column row:(NSInteger)row;
|
||||
{
|
||||
return [self.dataSource contentAtColumn:column row:row];
|
||||
}
|
||||
|
||||
- (CGFloat)contentWidthForColumn:(NSInteger)column
|
||||
{
|
||||
return [self.dataSource multiColumnTableView:self widthForContentCellInColumn:column];
|
||||
}
|
||||
|
||||
- (CGFloat)contentHeightForRow:(NSInteger)row
|
||||
{
|
||||
return [self.dataSource multiColumnTableView:self heightForContentCellInRow:row];
|
||||
}
|
||||
|
||||
- (CGFloat)topHeaderHeight
|
||||
{
|
||||
return [self.dataSource heightForTopHeaderInTableView:self];
|
||||
}
|
||||
|
||||
- (CGFloat)leftHeaderWidth
|
||||
{
|
||||
return [self.dataSource widthForLeftHeaderInTableView:self];
|
||||
}
|
||||
|
||||
- (CGFloat)columnMargin
|
||||
{
|
||||
return kColumnMargin;
|
||||
}
|
||||
|
||||
|
||||
- (void)tableContentCell:(FLEXTableContentCell *)tableView labelDidTapWithText:(NSString *)text
|
||||
{
|
||||
[self.delegate multiColumnTableView:self didTapLabelWithText:text];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,24 @@
|
||||
//
|
||||
// FLEXTableContentHeaderCell.h
|
||||
// UICatalog
|
||||
//
|
||||
// Created by Peng Tao on 15/11/26.
|
||||
// Copyright © 2015年 f. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
typedef NS_ENUM(NSUInteger, FLEXTableColumnHeaderSortType) {
|
||||
FLEXTableColumnHeaderSortTypeNone = 0,
|
||||
FLEXTableColumnHeaderSortTypeAsc,
|
||||
FLEXTableColumnHeaderSortTypeDesc,
|
||||
};
|
||||
|
||||
@interface FLEXTableColumnHeader : UIView
|
||||
|
||||
@property (nonatomic, strong) UILabel *label;
|
||||
|
||||
- (void)changeSortStatusWithType:(FLEXTableColumnHeaderSortType)type;
|
||||
|
||||
@end
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
//
|
||||
// FLEXTableContentHeaderCell.m
|
||||
// UICatalog
|
||||
//
|
||||
// Created by Peng Tao on 15/11/26.
|
||||
// Copyright © 2015年 f. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXTableColumnHeader.h"
|
||||
|
||||
@implementation FLEXTableColumnHeader
|
||||
{
|
||||
UILabel *_arrowLabel;
|
||||
}
|
||||
|
||||
|
||||
- (instancetype)initWithFrame:(CGRect)frame
|
||||
{
|
||||
self = [super initWithFrame:frame];
|
||||
if (self) {
|
||||
self.backgroundColor = [UIColor whiteColor];
|
||||
|
||||
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(5, 0, frame.size.width - 25, frame.size.height)];
|
||||
label.font = [UIFont systemFontOfSize:13.0];
|
||||
[self addSubview:label];
|
||||
self.label = label;
|
||||
|
||||
|
||||
_arrowLabel = [[UILabel alloc] initWithFrame:CGRectMake(frame.size.width - 20, 0, 20, frame.size.height)];
|
||||
_arrowLabel.font = [UIFont systemFontOfSize:13.0];
|
||||
[self addSubview:_arrowLabel];
|
||||
|
||||
UIView *line = [[UIView alloc] initWithFrame:CGRectMake(frame.size.width - 1, 2, 1, frame.size.height - 4)];
|
||||
line.backgroundColor = [UIColor colorWithWhite:0.803 alpha:0.850];
|
||||
[self addSubview:line];
|
||||
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)changeSortStatusWithType:(FLEXTableColumnHeaderSortType)type
|
||||
{
|
||||
switch (type) {
|
||||
case FLEXTableColumnHeaderSortTypeNone:
|
||||
_arrowLabel.text = @"";
|
||||
break;
|
||||
case FLEXTableColumnHeaderSortTypeAsc:
|
||||
_arrowLabel.text = @"⬆️";
|
||||
break;
|
||||
case FLEXTableColumnHeaderSortTypeDesc:
|
||||
_arrowLabel.text = @"⬇️";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,27 @@
|
||||
//
|
||||
// FLEXTableContentCell.h
|
||||
// UICatalog
|
||||
//
|
||||
// Created by Peng Tao on 15/11/24.
|
||||
// Copyright © 2015年 f. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@class FLEXTableContentCell;
|
||||
@protocol FLEXTableContentCellDelegate <NSObject>
|
||||
|
||||
@optional
|
||||
- (void)tableContentCell:(FLEXTableContentCell *)tableView labelDidTapWithText:(NSString *)text;
|
||||
|
||||
@end
|
||||
|
||||
@interface FLEXTableContentCell : UITableViewCell
|
||||
|
||||
@property (nonatomic, strong)NSArray *labels;
|
||||
|
||||
@property (nonatomic, weak) id<FLEXTableContentCellDelegate>delegate;
|
||||
|
||||
+ (instancetype)cellWithTableView:(UITableView *)tableView columnNumber:(NSInteger)number;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,66 @@
|
||||
//
|
||||
// FLEXTableContentCell.m
|
||||
// UICatalog
|
||||
//
|
||||
// Created by Peng Tao on 15/11/24.
|
||||
// Copyright © 2015年 f. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXTableContentCell.h"
|
||||
#import "FLEXMultiColumnTableView.h"
|
||||
|
||||
@interface FLEXTableContentCell ()
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXTableContentCell
|
||||
|
||||
+ (instancetype)cellWithTableView:(UITableView *)tableView columnNumber:(NSInteger)number;
|
||||
{
|
||||
static NSString *identifier = @"FLEXTableContentCell";
|
||||
FLEXTableContentCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
|
||||
if (!cell) {
|
||||
cell = [[FLEXTableContentCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
|
||||
NSMutableArray *labels = [NSMutableArray array];
|
||||
for (int i = 0; i < number ; i++) {
|
||||
UILabel *label = [[UILabel alloc] initWithFrame:CGRectZero];
|
||||
label.backgroundColor = [UIColor whiteColor];
|
||||
label.font = [UIFont systemFontOfSize:13.0];
|
||||
label.textAlignment = NSTextAlignmentLeft;
|
||||
label.backgroundColor = [UIColor greenColor];
|
||||
[labels addObject:label];
|
||||
|
||||
UITapGestureRecognizer *gesture = [[UITapGestureRecognizer alloc] initWithTarget:cell
|
||||
action:@selector(labelDidTap:)];
|
||||
[label addGestureRecognizer:gesture];
|
||||
label.userInteractionEnabled = YES;
|
||||
|
||||
[cell.contentView addSubview:label];
|
||||
cell.contentView.backgroundColor = [UIColor whiteColor];
|
||||
}
|
||||
cell.labels = labels;
|
||||
}
|
||||
return cell;
|
||||
}
|
||||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
[super layoutSubviews];
|
||||
CGFloat labelWidth = self.contentView.frame.size.width / self.labels.count;
|
||||
CGFloat labelHeight = self.contentView.frame.size.height;
|
||||
for (int i = 0; i < self.labels.count; i++) {
|
||||
UILabel *label = self.labels[i];
|
||||
label.frame = CGRectMake(labelWidth * i + 5, 0, (labelWidth - 10), labelHeight);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
- (void)labelDidTap:(UIGestureRecognizer *)gesture
|
||||
{
|
||||
UILabel *label = (UILabel *)gesture.view;
|
||||
if ([self.delegate respondsToSelector:@selector(tableContentCell:labelDidTapWithText:)]) {
|
||||
[self.delegate tableContentCell:self labelDidTapWithText:label.text];
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,16 @@
|
||||
//
|
||||
// PTTableContentViewController.h
|
||||
// PTDatabaseReader
|
||||
//
|
||||
// Created by Peng Tao on 15/11/23.
|
||||
// Copyright © 2015年 Peng Tao. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface FLEXTableContentViewController : UIViewController
|
||||
|
||||
@property (nonatomic, strong) NSArray *columnsArray;
|
||||
@property (nonatomic, strong) NSArray *contentsArray;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,184 @@
|
||||
//
|
||||
// PTTableContentViewController.m
|
||||
// PTDatabaseReader
|
||||
//
|
||||
// Created by Peng Tao on 15/11/23.
|
||||
// Copyright © 2015年 Peng Tao. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXTableContentViewController.h"
|
||||
#import "FLEXMultiColumnTableView.h"
|
||||
#import "FLEXWebViewController.h"
|
||||
|
||||
|
||||
@interface FLEXTableContentViewController ()<FLEXMultiColumnTableViewDataSource, FLEXMultiColumnTableViewDelegate>
|
||||
|
||||
@property (nonatomic, strong)FLEXMultiColumnTableView *multiColumView;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXTableContentViewController
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
|
||||
CGRect rectStatus = [UIApplication sharedApplication].statusBarFrame;
|
||||
CGFloat y = 64;
|
||||
if (rectStatus.size.height == 0) {
|
||||
y = 32;
|
||||
}
|
||||
_multiColumView = [[FLEXMultiColumnTableView alloc] initWithFrame:
|
||||
CGRectMake(0, y, self.view.frame.size.width, self.view.frame.size.height - y)];
|
||||
|
||||
_multiColumView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
|
||||
_multiColumView.backgroundColor = [UIColor whiteColor];
|
||||
_multiColumView.dataSource = self;
|
||||
_multiColumView.delegate = self;
|
||||
self.automaticallyAdjustsScrollViewInsets = NO;
|
||||
|
||||
|
||||
[self.view addSubview:_multiColumView];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated
|
||||
{
|
||||
[super viewWillAppear:animated];
|
||||
[self.multiColumView reloadData];
|
||||
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark MultiColumnTableView DataSource
|
||||
|
||||
- (NSInteger)numberOfColumnsInTableView:(FLEXMultiColumnTableView *)tableView
|
||||
{
|
||||
return self.columnsArray.count;
|
||||
}
|
||||
- (NSInteger)numberOfRowsInTableView:(FLEXMultiColumnTableView *)tableView
|
||||
{
|
||||
return self.contentsArray.count;
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)columnNameInColumn:(NSInteger)column
|
||||
{
|
||||
return self.columnsArray[column];
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)rowNameInRow:(NSInteger)row
|
||||
{
|
||||
return [NSString stringWithFormat:@"%ld",(long)row];
|
||||
}
|
||||
|
||||
- (NSString *)contentAtColumn:(NSInteger)column row:(NSInteger)row
|
||||
{
|
||||
if (self.contentsArray.count > row) {
|
||||
NSDictionary *dic = self.contentsArray[row];
|
||||
if (self.contentsArray.count > column) {
|
||||
return [NSString stringWithFormat:@"%@",[dic objectForKey:self.columnsArray[column]]];
|
||||
}
|
||||
}
|
||||
return @"";
|
||||
}
|
||||
|
||||
- (NSArray *)contentAtRow:(NSInteger)row
|
||||
{
|
||||
NSMutableArray *result = [NSMutableArray array];
|
||||
if (self.contentsArray.count > row) {
|
||||
NSDictionary *dic = self.contentsArray[row];
|
||||
for (int i = 0; i < self.columnsArray.count; i ++) {
|
||||
[result addObject:dic[self.columnsArray[i]]];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (CGFloat)multiColumnTableView:(FLEXMultiColumnTableView *)tableView
|
||||
heightForContentCellInRow:(NSInteger)row
|
||||
{
|
||||
return 40;
|
||||
}
|
||||
|
||||
- (CGFloat)multiColumnTableView:(FLEXMultiColumnTableView *)tableView
|
||||
widthForContentCellInColumn:(NSInteger)column
|
||||
{
|
||||
return 120;
|
||||
}
|
||||
|
||||
- (CGFloat)heightForTopHeaderInTableView:(FLEXMultiColumnTableView *)tableView
|
||||
{
|
||||
return 40;
|
||||
}
|
||||
|
||||
- (CGFloat)widthForLeftHeaderInTableView:(FLEXMultiColumnTableView *)tableView
|
||||
{
|
||||
NSString *str = [NSString stringWithFormat:@"%lu",(unsigned long)self.contentsArray.count];
|
||||
NSDictionary *attrs = @{@"NSFontAttributeName":[UIFont systemFontOfSize:17.0]};
|
||||
CGSize size = [str boundingRectWithSize:CGSizeMake(CGFLOAT_MAX, 14)
|
||||
options:NSStringDrawingUsesLineFragmentOrigin
|
||||
attributes:attrs context:nil].size;
|
||||
return size.width + 20;
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark MultiColumnTableView Delegate
|
||||
|
||||
|
||||
- (void)multiColumnTableView:(FLEXMultiColumnTableView *)tableView didTapLabelWithText:(NSString *)text
|
||||
{
|
||||
FLEXWebViewController * detailViewController = [[FLEXWebViewController alloc] initWithText:text];
|
||||
[self.navigationController pushViewController:detailViewController animated:YES];
|
||||
}
|
||||
|
||||
- (void)multiColumnTableView:(FLEXMultiColumnTableView *)tableView didTapHeaderWithText:(NSString *)text sortType:(FLEXTableColumnHeaderSortType)sortType
|
||||
{
|
||||
|
||||
NSArray *sortContentData = [self.contentsArray sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) {
|
||||
|
||||
if ([obj1 objectForKey:text] == [NSNull null]) {
|
||||
return NSOrderedAscending;
|
||||
}
|
||||
if ([obj2 objectForKey:text] == [NSNull null]) {
|
||||
return NSOrderedDescending;
|
||||
}
|
||||
NSComparisonResult result = [[obj1 objectForKey:text] compare:[obj2 objectForKey:text]];
|
||||
|
||||
return result;
|
||||
}];
|
||||
if (sortType == FLEXTableColumnHeaderSortTypeDesc) {
|
||||
NSEnumerator *contentReverseEvumerator = [sortContentData reverseObjectEnumerator];
|
||||
sortContentData = [NSArray arrayWithArray:[contentReverseEvumerator allObjects]];
|
||||
}
|
||||
|
||||
self.contentsArray = sortContentData;
|
||||
[self.multiColumView reloadData];
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
#pragma mark About Transition
|
||||
|
||||
- (void)willTransitionToTraitCollection:(UITraitCollection *)newCollection
|
||||
withTransitionCoordinator:(id <UIViewControllerTransitionCoordinator>)coordinator
|
||||
{
|
||||
[super willTransitionToTraitCollection:newCollection
|
||||
withTransitionCoordinator:coordinator];
|
||||
[coordinator animateAlongsideTransition:^(id <UIViewControllerTransitionCoordinatorContext> context) {
|
||||
if (newCollection.verticalSizeClass == UIUserInterfaceSizeClassCompact) {
|
||||
|
||||
_multiColumView.frame = CGRectMake(0, 32, self.view.frame.size.width, self.view.frame.size.height - 32);
|
||||
}
|
||||
else {
|
||||
_multiColumView.frame = CGRectMake(0, 64, self.view.frame.size.width, self.view.frame.size.height - 64);
|
||||
}
|
||||
[self.view setNeedsLayout];
|
||||
} completion:nil];
|
||||
}
|
||||
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,17 @@
|
||||
//
|
||||
// FLEXTableLeftCell.h
|
||||
// UICatalog
|
||||
//
|
||||
// Created by Peng Tao on 15/11/24.
|
||||
// Copyright © 2015年 f. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface FLEXTableLeftCell : UITableViewCell
|
||||
|
||||
@property (nonatomic, strong) UILabel *titlelabel;
|
||||
|
||||
+ (instancetype)cellWithTableView:(UITableView *)tableView;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,35 @@
|
||||
//
|
||||
// FLEXTableLeftCell.m
|
||||
// UICatalog
|
||||
//
|
||||
// Created by Peng Tao on 15/11/24.
|
||||
// Copyright © 2015年 f. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXTableLeftCell.h"
|
||||
|
||||
@implementation FLEXTableLeftCell
|
||||
|
||||
+ (instancetype)cellWithTableView:(UITableView *)tableView
|
||||
{
|
||||
static NSString *identifier = @"FLEXTableLeftCell";
|
||||
FLEXTableLeftCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
|
||||
|
||||
if (!cell) {
|
||||
cell = [[FLEXTableLeftCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
|
||||
UILabel *textLabel = [[UILabel alloc] initWithFrame:CGRectZero];
|
||||
textLabel.textAlignment = NSTextAlignmentCenter;
|
||||
textLabel.font = [UIFont systemFontOfSize:13.0];
|
||||
textLabel.backgroundColor = [UIColor clearColor];
|
||||
[cell.contentView addSubview:textLabel];
|
||||
cell.titlelabel = textLabel;
|
||||
}
|
||||
return cell;
|
||||
}
|
||||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
[super layoutSubviews];
|
||||
self.titlelabel.frame = self.contentView.frame;
|
||||
}
|
||||
@end
|
||||
@@ -0,0 +1,15 @@
|
||||
//
|
||||
// PTTableListViewController.h
|
||||
// PTDatabaseReader
|
||||
//
|
||||
// Created by Peng Tao on 15/11/23.
|
||||
// Copyright © 2015年 Peng Tao. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface FLEXTableListViewController : UITableViewController
|
||||
|
||||
- (instancetype)initWithPath:(NSString *)path;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,82 @@
|
||||
//
|
||||
// PTTableListViewController.m
|
||||
// PTDatabaseReader
|
||||
//
|
||||
// Created by Peng Tao on 15/11/23.
|
||||
// Copyright © 2015年 Peng Tao. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXTableListViewController.h"
|
||||
#import "FLEXDatabaseManager.h"
|
||||
#import "FLEXTableContentViewController.h"
|
||||
|
||||
@interface FLEXTableListViewController ()
|
||||
{
|
||||
FLEXDatabaseManager *_dbm;
|
||||
NSString *_databasePath;
|
||||
}
|
||||
|
||||
@property (nonatomic, strong) NSArray *tables;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXTableListViewController
|
||||
|
||||
|
||||
- (instancetype)initWithPath:(NSString *)path
|
||||
{
|
||||
self = [super initWithStyle:UITableViewStyleGrouped];
|
||||
if (self) {
|
||||
_databasePath = [path copy];
|
||||
_dbm = [[FLEXDatabaseManager alloc] initWithPath:path];
|
||||
[_dbm open];
|
||||
[self getAllTables];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)getAllTables
|
||||
{
|
||||
NSArray *resultArray = [_dbm queryAllTables];
|
||||
NSMutableArray *array = [NSMutableArray array];
|
||||
for (NSDictionary *dict in resultArray) {
|
||||
[array addObject:dict[@"name"]];
|
||||
}
|
||||
self.tables = array;
|
||||
}
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
|
||||
{
|
||||
return self.tables.count;
|
||||
}
|
||||
|
||||
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"FLEXTableListViewControllerCell"];
|
||||
if (!cell) {
|
||||
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
|
||||
reuseIdentifier:@"FLEXTableListViewControllerCell"];
|
||||
}
|
||||
cell.textLabel.text = self.tables[indexPath.row];
|
||||
return cell;
|
||||
}
|
||||
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
FLEXTableContentViewController *contentViewController = [[FLEXTableContentViewController alloc] init];
|
||||
|
||||
contentViewController.contentsArray = [_dbm queryAllDataWithTableName:self.tables[indexPath.row]];
|
||||
contentViewController.columnsArray = [_dbm queryAllColumnsWithTableName:self.tables[indexPath.row]];
|
||||
|
||||
contentViewController.title = self.tables[indexPath.row];
|
||||
[self.navigationController pushViewController:contentViewController animated:YES];
|
||||
}
|
||||
|
||||
|
||||
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
|
||||
{
|
||||
return [NSString stringWithFormat:@"%lu tables", (unsigned long)self.tables.count];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,21 @@
|
||||
|
||||
FMDB
|
||||
Copyright (c) 2008-2014 Flying Meat Inc.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
+1
-1
@@ -131,7 +131,7 @@
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
NSString *className = [self.filteredClassNames objectAtIndex:indexPath.row];
|
||||
NSString *className = self.filteredClassNames[indexPath.row];
|
||||
Class selectedClass = objc_getClass([className UTF8String]);
|
||||
FLEXObjectExplorerViewController *objectExplorer = [FLEXObjectExplorerFactory explorerViewControllerForObject:selectedClass];
|
||||
[self.navigationController pushViewController:objectExplorer animated:YES];
|
||||
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// FLEXCookiesTableViewController.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Rich Robinson on 19/10/2015.
|
||||
// Copyright © 2015 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface FLEXCookiesTableViewController : UITableViewController
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,71 @@
|
||||
//
|
||||
// FLEXCookiesTableViewController.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Rich Robinson on 19/10/2015.
|
||||
// Copyright © 2015 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXCookiesTableViewController.h"
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
#import "FLEXUtility.h"
|
||||
|
||||
@interface FLEXCookiesTableViewController ()
|
||||
|
||||
@property (nonatomic, strong) NSArray *cookies;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXCookiesTableViewController
|
||||
|
||||
- (id)initWithStyle:(UITableViewStyle)style {
|
||||
self = [super initWithStyle:style];
|
||||
|
||||
if (self) {
|
||||
self.title = @"Cookies";
|
||||
|
||||
NSSortDescriptor *nameSortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"name" ascending:YES selector:@selector(caseInsensitiveCompare:)];
|
||||
_cookies =[[NSHTTPCookieStorage sharedHTTPCookieStorage].cookies sortedArrayUsingDescriptors:@[nameSortDescriptor]];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSHTTPCookie *)cookieForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
return self.cookies[indexPath.row];
|
||||
}
|
||||
|
||||
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
|
||||
return 1;
|
||||
}
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
|
||||
return self.cookies.count;
|
||||
}
|
||||
|
||||
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
static NSString *CellIdentifier = @"Cell";
|
||||
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
|
||||
if (!cell) {
|
||||
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];
|
||||
cell.textLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
|
||||
cell.detailTextLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
|
||||
cell.detailTextLabel.textColor = [UIColor grayColor];
|
||||
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
|
||||
}
|
||||
|
||||
NSHTTPCookie *cookie = [self cookieForRowAtIndexPath:indexPath];
|
||||
cell.textLabel.text = [NSString stringWithFormat:@"%@ (%@)", cookie.name, cookie.value];
|
||||
cell.detailTextLabel.text = cookie.domain;
|
||||
|
||||
return cell;
|
||||
}
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
|
||||
NSHTTPCookie *cookie = [self cookieForRowAtIndexPath:indexPath];
|
||||
UIViewController *cookieViewController = (UIViewController *)[FLEXObjectExplorerFactory explorerViewControllerForObject:cookie];
|
||||
|
||||
[self.navigationController pushViewController:cookieViewController animated:YES];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,33 @@
|
||||
//
|
||||
// FLEXFileBrowserFileOperationController.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Daniel Rodriguez Troitino on 2/13/15.
|
||||
// Copyright (c) 2015 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@protocol FLEXFileBrowserFileOperationController;
|
||||
|
||||
@protocol FLEXFileBrowserFileOperationControllerDelegate <NSObject>
|
||||
|
||||
- (void)fileOperationControllerDidDismiss:(id<FLEXFileBrowserFileOperationController>)controller;
|
||||
|
||||
@end
|
||||
|
||||
@protocol FLEXFileBrowserFileOperationController <NSObject>
|
||||
|
||||
@property (nonatomic, weak) id<FLEXFileBrowserFileOperationControllerDelegate> delegate;
|
||||
|
||||
- (instancetype)initWithPath:(NSString *)path;
|
||||
|
||||
- (void)show;
|
||||
|
||||
@end
|
||||
|
||||
@interface FLEXFileBrowserFileDeleteOperationController : NSObject <FLEXFileBrowserFileOperationController>
|
||||
@end
|
||||
|
||||
@interface FLEXFileBrowserFileRenameOperationController : NSObject <FLEXFileBrowserFileOperationController>
|
||||
@end
|
||||
@@ -0,0 +1,142 @@
|
||||
//
|
||||
// FLEXFileBrowserFileOperationController.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Daniel Rodriguez Troitino on 2/13/15.
|
||||
// Copyright (c) 2015 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXFileBrowserFileOperationController.h"
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface FLEXFileBrowserFileDeleteOperationController () <UIAlertViewDelegate>
|
||||
|
||||
@property (nonatomic, copy, readonly) NSString *path;
|
||||
|
||||
- (instancetype)initWithPath:(NSString *)path NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXFileBrowserFileDeleteOperationController
|
||||
|
||||
@synthesize delegate = _delegate;
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
return [self initWithPath:nil];
|
||||
}
|
||||
|
||||
- (instancetype)initWithPath:(NSString *)path
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_path = path;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)show
|
||||
{
|
||||
BOOL isDirectory = NO;
|
||||
BOOL stillExists = [[NSFileManager defaultManager] fileExistsAtPath:self.path isDirectory:&isDirectory];
|
||||
|
||||
if (stillExists) {
|
||||
UIAlertView *deleteWarning = [[UIAlertView alloc]
|
||||
initWithTitle:[NSString stringWithFormat:@"Delete %@?", self.path.lastPathComponent]
|
||||
message:[NSString stringWithFormat:@"The %@ will be deleted. This operation cannot be undone", isDirectory ? @"directory" : @"file"]
|
||||
delegate:self
|
||||
cancelButtonTitle:@"Cancel"
|
||||
otherButtonTitles:@"Delete", nil];
|
||||
[deleteWarning show];
|
||||
} else {
|
||||
[[[UIAlertView alloc] initWithTitle:@"File Removed" message:@"The file at the specified path no longer exists." delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - UIAlertViewDelegate
|
||||
|
||||
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
|
||||
{
|
||||
if (buttonIndex == alertView.cancelButtonIndex) {
|
||||
// Nothing, just cancel
|
||||
} else if (buttonIndex == alertView.firstOtherButtonIndex) {
|
||||
[[NSFileManager defaultManager] removeItemAtPath:self.path error:NULL];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex
|
||||
{
|
||||
[self.delegate fileOperationControllerDidDismiss:self];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@interface FLEXFileBrowserFileRenameOperationController () <UIAlertViewDelegate>
|
||||
|
||||
@property (nonatomic, copy, readonly) NSString *path;
|
||||
|
||||
- (instancetype)initWithPath:(NSString *)path NS_DESIGNATED_INITIALIZER;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXFileBrowserFileRenameOperationController
|
||||
|
||||
@synthesize delegate = _delegate;
|
||||
|
||||
- (instancetype)init
|
||||
{
|
||||
return [self initWithPath:nil];
|
||||
}
|
||||
|
||||
- (instancetype)initWithPath:(NSString *)path
|
||||
{
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_path = path;
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)show
|
||||
{
|
||||
BOOL isDirectory = NO;
|
||||
BOOL stillExists = [[NSFileManager defaultManager] fileExistsAtPath:self.path isDirectory:&isDirectory];
|
||||
|
||||
if (stillExists) {
|
||||
UIAlertView *renameDialog = [[UIAlertView alloc]
|
||||
initWithTitle:[NSString stringWithFormat:@"Rename %@?", self.path.lastPathComponent]
|
||||
message:nil
|
||||
delegate:self
|
||||
cancelButtonTitle:@"Cancel"
|
||||
otherButtonTitles:@"Rename", nil];
|
||||
renameDialog.alertViewStyle = UIAlertViewStylePlainTextInput;
|
||||
UITextField *textField = [renameDialog textFieldAtIndex:0];
|
||||
textField.placeholder = @"New file name";
|
||||
textField.text = self.path.lastPathComponent;
|
||||
[renameDialog show];
|
||||
} else {
|
||||
[[[UIAlertView alloc] initWithTitle:@"File Removed" message:@"The file at the specified path no longer exists." delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - UIAlertViewDelegate
|
||||
|
||||
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
|
||||
{
|
||||
if (buttonIndex == alertView.cancelButtonIndex) {
|
||||
// Nothing, just cancel
|
||||
} else if (buttonIndex == alertView.firstOtherButtonIndex) {
|
||||
NSString *newFileName = [alertView textFieldAtIndex:0].text;
|
||||
NSString *newPath = [[self.path stringByDeletingLastPathComponent] stringByAppendingPathComponent:newFileName];
|
||||
[[NSFileManager defaultManager] moveItemAtPath:self.path toPath:newPath error:NULL];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex
|
||||
{
|
||||
[self.delegate fileOperationControllerDidDismiss:self];
|
||||
}
|
||||
|
||||
@end
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
|
||||
#import "FLEXFileBrowserSearchOperation.h"
|
||||
|
||||
@interface FLEXFileBrowserTableViewController : UITableViewController <UISearchDisplayDelegate, FLEXFileBrowserSearchOperationDelegate>
|
||||
@interface FLEXFileBrowserTableViewController : UITableViewController
|
||||
|
||||
- (id)initWithPath:(NSString *)path;
|
||||
|
||||
+146
-64
@@ -7,20 +7,26 @@
|
||||
//
|
||||
|
||||
#import "FLEXFileBrowserTableViewController.h"
|
||||
#import "FLEXFileBrowserFileOperationController.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXWebViewController.h"
|
||||
#import "FLEXImagePreviewViewController.h"
|
||||
#import "FLEXTableListViewController.h"
|
||||
|
||||
@interface FLEXFileBrowserTableViewController ()
|
||||
@interface FLEXFileBrowserTableViewCell : UITableViewCell
|
||||
@end
|
||||
|
||||
@interface FLEXFileBrowserTableViewController () <FLEXFileBrowserFileOperationControllerDelegate, FLEXFileBrowserSearchOperationDelegate, UISearchResultsUpdating, UISearchControllerDelegate>
|
||||
|
||||
@property (nonatomic, copy) NSString *path;
|
||||
@property (nonatomic, copy) NSArray *childPaths;
|
||||
@property (nonatomic, strong) NSArray *searchPaths;
|
||||
@property (nonatomic, strong) NSNumber *recursiveSize;
|
||||
@property (nonatomic, strong) NSNumber *searchPathsSize;
|
||||
@property (nonatomic, strong) UISearchDisplayController *searchController;
|
||||
@property (nonatomic, strong) UISearchController *searchController;
|
||||
@property (nonatomic) NSOperationQueue *operationQueue;
|
||||
@property (nonatomic, strong) UIDocumentInteractionController *documentController;
|
||||
@property (nonatomic, strong) id<FLEXFileBrowserFileOperationController> fileOperationController;
|
||||
|
||||
@end
|
||||
|
||||
@@ -39,13 +45,10 @@
|
||||
self.title = [path lastPathComponent];
|
||||
self.operationQueue = [NSOperationQueue new];
|
||||
|
||||
//add search controller
|
||||
UISearchBar *searchBar = [UISearchBar new];
|
||||
[searchBar sizeToFit];
|
||||
self.searchController = [[UISearchDisplayController alloc] initWithSearchBar:searchBar contentsController:self];
|
||||
self.searchController = [[UISearchController alloc] initWithSearchResultsController:nil];
|
||||
self.searchController.searchResultsUpdater = self;
|
||||
self.searchController.delegate = self;
|
||||
self.searchController.searchResultsDataSource = self;
|
||||
self.searchController.searchResultsDelegate = self;
|
||||
self.searchController.dimsBackgroundDuringPresentation = NO;
|
||||
self.tableView.tableHeaderView = self.searchController.searchBar;
|
||||
|
||||
//computing path size
|
||||
@@ -71,41 +74,46 @@
|
||||
[strongSelf.tableView reloadData];
|
||||
});
|
||||
});
|
||||
|
||||
self.childPaths = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:path error:NULL];
|
||||
|
||||
[self reloadChildPaths];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
#pragma mark - UIViewController
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
[super viewDidLoad];
|
||||
|
||||
UIMenuItem *renameMenuItem = [[UIMenuItem alloc] initWithTitle:@"Rename" action:@selector(fileBrowserRename:)];
|
||||
UIMenuItem *deleteMenuItem = [[UIMenuItem alloc] initWithTitle:@"Delete" action:@selector(fileBrowserDelete:)];
|
||||
[UIMenuController sharedMenuController].menuItems = @[renameMenuItem, deleteMenuItem];
|
||||
}
|
||||
|
||||
#pragma mark - FLEXFileBrowserSearchOperationDelegate
|
||||
|
||||
- (void)fileBrowserSearchOperationResult:(NSArray *)searchResult size:(uint64_t)size
|
||||
{
|
||||
self.searchPaths = searchResult;
|
||||
self.searchPathsSize = @(size);
|
||||
[self.searchController.searchResultsTableView reloadData];
|
||||
[self.tableView reloadData];
|
||||
}
|
||||
|
||||
#pragma mark - UISearchDisplayDelegate
|
||||
#pragma mark - UISearchResultsUpdating
|
||||
|
||||
- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchString:(NSString *)searchString
|
||||
- (void)updateSearchResultsForSearchController:(UISearchController *)searchController
|
||||
{
|
||||
self.searchPaths = nil;
|
||||
self.searchPathsSize = nil;
|
||||
|
||||
//clear pre search request and start a new one
|
||||
[self.operationQueue cancelAllOperations];
|
||||
FLEXFileBrowserSearchOperation *newOperation = [[FLEXFileBrowserSearchOperation alloc] initWithPath:self.path searchString:searchString];
|
||||
newOperation.delegate = self;
|
||||
[self.operationQueue addOperation:newOperation];
|
||||
|
||||
return YES;
|
||||
[self reloadDisplayedPaths];
|
||||
}
|
||||
|
||||
- (void)searchDisplayController:(UISearchDisplayController *)controller willHideSearchResultsTableView:(UITableView *)tableView
|
||||
#pragma mark - UISearchControllerDelegate
|
||||
|
||||
- (void)willDismissSearchController:(UISearchController *)searchController
|
||||
{
|
||||
//confirm to clear all operations
|
||||
[self.operationQueue cancelAllOperations];
|
||||
[self reloadChildPaths];
|
||||
[self.tableView reloadData];
|
||||
}
|
||||
|
||||
|
||||
@@ -118,25 +126,14 @@
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
|
||||
{
|
||||
if (tableView == self.tableView) {
|
||||
return [self.childPaths count];
|
||||
} else {
|
||||
return [self.searchPaths count];
|
||||
}
|
||||
return self.searchController.isActive ? [self.searchPaths count] : [self.childPaths count];
|
||||
}
|
||||
|
||||
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
|
||||
{
|
||||
NSNumber *currentSize = nil;
|
||||
NSArray *currentPaths = nil;
|
||||
|
||||
if (tableView == self.tableView) {
|
||||
currentSize = self.recursiveSize;
|
||||
currentPaths = self.childPaths;
|
||||
} else {
|
||||
currentSize = self.searchPathsSize;
|
||||
currentPaths = self.searchPaths;
|
||||
}
|
||||
BOOL isSearchActive = self.searchController.isActive;
|
||||
NSNumber *currentSize = isSearchActive ? self.searchPathsSize : self.recursiveSize;
|
||||
NSArray *currentPaths = isSearchActive ? self.searchPaths : self.childPaths;
|
||||
|
||||
NSString *sizeString = nil;
|
||||
if (!currentSize) {
|
||||
@@ -150,14 +147,7 @@
|
||||
|
||||
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
NSString *fullPath = nil;
|
||||
if (tableView == self.tableView) {
|
||||
NSString *subpath = [self.childPaths objectAtIndex:indexPath.row];
|
||||
fullPath = [self.path stringByAppendingPathComponent:subpath];
|
||||
} else {
|
||||
fullPath = [self.searchPaths objectAtIndex:indexPath.row];
|
||||
}
|
||||
|
||||
NSString *fullPath = [self filePathAtIndexPath:indexPath];
|
||||
NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:fullPath error:NULL];
|
||||
BOOL isDirectory = [[attributes fileType] isEqual:NSFileTypeDirectory];
|
||||
NSString *subtitle = nil;
|
||||
@@ -178,7 +168,7 @@
|
||||
NSString *cellIdentifier = showImagePreview ? imageCellIdentifier : textCellIdentifier;
|
||||
|
||||
if (!cell) {
|
||||
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:cellIdentifier];
|
||||
cell = [[FLEXFileBrowserTableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:cellIdentifier];
|
||||
cell.textLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
|
||||
cell.detailTextLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
|
||||
cell.detailTextLabel.textColor = [UIColor grayColor];
|
||||
@@ -198,16 +188,8 @@
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
NSString *subpath = nil;
|
||||
NSString *fullPath = nil;
|
||||
|
||||
if (tableView == self.tableView) {
|
||||
subpath = [self.childPaths objectAtIndex:indexPath.row];
|
||||
fullPath = [self.path stringByAppendingPathComponent:subpath];
|
||||
} else {
|
||||
fullPath = [self.searchPaths objectAtIndex:indexPath.row];
|
||||
subpath = [fullPath lastPathComponent];
|
||||
}
|
||||
NSString *fullPath = [self filePathAtIndexPath:indexPath];
|
||||
NSString *subpath = [fullPath lastPathComponent];
|
||||
|
||||
BOOL isDirectory = NO;
|
||||
BOOL stillExists = [[NSFileManager defaultManager] fileExistsAtPath:fullPath isDirectory:&isDirectory];
|
||||
@@ -224,9 +206,7 @@
|
||||
if ([[subpath pathExtension] isEqual:@"archive"]) {
|
||||
prettyString = [[NSKeyedUnarchiver unarchiveObjectWithFile:fullPath] description];
|
||||
} 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"]) {
|
||||
NSData *fileData = [NSData dataWithContentsOfFile:fullPath];
|
||||
prettyString = [[NSPropertyListSerialization propertyListWithData:fileData options:0 format:NULL error:NULL] description];
|
||||
@@ -236,7 +216,10 @@
|
||||
drillInViewController = [[FLEXWebViewController alloc] initWithText:prettyString];
|
||||
} else if ([FLEXWebViewController supportsPathExtension:[subpath pathExtension]]) {
|
||||
drillInViewController = [[FLEXWebViewController alloc] initWithURL:[NSURL fileURLWithPath:fullPath]];
|
||||
} else {
|
||||
} else if ([[subpath pathExtension] isEqualToString:@"db"]) {
|
||||
drillInViewController = [[FLEXTableListViewController alloc] initWithPath:fullPath];
|
||||
}
|
||||
else {
|
||||
NSString *fileString = [NSString stringWithContentsOfFile:fullPath encoding:NSUTF8StringEncoding error:NULL];
|
||||
if ([fileString length] > 0) {
|
||||
drillInViewController = [[FLEXWebViewController alloc] initWithText:fileString];
|
||||
@@ -253,16 +236,115 @@
|
||||
}
|
||||
} else {
|
||||
[[[UIAlertView alloc] initWithTitle:@"File Removed" message:@"The file at the specified path no longer exists." delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show];
|
||||
[self reloadDisplayedPaths];
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)tableView:(UITableView *)tableView shouldShowMenuForRowAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)tableView:(UITableView *)tableView canPerformAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender
|
||||
{
|
||||
return action == @selector(fileBrowserDelete:) || action == @selector(fileBrowserRename:);
|
||||
}
|
||||
|
||||
- (void)tableView:(UITableView *)tableView performAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender
|
||||
{
|
||||
// Empty, but has to exist for the menu to show
|
||||
// The table view only calls this method for actions in the UIResponderStandardEditActions informal protocol.
|
||||
// Since our actions are outside of that protocol, we need to manually handle the action forwarding from the cells.
|
||||
}
|
||||
|
||||
#pragma mark - FLEXFileBrowserFileOperationControllerDelegate
|
||||
|
||||
- (void)fileOperationControllerDidDismiss:(id<FLEXFileBrowserFileOperationController>)controller
|
||||
{
|
||||
[self reloadDisplayedPaths];
|
||||
}
|
||||
|
||||
- (void)openFileController:(NSString *)fullPath
|
||||
{
|
||||
UIDocumentInteractionController *controller = [UIDocumentInteractionController new];
|
||||
controller.URL = [[NSURL alloc] initFileURLWithPath:fullPath];
|
||||
|
||||
|
||||
[controller presentOptionsMenuFromRect:self.view.bounds inView:self.view animated:YES];
|
||||
self.documentController = controller;
|
||||
}
|
||||
|
||||
- (void)fileBrowserRename:(UITableViewCell *)sender
|
||||
{
|
||||
NSIndexPath *indexPath = [self.tableView indexPathForCell:sender];
|
||||
NSString *fullPath = [self filePathAtIndexPath:indexPath];
|
||||
|
||||
self.fileOperationController = [[FLEXFileBrowserFileRenameOperationController alloc] initWithPath:fullPath];
|
||||
self.fileOperationController.delegate = self;
|
||||
[self.fileOperationController show];
|
||||
}
|
||||
|
||||
- (void)fileBrowserDelete:(UITableViewCell *)sender
|
||||
{
|
||||
NSIndexPath *indexPath = [self.tableView indexPathForCell:sender];
|
||||
NSString *fullPath = [self filePathAtIndexPath:indexPath];
|
||||
|
||||
self.fileOperationController = [[FLEXFileBrowserFileDeleteOperationController alloc] initWithPath:fullPath];
|
||||
self.fileOperationController.delegate = self;
|
||||
[self.fileOperationController show];
|
||||
}
|
||||
|
||||
- (void)reloadDisplayedPaths
|
||||
{
|
||||
if (self.searchController.isActive) {
|
||||
[self reloadSearchPaths];
|
||||
} else {
|
||||
[self reloadChildPaths];
|
||||
}
|
||||
[self.tableView reloadData];
|
||||
}
|
||||
|
||||
- (void)reloadChildPaths
|
||||
{
|
||||
NSMutableArray *childPaths = [NSMutableArray array];
|
||||
NSArray *subpaths = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:self.path error:NULL];
|
||||
for (NSString *subpath in subpaths) {
|
||||
[childPaths addObject:[self.path stringByAppendingPathComponent:subpath]];
|
||||
}
|
||||
self.childPaths = childPaths;
|
||||
}
|
||||
|
||||
- (void)reloadSearchPaths
|
||||
{
|
||||
self.searchPaths = nil;
|
||||
self.searchPathsSize = nil;
|
||||
|
||||
//clear pre search request and start a new one
|
||||
[self.operationQueue cancelAllOperations];
|
||||
FLEXFileBrowserSearchOperation *newOperation = [[FLEXFileBrowserSearchOperation alloc] initWithPath:self.path searchString:self.searchController.searchBar.text];
|
||||
newOperation.delegate = self;
|
||||
[self.operationQueue addOperation:newOperation];
|
||||
}
|
||||
|
||||
- (NSString *)filePathAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
return self.searchController.isActive ? self.searchPaths[indexPath.row] : self.childPaths[indexPath.row];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@implementation FLEXFileBrowserTableViewCell
|
||||
|
||||
- (void)fileBrowserRename:(UIMenuController *)sender
|
||||
{
|
||||
id target = [self.nextResponder targetForAction:_cmd withSender:sender];
|
||||
[[UIApplication sharedApplication] sendAction:_cmd to:target from:self forEvent:nil];
|
||||
}
|
||||
|
||||
- (void)fileBrowserDelete:(UIMenuController *)sender
|
||||
{
|
||||
id target = [self.nextResponder targetForAction:_cmd withSender:sender];
|
||||
[[UIApplication sharedApplication] sendAction:_cmd to:target from:self forEvent:nil];
|
||||
}
|
||||
|
||||
@end
|
||||
+35
-2
@@ -14,14 +14,20 @@
|
||||
#import "FLEXObjectExplorerFactory.h"
|
||||
#import "FLEXLiveObjectsTableViewController.h"
|
||||
#import "FLEXFileBrowserTableViewController.h"
|
||||
#import "FLEXCookiesTableViewController.h"
|
||||
#import "FLEXGlobalsTableViewControllerEntry.h"
|
||||
#import "FLEXManager+Private.h"
|
||||
#import "FLEXSystemLogTableViewController.h"
|
||||
#import "FLEXNetworkHistoryTableViewController.h"
|
||||
|
||||
static __weak UIWindow *s_applicationWindow = nil;
|
||||
|
||||
typedef NS_ENUM(NSUInteger, FLEXGlobalsRow) {
|
||||
FLEXGlobalsRowNetworkHistory,
|
||||
FLEXGlobalsRowSystemLog,
|
||||
FLEXGlobalsRowLiveObjects,
|
||||
FLEXGlobalsRowFileBrowser,
|
||||
FLEXGlobalsCookies,
|
||||
FLEXGlobalsRowSystemLibraries,
|
||||
FLEXGlobalsRowAppClasses,
|
||||
FLEXGlobalsRowAppDelegate,
|
||||
@@ -158,6 +164,15 @@ typedef NS_ENUM(NSUInteger, FLEXGlobalsRow) {
|
||||
};
|
||||
break;
|
||||
|
||||
case FLEXGlobalsCookies:
|
||||
titleFuture = ^NSString *{
|
||||
return @"🍪 Cookies";
|
||||
};
|
||||
viewControllerFuture = ^UIViewController *{
|
||||
return [[FLEXCookiesTableViewController alloc] init];
|
||||
};
|
||||
break;
|
||||
|
||||
case FLEXGlobalsRowFileBrowser:
|
||||
titleFuture = ^NSString *{
|
||||
return @"📁 File Browser";
|
||||
@@ -166,6 +181,24 @@ typedef NS_ENUM(NSUInteger, FLEXGlobalsRow) {
|
||||
return [[FLEXFileBrowserTableViewController alloc] init];
|
||||
};
|
||||
break;
|
||||
|
||||
case FLEXGlobalsRowSystemLog:
|
||||
titleFuture = ^{
|
||||
return @"⚠️ System Log";
|
||||
};
|
||||
viewControllerFuture = ^{
|
||||
return [[FLEXSystemLogTableViewController alloc] init];
|
||||
};
|
||||
break;
|
||||
|
||||
case FLEXGlobalsRowNetworkHistory:
|
||||
titleFuture = ^{
|
||||
return @"📡 Network History";
|
||||
};
|
||||
viewControllerFuture = ^{
|
||||
return [[FLEXNetworkHistoryTableViewController alloc] init];
|
||||
};
|
||||
break;
|
||||
case FLEXGlobalsRowCount:
|
||||
break;
|
||||
}
|
||||
@@ -183,7 +216,7 @@ typedef NS_ENUM(NSUInteger, FLEXGlobalsRow) {
|
||||
{
|
||||
self = [super initWithStyle:style];
|
||||
if (self) {
|
||||
self.title = @"🌎 Global State";
|
||||
self.title = @"💪 FLEX";
|
||||
_entries = [[[self class] defaultGlobalEntries] arrayByAddingObjectsFromArray:[FLEXManager sharedManager].userGlobalEntries];
|
||||
}
|
||||
return self;
|
||||
@@ -252,7 +285,7 @@ typedef NS_ENUM(NSUInteger, FLEXGlobalsRow) {
|
||||
if (!cell) {
|
||||
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
|
||||
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
|
||||
cell.textLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
|
||||
cell.textLabel.font = [FLEXUtility defaultFontOfSize:14.0];
|
||||
}
|
||||
|
||||
cell.textLabel.text = [self titleForRowAtIndexPath:indexPath];
|
||||
+3
-3
@@ -100,10 +100,10 @@
|
||||
cell.detailTextLabel.textColor = [UIColor grayColor];
|
||||
}
|
||||
|
||||
id instance = [self.instances objectAtIndex:indexPath.row];
|
||||
id instance = self.instances[indexPath.row];
|
||||
NSString *title = nil;
|
||||
if ((NSInteger)[self.fieldNames count] > indexPath.row) {
|
||||
title = [NSString stringWithFormat:@"%@ %@", NSStringFromClass(object_getClass(instance)), [self.fieldNames objectAtIndex:indexPath.row]];
|
||||
title = [NSString stringWithFormat:@"%@ %@", NSStringFromClass(object_getClass(instance)), self.fieldNames[indexPath.row]];
|
||||
} else {
|
||||
title = [NSString stringWithFormat:@"%@ %p", NSStringFromClass(object_getClass(instance)), instance];
|
||||
}
|
||||
@@ -118,7 +118,7 @@
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
id instance = [self.instances objectAtIndex:indexPath.row];
|
||||
id instance = self.instances[indexPath.row];
|
||||
FLEXObjectExplorerViewController *drillInViewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:instance];
|
||||
[self.navigationController pushViewController:drillInViewController animated:YES];
|
||||
}
|
||||
+6
-6
@@ -100,11 +100,11 @@ static const NSInteger kFLEXLiveObjectsSortByCountIndex = 1;
|
||||
|
||||
NSUInteger totalCount = 0;
|
||||
for (NSString *className in self.allClassNames) {
|
||||
totalCount += [[self.instanceCountsForClassNames objectForKey:className] unsignedIntegerValue];
|
||||
totalCount += [self.instanceCountsForClassNames[className] unsignedIntegerValue];
|
||||
}
|
||||
NSUInteger filteredCount = 0;
|
||||
for (NSString *className in self.filteredClassNames) {
|
||||
filteredCount += [[self.instanceCountsForClassNames objectForKey:className] unsignedIntegerValue];
|
||||
filteredCount += [self.instanceCountsForClassNames[className] unsignedIntegerValue];
|
||||
}
|
||||
|
||||
if (filteredCount == totalCount) {
|
||||
@@ -154,8 +154,8 @@ static const NSInteger kFLEXLiveObjectsSortByCountIndex = 1;
|
||||
self.filteredClassNames = [self.filteredClassNames sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)];
|
||||
} else if (self.searchBar.selectedScopeButtonIndex == kFLEXLiveObjectsSortByCountIndex) {
|
||||
self.filteredClassNames = [self.filteredClassNames sortedArrayUsingComparator:^NSComparisonResult(NSString *className1, NSString *className2) {
|
||||
NSNumber *count1 = [self.instanceCountsForClassNames objectForKey:className1];
|
||||
NSNumber *count2 = [self.instanceCountsForClassNames objectForKey:className2];
|
||||
NSNumber *count1 = self.instanceCountsForClassNames[className1];
|
||||
NSNumber *count2 = self.instanceCountsForClassNames[className2];
|
||||
// Reversed for descending counts.
|
||||
return [count2 compare:count1];
|
||||
}];
|
||||
@@ -189,7 +189,7 @@ static const NSInteger kFLEXLiveObjectsSortByCountIndex = 1;
|
||||
}
|
||||
|
||||
NSString *className = self.filteredClassNames[indexPath.row];
|
||||
NSNumber *count = [self.instanceCountsForClassNames objectForKey:className];
|
||||
NSNumber *count = self.instanceCountsForClassNames[className];
|
||||
cell.textLabel.text = [NSString stringWithFormat:@"%@ (%ld)", className, (long)[count integerValue]];
|
||||
|
||||
return cell;
|
||||
@@ -200,7 +200,7 @@ static const NSInteger kFLEXLiveObjectsSortByCountIndex = 1;
|
||||
|
||||
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
NSString *className = [self.filteredClassNames objectAtIndex:indexPath.row];
|
||||
NSString *className = self.filteredClassNames[indexPath.row];
|
||||
FLEXInstancesTableViewController *instancesViewController = [FLEXInstancesTableViewController instancesTableViewControllerForClassName:className];
|
||||
[self.navigationController pushViewController:instancesViewController animated:YES];
|
||||
}
|
||||
+10
-1
@@ -35,7 +35,7 @@
|
||||
self = [self initWithNibName:nil bundle:nil];
|
||||
if (self) {
|
||||
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];
|
||||
}
|
||||
return self;
|
||||
@@ -51,6 +51,14 @@
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)dealloc
|
||||
{
|
||||
// UIWebView's delegate is assign so we need to clear it manually.
|
||||
if (_webView.delegate == self) {
|
||||
_webView.delegate = nil;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
[super viewDidLoad];
|
||||
@@ -82,6 +90,7 @@
|
||||
// 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.
|
||||
FLEXWebViewController *webVC = [[[self class] alloc] initWithURL:[request URL]];
|
||||
webVC.title = [[request URL] absoluteString];
|
||||
[self.navigationController pushViewController:webVC animated:YES];
|
||||
}
|
||||
return shouldStart;
|
||||
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// FLEXSystemLogMessage.h
|
||||
// UICatalog
|
||||
//
|
||||
// Created by Ryan Olson on 1/25/15.
|
||||
// Copyright (c) 2015 f. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <asl.h>
|
||||
|
||||
@interface FLEXSystemLogMessage : NSObject
|
||||
|
||||
+ (instancetype)logMessageFromASLMessage:(aslmsg)aslMessage;
|
||||
|
||||
@property (nonatomic, strong) NSDate *date;
|
||||
@property (nonatomic, copy) NSString *sender;
|
||||
@property (nonatomic, copy) NSString *messageText;
|
||||
@property (nonatomic, assign) long long messageID;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,55 @@
|
||||
//
|
||||
// FLEXSystemLogMessage.m
|
||||
// UICatalog
|
||||
//
|
||||
// Created by Ryan Olson on 1/25/15.
|
||||
// Copyright (c) 2015 f. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXSystemLogMessage.h"
|
||||
|
||||
@implementation FLEXSystemLogMessage
|
||||
|
||||
+(instancetype)logMessageFromASLMessage:(aslmsg)aslMessage
|
||||
{
|
||||
FLEXSystemLogMessage *logMessage = [[FLEXSystemLogMessage alloc] init];
|
||||
|
||||
const char *timestamp = asl_get(aslMessage, ASL_KEY_TIME);
|
||||
if (timestamp) {
|
||||
NSTimeInterval timeInterval = [@(timestamp) integerValue];
|
||||
const char *nanoseconds = asl_get(aslMessage, ASL_KEY_TIME_NSEC);
|
||||
if (nanoseconds) {
|
||||
timeInterval += [@(nanoseconds) doubleValue] / NSEC_PER_SEC;
|
||||
}
|
||||
logMessage.date = [NSDate dateWithTimeIntervalSince1970:timeInterval];
|
||||
}
|
||||
|
||||
const char *sender = asl_get(aslMessage, ASL_KEY_SENDER);
|
||||
if (sender) {
|
||||
logMessage.sender = @(sender);
|
||||
}
|
||||
|
||||
const char *messageText = asl_get(aslMessage, ASL_KEY_MSG);
|
||||
if (messageText) {
|
||||
logMessage.messageText = @(messageText);
|
||||
}
|
||||
|
||||
const char *messageID = asl_get(aslMessage, ASL_KEY_MSG_ID);
|
||||
if (messageID) {
|
||||
logMessage.messageID = [@(messageID) longLongValue];
|
||||
}
|
||||
|
||||
return logMessage;
|
||||
}
|
||||
|
||||
- (BOOL)isEqual:(id)object
|
||||
{
|
||||
return [object isKindOfClass:[FLEXSystemLogMessage class]] && self.messageID == [object messageID];
|
||||
}
|
||||
|
||||
- (NSUInteger)hash
|
||||
{
|
||||
return (NSUInteger)self.messageID;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,23 @@
|
||||
//
|
||||
// FLEXSystemLogTableViewCell.h
|
||||
// UICatalog
|
||||
//
|
||||
// Created by Ryan Olson on 1/25/15.
|
||||
// Copyright (c) 2015 f. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@class FLEXSystemLogMessage;
|
||||
|
||||
extern NSString *const kFLEXSystemLogTableViewCellIdentifier;
|
||||
|
||||
@interface FLEXSystemLogTableViewCell : UITableViewCell
|
||||
|
||||
@property (nonatomic, strong) FLEXSystemLogMessage *logMessage;
|
||||
@property (nonatomic, copy) NSString *highlightedText;
|
||||
|
||||
+ (NSString *)displayedTextForLogMessage:(FLEXSystemLogMessage *)logMessage;
|
||||
+ (CGFloat)preferredHeightForLogMessage:(FLEXSystemLogMessage *)logMessage inWidth:(CGFloat)width;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,128 @@
|
||||
//
|
||||
// FLEXSystemLogTableViewCell.m
|
||||
// UICatalog
|
||||
//
|
||||
// Created by Ryan Olson on 1/25/15.
|
||||
// Copyright (c) 2015 f. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXSystemLogTableViewCell.h"
|
||||
#import "FLEXSystemLogMessage.h"
|
||||
|
||||
NSString *const kFLEXSystemLogTableViewCellIdentifier = @"FLEXSystemLogTableViewCellIdentifier";
|
||||
|
||||
@interface FLEXSystemLogTableViewCell ()
|
||||
|
||||
@property (nonatomic, strong) UILabel *logMessageLabel;
|
||||
@property (nonatomic, strong) NSAttributedString *logMessageAttributedText;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXSystemLogTableViewCell
|
||||
|
||||
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
|
||||
{
|
||||
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
|
||||
if (self) {
|
||||
self.logMessageLabel = [[UILabel alloc] init];
|
||||
self.logMessageLabel.numberOfLines = 0;
|
||||
self.separatorInset = UIEdgeInsetsZero;
|
||||
self.selectionStyle = UITableViewCellSelectionStyleNone;
|
||||
[self.contentView addSubview:self.logMessageLabel];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)setLogMessage:(FLEXSystemLogMessage *)logMessage
|
||||
{
|
||||
if (![_logMessage isEqual:logMessage]) {
|
||||
_logMessage = logMessage;
|
||||
self.logMessageAttributedText = nil;
|
||||
[self setNeedsLayout];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)setHighlightedText:(NSString *)highlightedText
|
||||
{
|
||||
if (![_highlightedText isEqual:highlightedText]) {
|
||||
_highlightedText = highlightedText;
|
||||
self.logMessageAttributedText = nil;
|
||||
[self setNeedsLayout];
|
||||
}
|
||||
}
|
||||
|
||||
- (NSAttributedString *)logMessageAttributedText
|
||||
{
|
||||
if (!_logMessageAttributedText) {
|
||||
_logMessageAttributedText = [[self class] attributedTextForLogMessage:self.logMessage highlightedText:self.highlightedText];
|
||||
}
|
||||
return _logMessageAttributedText;
|
||||
}
|
||||
|
||||
static const UIEdgeInsets kFLEXLogMessageCellInsets = {10.0, 10.0, 10.0, 10.0};
|
||||
|
||||
- (void)layoutSubviews
|
||||
{
|
||||
[super layoutSubviews];
|
||||
|
||||
self.logMessageLabel.attributedText = self.logMessageAttributedText;
|
||||
self.logMessageLabel.frame = UIEdgeInsetsInsetRect(self.contentView.bounds, kFLEXLogMessageCellInsets);
|
||||
}
|
||||
|
||||
#pragma mark - Stateless helpers
|
||||
|
||||
+ (NSAttributedString *)attributedTextForLogMessage:(FLEXSystemLogMessage *)logMessage highlightedText:(NSString *)highlightedText
|
||||
{
|
||||
NSString *text = [self displayedTextForLogMessage:logMessage];
|
||||
NSDictionary *attributes = @{ NSFontAttributeName : [UIFont fontWithName:@"CourierNewPSMT" size:12.0] };
|
||||
NSAttributedString *attributedText = [[NSAttributedString alloc] initWithString:text attributes:attributes];
|
||||
|
||||
if ([highlightedText length] > 0) {
|
||||
NSMutableAttributedString *mutableAttributedText = [attributedText mutableCopy];
|
||||
NSMutableDictionary *highlightAttributes = [@{ NSBackgroundColorAttributeName : [UIColor yellowColor] } mutableCopy];
|
||||
[highlightAttributes addEntriesFromDictionary:attributes];
|
||||
|
||||
NSRange remainingSearchRange = NSMakeRange(0, text.length);
|
||||
while (remainingSearchRange.location < text.length) {
|
||||
remainingSearchRange.length = text.length - remainingSearchRange.location;
|
||||
NSRange foundRange = [text rangeOfString:highlightedText options:NSCaseInsensitiveSearch range:remainingSearchRange];
|
||||
if (foundRange.location != NSNotFound) {
|
||||
remainingSearchRange.location = foundRange.location + foundRange.length;
|
||||
[mutableAttributedText setAttributes:highlightAttributes range:foundRange];
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
attributedText = mutableAttributedText;
|
||||
}
|
||||
|
||||
return attributedText;
|
||||
}
|
||||
|
||||
+ (NSString *)displayedTextForLogMessage:(FLEXSystemLogMessage *)logMessage
|
||||
{
|
||||
return [NSString stringWithFormat:@"%@: %@", [self logTimeStringFromDate:logMessage.date], logMessage.messageText];
|
||||
}
|
||||
|
||||
+ (CGFloat)preferredHeightForLogMessage:(FLEXSystemLogMessage *)logMessage inWidth:(CGFloat)width
|
||||
{
|
||||
UIEdgeInsets insets = kFLEXLogMessageCellInsets;
|
||||
CGFloat availableWidth = width - insets.left - insets.right;
|
||||
NSAttributedString *attributedLogText = [self attributedTextForLogMessage:logMessage highlightedText:nil];
|
||||
CGSize labelSize = [attributedLogText boundingRectWithSize:CGSizeMake(availableWidth, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading context:nil].size;
|
||||
return labelSize.height + insets.top + insets.bottom;
|
||||
}
|
||||
|
||||
+ (NSString *)logTimeStringFromDate:(NSDate *)date
|
||||
{
|
||||
static NSDateFormatter *formatter = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
formatter = [[NSDateFormatter alloc] init];
|
||||
formatter.dateFormat = @"yyyy-MM-dd HH:mm:ss.SSS";
|
||||
});
|
||||
|
||||
return [formatter stringFromDate:date];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// FLEXSystemLogTableViewController.h
|
||||
// UICatalog
|
||||
//
|
||||
// Created by Ryan Olson on 1/19/15.
|
||||
// Copyright (c) 2015 f. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface FLEXSystemLogTableViewController : UITableViewController
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,209 @@
|
||||
//
|
||||
// FLEXSystemLogTableViewController.m
|
||||
// UICatalog
|
||||
//
|
||||
// Created by Ryan Olson on 1/19/15.
|
||||
// Copyright (c) 2015 f. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXSystemLogTableViewController.h"
|
||||
#import "FLEXUtility.h"
|
||||
#import "FLEXSystemLogMessage.h"
|
||||
#import "FLEXSystemLogTableViewCell.h"
|
||||
#import <asl.h>
|
||||
|
||||
@interface FLEXSystemLogTableViewController () <UISearchResultsUpdating, UISearchControllerDelegate>
|
||||
|
||||
@property (nonatomic, strong) UISearchController *searchController;
|
||||
@property (nonatomic, copy) NSArray *logMessages;
|
||||
@property (nonatomic, copy) NSArray *filteredLogMessages;
|
||||
@property (nonatomic, strong) NSTimer *logUpdateTimer;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXSystemLogTableViewController
|
||||
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
[super viewDidLoad];
|
||||
|
||||
[self.tableView registerClass:[FLEXSystemLogTableViewCell class] forCellReuseIdentifier:kFLEXSystemLogTableViewCellIdentifier];
|
||||
self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
|
||||
self.title = @"Loading...";
|
||||
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@" ⬇︎ " style:UIBarButtonItemStylePlain target:self action:@selector(scrollToLastRow)];
|
||||
|
||||
self.searchController = [[UISearchController alloc] initWithSearchResultsController:nil];
|
||||
self.searchController.delegate = self;
|
||||
self.searchController.searchResultsUpdater = self;
|
||||
self.searchController.dimsBackgroundDuringPresentation = NO;
|
||||
self.tableView.tableHeaderView = self.searchController.searchBar;
|
||||
|
||||
[self updateLogMessages];
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated
|
||||
{
|
||||
[super viewWillAppear:animated];
|
||||
|
||||
NSTimeInterval updateInterval = 1.0;
|
||||
|
||||
#if TARGET_IPHONE_SIMULATOR
|
||||
// Querrying the ASL is much slower in the simulator. We need a longer polling interval to keep things repsonsive.
|
||||
updateInterval = 5.0;
|
||||
#endif
|
||||
|
||||
self.logUpdateTimer = [NSTimer scheduledTimerWithTimeInterval:updateInterval target:self selector:@selector(updateLogMessages) userInfo:nil repeats:YES];
|
||||
}
|
||||
|
||||
- (void)viewWillDisappear:(BOOL)animated
|
||||
{
|
||||
[super viewWillDisappear:animated];
|
||||
|
||||
[self.logUpdateTimer invalidate];
|
||||
}
|
||||
|
||||
- (void)updateLogMessages
|
||||
{
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
NSArray *logMessages = [[self class] allLogMessagesForCurrentProcess];
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
self.title = @"System Log";
|
||||
self.logMessages = logMessages;
|
||||
|
||||
// "Follow" the log as new messages stream in if we were previously near the bottom.
|
||||
BOOL wasNearBottom = self.tableView.contentOffset.y >= self.tableView.contentSize.height - self.tableView.frame.size.height - 100.0;
|
||||
[self.tableView reloadData];
|
||||
if (wasNearBottom) {
|
||||
[self scrollToLastRow];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
- (void)scrollToLastRow
|
||||
{
|
||||
NSInteger numberOfRows = [self.tableView numberOfRowsInSection:0];
|
||||
if (numberOfRows > 0) {
|
||||
NSIndexPath *lastIndexPath = [NSIndexPath indexPathForRow:numberOfRows - 1 inSection:0];
|
||||
[self.tableView scrollToRowAtIndexPath:lastIndexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Table view data source
|
||||
|
||||
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
|
||||
{
|
||||
return self.searchController.isActive ? [self.filteredLogMessages count] : [self.logMessages count];
|
||||
}
|
||||
|
||||
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
FLEXSystemLogTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kFLEXSystemLogTableViewCellIdentifier forIndexPath:indexPath];
|
||||
cell.logMessage = [self logMessageAtIndexPath:indexPath];
|
||||
cell.highlightedText = self.searchController.searchBar.text;
|
||||
|
||||
if (indexPath.row % 2 == 0) {
|
||||
cell.backgroundColor = [UIColor colorWithWhite:0.95 alpha:1.0];
|
||||
} else {
|
||||
cell.backgroundColor = [UIColor whiteColor];
|
||||
}
|
||||
|
||||
return cell;
|
||||
}
|
||||
|
||||
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
FLEXSystemLogMessage *logMessage = [self logMessageAtIndexPath:indexPath];
|
||||
return [FLEXSystemLogTableViewCell preferredHeightForLogMessage:logMessage inWidth:self.tableView.bounds.size.width];
|
||||
}
|
||||
|
||||
#pragma mark - Copy on long press
|
||||
|
||||
- (BOOL)tableView:(UITableView *)tableView shouldShowMenuForRowAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)tableView:(UITableView *)tableView canPerformAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender
|
||||
{
|
||||
return action == @selector(copy:);
|
||||
}
|
||||
|
||||
- (void)tableView:(UITableView *)tableView performAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender
|
||||
{
|
||||
if (action == @selector(copy:)) {
|
||||
FLEXSystemLogMessage *logMessage = [self logMessageAtIndexPath:indexPath];
|
||||
NSString *stringToCopy = [FLEXSystemLogTableViewCell displayedTextForLogMessage:logMessage] ?: @"";
|
||||
[[UIPasteboard generalPasteboard] setString:stringToCopy];
|
||||
}
|
||||
}
|
||||
|
||||
- (FLEXSystemLogMessage *)logMessageAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
return self.searchController.isActive ? self.filteredLogMessages[indexPath.row] : self.logMessages[indexPath.row];
|
||||
}
|
||||
|
||||
#pragma mark - UISearchResultsUpdating
|
||||
|
||||
- (void)updateSearchResultsForSearchController:(UISearchController *)searchController
|
||||
{
|
||||
NSString *searchString = searchController.searchBar.text;
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
NSArray *filteredLogMessages = [self.logMessages filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(FLEXSystemLogMessage *logMessage, NSDictionary *bindings) {
|
||||
NSString *displayedText = [FLEXSystemLogTableViewCell displayedTextForLogMessage:logMessage];
|
||||
return [displayedText rangeOfString:searchString options:NSCaseInsensitiveSearch].length > 0;
|
||||
}]];
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if ([searchController.searchBar.text isEqual:searchString]) {
|
||||
self.filteredLogMessages = filteredLogMessages;
|
||||
[self.tableView reloadData];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark - Log Message Fetching
|
||||
|
||||
// Due to a mistake in asl.h, things get a little messy. We need to mark these symbols as weak since they won't exist on iOS 7 despite the compiler thinking otherwise.
|
||||
// asl.h in the iOS 8.1 SDK claims that asl_next() and asl_release() were introduced in iOS 7 to replace aslresponse_next() and aslresponse_free(). However, they were actually added in iOS 8.0.
|
||||
extern aslmsg asl_next(asl_object_t obj) __attribute__((weak_import));
|
||||
extern void asl_release(asl_object_t obj) __attribute__((weak_import));
|
||||
|
||||
+ (NSArray *)allLogMessagesForCurrentProcess
|
||||
{
|
||||
asl_object_t query = asl_new(ASL_TYPE_QUERY);
|
||||
|
||||
// Filter for messages from the current process. Note that this appears to happen by default on device, but is required in the simulator.
|
||||
NSString *pidString = [NSString stringWithFormat:@"%d", [[NSProcessInfo processInfo] processIdentifier]];
|
||||
asl_set_query(query, ASL_KEY_PID, [pidString UTF8String], ASL_QUERY_OP_EQUAL);
|
||||
|
||||
aslresponse response = asl_search(NULL, query);
|
||||
aslmsg aslMessage = NULL;
|
||||
|
||||
NSMutableArray *logMessages = [NSMutableArray array];
|
||||
|
||||
if (&asl_next != NULL && &asl_release != NULL) {
|
||||
while ((aslMessage = asl_next(response))) {
|
||||
[logMessages addObject:[FLEXSystemLogMessage logMessageFromASLMessage:aslMessage]];
|
||||
}
|
||||
asl_release(response);
|
||||
} else {
|
||||
// Mute incorrect deprecated warnings. We'll need the "deprecated" functions on iOS 7, where their replacements don't yet exist.
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
while ((aslMessage = aslresponse_next(response))) {
|
||||
[logMessages addObject:[FLEXSystemLogMessage logMessageFromASLMessage:aslMessage]];
|
||||
}
|
||||
aslresponse_free(response);
|
||||
#pragma clang diagnostic pop
|
||||
}
|
||||
|
||||
return logMessages;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>FMWK</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string></string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -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
|
||||
@@ -0,0 +1,360 @@
|
||||
//
|
||||
// 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 () <UISearchResultsUpdating, UISearchControllerDelegate>
|
||||
|
||||
/// 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;
|
||||
@property (nonatomic, assign) BOOL isPresentingSearch;
|
||||
|
||||
@property (nonatomic, strong) UISearchController *searchController;
|
||||
|
||||
@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];
|
||||
|
||||
self.searchController = [[UISearchController alloc] initWithSearchResultsController:nil];
|
||||
self.searchController.delegate = self;
|
||||
self.searchController.searchResultsUpdater = self;
|
||||
self.searchController.dimsBackgroundDuringPresentation = NO;
|
||||
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 updateFirstSectionHeader];
|
||||
}
|
||||
|
||||
- (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 updateFirstSectionHeader];
|
||||
}
|
||||
|
||||
- (void)updateFirstSectionHeader
|
||||
{
|
||||
UIView *view = [self.tableView headerViewForSection:0];
|
||||
if ([view isKindOfClass:[UITableViewHeaderFooterView class]]) {
|
||||
UITableViewHeaderFooterView *headerView = (UITableViewHeaderFooterView *)view;
|
||||
headerView.textLabel.text = [self headerText];
|
||||
[headerView setNeedsLayout];
|
||||
}
|
||||
}
|
||||
|
||||
- (NSString *)headerText
|
||||
{
|
||||
NSString *headerText = nil;
|
||||
if ([FLEXNetworkObserver isEnabled]) {
|
||||
long long bytesReceived = 0;
|
||||
NSInteger totalRequests = 0;
|
||||
if (self.searchController.isActive) {
|
||||
bytesReceived = self.filteredBytesReceived;
|
||||
totalRequests = [self.filteredNetworkTransactions count];
|
||||
} else {
|
||||
bytesReceived = self.bytesReceived;
|
||||
totalRequests = [self.networkTransactions 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;
|
||||
}
|
||||
|
||||
if (self.searchController.isActive) {
|
||||
[self updateTransactions];
|
||||
[self updateSearchResults];
|
||||
return;
|
||||
}
|
||||
|
||||
NSInteger existingRowCount = [self.networkTransactions count];
|
||||
[self updateTransactions];
|
||||
NSInteger newRowCount = [self.networkTransactions count];
|
||||
NSInteger addedRowCount = newRowCount - existingRowCount;
|
||||
|
||||
if (addedRowCount != 0 && !self.isPresentingSearch) {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)handleTransactionUpdatedNotification:(NSNotification *)notification
|
||||
{
|
||||
[self updateBytesReceived];
|
||||
[self updateFilteredBytesReceived];
|
||||
|
||||
FLEXNetworkTransaction *transaction = notification.userInfo[kFLEXNetworkRecorderUserInfoTransactionKey];
|
||||
|
||||
// Update both the main table view and search table view if needed.
|
||||
for (FLEXNetworkTransactionTableViewCell *cell in [self.tableView visibleCells]) {
|
||||
if ([cell.transaction isEqual:transaction]) {
|
||||
// Using -[UITableView reloadRowsAtIndexPaths:withRowAnimation:] is overkill here and kicks off a lot of
|
||||
// work that can make the table view somewhat unresponseive when lots of updates are streaming in.
|
||||
// We just need to tell the cell that it needs to re-layout.
|
||||
[cell setNeedsLayout];
|
||||
break;
|
||||
}
|
||||
}
|
||||
[self updateFirstSectionHeader];
|
||||
}
|
||||
|
||||
- (void)handleTransactionsClearedNotification:(NSNotification *)notification
|
||||
{
|
||||
[self updateTransactions];
|
||||
[self.tableView reloadData];
|
||||
}
|
||||
|
||||
- (void)handleNetworkObserverEnabledStateChangedNotification:(NSNotification *)notification
|
||||
{
|
||||
// Update the header, which displays a warning when network debugging is disabled
|
||||
[self updateFirstSectionHeader];
|
||||
}
|
||||
|
||||
#pragma mark - Table view data source
|
||||
|
||||
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
|
||||
{
|
||||
return self.searchController.isActive ? [self.filteredNetworkTransactions count] : [self.networkTransactions count];
|
||||
}
|
||||
|
||||
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
|
||||
{
|
||||
return [self headerText];
|
||||
}
|
||||
|
||||
- (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];
|
||||
}
|
||||
|
||||
#pragma mark - Menu Actions
|
||||
|
||||
- (BOOL)tableView:(UITableView *)tableView shouldShowMenuForRowAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)tableView:(UITableView *)tableView canPerformAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender
|
||||
{
|
||||
return action == @selector(copy:);
|
||||
}
|
||||
|
||||
- (void)tableView:(UITableView *)tableView performAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender
|
||||
{
|
||||
if (action == @selector(copy:)) {
|
||||
FLEXNetworkTransaction *transaction = [self transactionAtIndexPath:indexPath inTableView:tableView];
|
||||
NSString *requestURLString = transaction.request.URL.absoluteString ?: @"";
|
||||
[[UIPasteboard generalPasteboard] setString:requestURLString];
|
||||
}
|
||||
}
|
||||
|
||||
- (FLEXNetworkTransaction *)transactionAtIndexPath:(NSIndexPath *)indexPath inTableView:(UITableView *)tableView
|
||||
{
|
||||
return self.searchController.isActive ? self.filteredNetworkTransactions[indexPath.row] : self.networkTransactions[indexPath.row];
|
||||
}
|
||||
|
||||
#pragma mark - UISearchResultsUpdating
|
||||
|
||||
- (void)updateSearchResultsForSearchController:(UISearchController *)searchController
|
||||
{
|
||||
[self updateSearchResults];
|
||||
}
|
||||
|
||||
- (void)updateSearchResults
|
||||
{
|
||||
NSString *searchString = self.searchController.searchBar.text;
|
||||
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.searchController.searchBar.text isEqual:searchString]) {
|
||||
self.filteredNetworkTransactions = filteredNetworkTransactions;
|
||||
[self.tableView reloadData];
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
#pragma mark - UISearchControllerDelegate
|
||||
|
||||
- (void)willPresentSearchController:(UISearchController *)searchController
|
||||
{
|
||||
self.isPresentingSearch = YES;
|
||||
}
|
||||
|
||||
- (void)didPresentSearchController:(UISearchController *)searchController
|
||||
{
|
||||
self.isPresentingSearch = NO;
|
||||
}
|
||||
|
||||
- (void)willDismissSearchController:(UISearchController *)searchController
|
||||
{
|
||||
[self.tableView reloadData];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -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
|
||||
@@ -0,0 +1,261 @@
|
||||
//
|
||||
// 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
|
||||
{
|
||||
NSDate *startDate = [NSDate date];
|
||||
|
||||
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 = startDate;
|
||||
|
||||
[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
|
||||
{
|
||||
NSDate *responseDate = [NSDate date];
|
||||
|
||||
dispatch_async(self.queue, ^{
|
||||
FLEXNetworkTransaction *transaction = self.networkTransactionsForRequestIdentifiers[requestID];
|
||||
if (!transaction) {
|
||||
return;
|
||||
}
|
||||
transaction.response = response;
|
||||
transaction.transactionState = FLEXNetworkTransactionStateReceivingData;
|
||||
transaction.latency = -[transaction.startTime timeIntervalSinceDate:responseDate];
|
||||
|
||||
[self postUpdateNotificationForTransaction:transaction];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)recordDataReceivedWithRequestID:(NSString *)requestID dataLength:(int64_t)dataLength
|
||||
{
|
||||
dispatch_async(self.queue, ^{
|
||||
FLEXNetworkTransaction *transaction = self.networkTransactionsForRequestIdentifiers[requestID];
|
||||
if (!transaction) {
|
||||
return;
|
||||
}
|
||||
transaction.receivedDataLength += dataLength;
|
||||
|
||||
[self postUpdateNotificationForTransaction:transaction];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)recordLoadingFinishedWithRequestID:(NSString *)requestID responseBody:(NSData *)responseBody
|
||||
{
|
||||
NSDate *finishedDate = [NSDate date];
|
||||
|
||||
dispatch_async(self.queue, ^{
|
||||
FLEXNetworkTransaction *transaction = self.networkTransactionsForRequestIdentifiers[requestID];
|
||||
if (!transaction) {
|
||||
return;
|
||||
}
|
||||
transaction.transactionState = FLEXNetworkTransactionStateFinished;
|
||||
transaction.duration = -[transaction.startTime timeIntervalSinceDate:finishedDate];
|
||||
|
||||
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/"] && [responseBody length] > 0) {
|
||||
// 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[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[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
|
||||
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// FLEXNetworkSettingsTableViewController.h
|
||||
// FLEXInjected
|
||||
//
|
||||
// Created by Ryan Olson on 2/20/15.
|
||||
//
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface FLEXNetworkSettingsTableViewController : UITableViewController
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,186 @@
|
||||
//
|
||||
// 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 *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)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[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
|
||||
@@ -0,0 +1,41 @@
|
||||
//
|
||||
// FLEXNetworkTransaction.h
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 2/8/15.
|
||||
// Copyright (c) 2015 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "UIKit/UIKit.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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,483 @@
|
||||
//
|
||||
// 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];
|
||||
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Copy" style:UIBarButtonItemStylePlain target:self action:@selector(copyButtonPressed:)];
|
||||
}
|
||||
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];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)copyButtonPressed:(id)sender
|
||||
{
|
||||
NSMutableString *requestDetailString = [NSMutableString string];
|
||||
|
||||
for (FLEXNetworkDetailSection *section in self.sections) {
|
||||
if ([section.rows count] > 0) {
|
||||
if ([section.title length] > 0) {
|
||||
[requestDetailString appendString:section.title];
|
||||
[requestDetailString appendString:@"\n\n"];
|
||||
}
|
||||
for (FLEXNetworkDetailRow *row in section.rows) {
|
||||
NSString *rowDescription = [[[self class] attributedTextForRow:row] string];
|
||||
if ([rowDescription length] > 0) {
|
||||
[requestDetailString appendString:rowDescription];
|
||||
[requestDetailString appendString:@"\n"];
|
||||
}
|
||||
}
|
||||
[requestDetailString appendString:@"\n\n"];
|
||||
}
|
||||
}
|
||||
|
||||
[[UIPasteboard generalPasteboard] setString:requestDetailString];
|
||||
}
|
||||
|
||||
#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[section];
|
||||
return [sectionModel.rows count];
|
||||
}
|
||||
|
||||
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
|
||||
{
|
||||
FLEXNetworkDetailSection *sectionModel = self.sections[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[indexPath.section];
|
||||
return sectionModel.rows[indexPath.row];
|
||||
}
|
||||
|
||||
#pragma mark - Cell Copying
|
||||
|
||||
- (BOOL)tableView:(UITableView *)tableView shouldShowMenuForRowAtIndexPath:(NSIndexPath *)indexPath
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)tableView:(UITableView *)tableView canPerformAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender
|
||||
{
|
||||
return action == @selector(copy:);
|
||||
}
|
||||
|
||||
- (void)tableView:(UITableView *)tableView performAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender
|
||||
{
|
||||
if (action == @selector(copy:)) {
|
||||
FLEXNetworkDetailRow *row = [self rowModelAtIndexPath:indexPath];
|
||||
[[UIPasteboard generalPasteboard] setString:row.detailText];
|
||||
}
|
||||
}
|
||||
|
||||
#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[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 ([FLEXUtility isValidJSONData:data]) {
|
||||
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
|
||||
@@ -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
|
||||
@@ -0,0 +1,180 @@
|
||||
//
|
||||
// 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];
|
||||
if ([timestamp length] > 0) {
|
||||
[detailComponents addObject:timestamp];
|
||||
}
|
||||
|
||||
// Omit method for GET (assumed as default)
|
||||
NSString *httpMethod = self.transaction.request.HTTPMethod;
|
||||
if ([httpMethod length] > 0) {
|
||||
[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
|
||||
@@ -0,0 +1,29 @@
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
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.
|
||||
/// NOTE: this setting persists between launches of the app.
|
||||
+ (void)setEnabled:(BOOL)enabled;
|
||||
+ (BOOL)isEnabled;
|
||||
|
||||
@end
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user