Compare commits
296 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d8f4dbcef | |||
| 24985ac984 | |||
| e13717b056 | |||
| 552e687b9c | |||
| fb29421644 | |||
| f5d930bd58 | |||
| 26af4ef476 | |||
| ac8940da26 | |||
| f597152a62 | |||
| 58f94f108c | |||
| a5e0bbd50e | |||
| ac273fbfc9 | |||
| 548fd03bd5 | |||
| b564c25d2a | |||
| bd821dc553 | |||
| e46a33417b | |||
| a3419a841f | |||
| cafc1ba0bd | |||
| 0112c097d9 | |||
| 507d03fd90 | |||
| b6453ac360 | |||
| b693ceb20e | |||
| 31e81a616d | |||
| 928f60b56f | |||
| 12a1900d75 | |||
| c72b6f7e5b | |||
| 3cf9f72dcb | |||
| 9b1318e975 | |||
| d3d0f04c23 | |||
| b606d04944 | |||
| 7afd50d241 | |||
| 94f06d5ff8 | |||
| 85424fd15e | |||
| cff391f78c | |||
| 3e420bb747 | |||
| 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,3 @@
|
||||
# Contributing to FLEX #
|
||||
|
||||
We welcome contributions! Please open a pull request with your changes.
|
||||
@@ -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
|
||||
+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
|
||||
|
||||
+158
-124
@@ -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,27 +368,18 @@ 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
|
||||
{
|
||||
NSMutableArray *allViews = [NSMutableArray array];
|
||||
NSArray *windows = [self allWindows];
|
||||
NSArray *windows = [FLEXUtility allWindows];
|
||||
for (UIWindow *window in windows) {
|
||||
if (window != self.view.window) {
|
||||
[allViews addObject:window];
|
||||
@@ -445,25 +389,6 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
return allViews;
|
||||
}
|
||||
|
||||
- (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];
|
||||
}
|
||||
return windows;
|
||||
}
|
||||
|
||||
- (UIWindow *)statusWindow
|
||||
{
|
||||
NSString *statusBarString = [NSString stringWithFormat:@"%@arWindow", @"_statusB"];
|
||||
@@ -472,20 +397,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 +509,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];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -644,7 +565,7 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
- (NSArray *)viewsAtPoint:(CGPoint)tapPointInWindow skipHiddenViews:(BOOL)skipHidden
|
||||
{
|
||||
NSMutableArray *views = [NSMutableArray array];
|
||||
for (UIWindow *window in [self allWindows]) {
|
||||
for (UIWindow *window in [FLEXUtility allWindows]) {
|
||||
// Don't include the explorer's own window or subviews.
|
||||
if (window != self.view.window && [window pointInside:tapPointInWindow withEvent:nil]) {
|
||||
[views addObject:window];
|
||||
@@ -659,7 +580,7 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
|
||||
// Select in the window that would handle the touch, but don't just use the result of hitTest:withEvent: so we can still select views with interaction disabled.
|
||||
// Default to the the application's key window if none of the windows want the touch.
|
||||
UIWindow *windowForSelection = [[UIApplication sharedApplication] keyWindow];
|
||||
for (UIWindow *window in [[self allWindows] reverseObjectEnumerator]) {
|
||||
for (UIWindow *window in [[FLEXUtility allWindows] reverseObjectEnumerator]) {
|
||||
// Ignore the explorer's own window.
|
||||
if (window != self.view.window) {
|
||||
if ([window hitTest:tapPointInWindow withEvent:nil]) {
|
||||
@@ -842,11 +763,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 +772,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
|
||||
@@ -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,9 @@
|
||||
//
|
||||
// FLEX.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Eric Horacek on 7/18/15.
|
||||
// Copyright (c) 2015 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import <FLEX/FLEXManager.h>
|
||||
@@ -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,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>
|
||||
|
||||
@protocol FLEXDatabaseManager <NSObject>
|
||||
|
||||
@required
|
||||
- (instancetype)initWithPath:(NSString*)path;
|
||||
|
||||
- (BOOL)open;
|
||||
- (NSArray *)queryAllTables;
|
||||
- (NSArray *)queryAllColumnsWithTableName:(NSString *)tableName;
|
||||
- (NSArray *)queryAllDataWithTableName:(NSString *)tableName;
|
||||
|
||||
@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,14 @@
|
||||
//
|
||||
// FLEXRealmDatabaseManager.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tim Oliver on 28/01/2016.
|
||||
// Copyright © 2016 Realm. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "FLEXDatabaseManager.h"
|
||||
|
||||
@interface FLEXRealmDatabaseManager : NSObject <FLEXDatabaseManager>
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,114 @@
|
||||
//
|
||||
// FLEXRealmDatabaseManager.m
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tim Oliver on 28/01/2016.
|
||||
// Copyright © 2016 Realm. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXRealmDatabaseManager.h"
|
||||
|
||||
#if __has_include(<Realm/Realm.h>)
|
||||
#import <Realm/Realm.h>
|
||||
#import <Realm/RLMRealm_Dynamic.h>
|
||||
#else
|
||||
#import "FLEXRealmDefines.h"
|
||||
#endif
|
||||
|
||||
@interface FLEXRealmDatabaseManager ()
|
||||
|
||||
@property (nonatomic, copy) NSString *path;
|
||||
@property (nonatomic, strong) id realm;
|
||||
|
||||
@end
|
||||
|
||||
//#endif
|
||||
|
||||
@implementation FLEXRealmDatabaseManager
|
||||
|
||||
- (instancetype)initWithPath:(NSString*)aPath
|
||||
{
|
||||
Class realmClass = NSClassFromString(@"RLMRealm");
|
||||
if (realmClass == nil) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
self = [super init];
|
||||
|
||||
if (self) {
|
||||
_path = aPath;
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (BOOL)open
|
||||
{
|
||||
Class realmClass = NSClassFromString(@"RLMRealm");
|
||||
Class configurationClass = NSClassFromString(@"RLMRealmConfiguration");
|
||||
|
||||
if (realmClass == nil || configurationClass == nil) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
NSError *error = nil;
|
||||
id configuration = [[configurationClass alloc] init];
|
||||
[(RLMRealmConfiguration *)configuration setPath:self.path];
|
||||
self.realm = [realmClass realmWithConfiguration:configuration error:&error];
|
||||
return (error == nil);
|
||||
}
|
||||
|
||||
- (NSArray *)queryAllTables
|
||||
{
|
||||
NSMutableArray *allTables = [NSMutableArray array];
|
||||
RLMSchema *schema = [self.realm schema];
|
||||
|
||||
for (RLMObjectSchema *objectSchema in schema.objectSchema) {
|
||||
if (objectSchema.className == nil) {
|
||||
continue;
|
||||
}
|
||||
|
||||
NSDictionary *dictionary = @{@"name":objectSchema.className};
|
||||
[allTables addObject:dictionary];
|
||||
}
|
||||
|
||||
return allTables;
|
||||
}
|
||||
|
||||
- (NSArray *)queryAllColumnsWithTableName:(NSString *)tableName
|
||||
{
|
||||
RLMObjectSchema *objectSchema = [[self.realm schema] schemaForClassName:tableName];
|
||||
if (objectSchema == nil) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSMutableArray *columnNames = [NSMutableArray array];
|
||||
for (RLMProperty *property in objectSchema.properties) {
|
||||
[columnNames addObject:property.name];
|
||||
}
|
||||
|
||||
return columnNames;
|
||||
}
|
||||
|
||||
- (NSArray *)queryAllDataWithTableName:(NSString *)tableName
|
||||
{
|
||||
RLMObjectSchema *objectSchema = [[self.realm schema] schemaForClassName:tableName];
|
||||
RLMResults *results = [self.realm allObjects:tableName];
|
||||
if (results.count == 0 || objectSchema == nil) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSMutableArray *allDataEntries = [NSMutableArray array];
|
||||
for (RLMObject *result in results) {
|
||||
NSMutableDictionary *entry = [NSMutableDictionary dictionary];
|
||||
for (RLMProperty *property in objectSchema.properties) {
|
||||
id value = [result valueForKey:property.name];
|
||||
entry[property.name] = (value) ? (value) : [NSNull null];
|
||||
}
|
||||
|
||||
[allDataEntries addObject:entry];
|
||||
}
|
||||
|
||||
return allDataEntries;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,46 @@
|
||||
//
|
||||
// Realm.h
|
||||
// FLEX
|
||||
//
|
||||
// Created by Tim Oliver on 16/02/2016.
|
||||
// Copyright © 2016 Realm. All rights reserved.
|
||||
//
|
||||
|
||||
#if __has_include(<Realm/Realm.h>)
|
||||
#else
|
||||
|
||||
@class RLMObject, RLMResults, RLMRealm, RLMRealmConfiguration, RLMSchema, RLMObjectSchema, RLMProperty;
|
||||
|
||||
@interface RLMRealmConfiguration : NSObject
|
||||
@property (nonatomic, copy) NSString *path;
|
||||
@end
|
||||
|
||||
@interface RLMRealm : NSObject
|
||||
@property (nonatomic, readonly) RLMSchema *schema;
|
||||
+ (RLMRealm *)realmWithConfiguration:(RLMRealmConfiguration *)configuration error:(NSError **)error;
|
||||
- (RLMResults *)allObjects:(NSString *)className;
|
||||
@end
|
||||
|
||||
@interface RLMSchema : NSObject
|
||||
@property (nonatomic, readonly) NSArray *objectSchema;
|
||||
- (RLMObjectSchema *)schemaForClassName:(NSString *)className;
|
||||
@end
|
||||
|
||||
@interface RLMObjectSchema : NSObject
|
||||
@property (nonatomic, readonly) NSString *className;
|
||||
@property (nonatomic, readonly) NSArray *properties;
|
||||
@end
|
||||
|
||||
@interface RLMProperty : NSString
|
||||
@property (nonatomic, readonly) NSString *name;
|
||||
@end
|
||||
|
||||
@interface RLMResults : NSObject <NSFastEnumeration>
|
||||
@property (nonatomic, readonly) NSInteger count;
|
||||
@end
|
||||
|
||||
@interface RLMObject : NSObject
|
||||
|
||||
@end
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,19 @@
|
||||
//
|
||||
// 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>
|
||||
#import "FLEXDatabaseManager.h"
|
||||
|
||||
@interface FLEXSQLiteDatabaseManager : NSObject <FLEXDatabaseManager>
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,190 @@
|
||||
//
|
||||
// PTDatabaseManager.m
|
||||
// PTDatabaseReader
|
||||
//
|
||||
// Created by Peng Tao on 15/11/23.
|
||||
// Copyright © 2015年 Peng Tao. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXSQLiteDatabaseManager.h"
|
||||
#import <sqlite3.h>
|
||||
|
||||
|
||||
static NSString *const QUERY_TABLENAMES_SQL = @"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name";
|
||||
|
||||
@implementation FLEXSQLiteDatabaseManager
|
||||
{
|
||||
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,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,16 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
+ (BOOL)supportsExtension:(NSString *)extension;
|
||||
- (instancetype)initWithPath:(NSString *)path;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,136 @@
|
||||
//
|
||||
// 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 "FLEXSQLiteDatabaseManager.h"
|
||||
#import "FLEXRealmDatabaseManager.h"
|
||||
|
||||
#import "FLEXTableContentViewController.h"
|
||||
|
||||
@interface FLEXTableListViewController ()
|
||||
{
|
||||
id<FLEXDatabaseManager> _dbm;
|
||||
NSString *_databasePath;
|
||||
}
|
||||
|
||||
@property (nonatomic, strong) NSArray *tables;
|
||||
|
||||
+ (NSArray *)supportedSQLiteExtensions;
|
||||
+ (NSArray *)supportedRealmExtensions;
|
||||
|
||||
@end
|
||||
|
||||
@implementation FLEXTableListViewController
|
||||
|
||||
- (instancetype)initWithPath:(NSString *)path
|
||||
{
|
||||
self = [super initWithStyle:UITableViewStyleGrouped];
|
||||
if (self) {
|
||||
_databasePath = [path copy];
|
||||
_dbm = [self databaseManagerForFileAtPath:_databasePath];
|
||||
[_dbm open];
|
||||
[self getAllTables];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
- (id<FLEXDatabaseManager>)databaseManagerForFileAtPath:(NSString *)path
|
||||
{
|
||||
NSString *pathExtension = path.pathExtension.lowercaseString;
|
||||
|
||||
NSArray *sqliteExtensions = [FLEXTableListViewController supportedSQLiteExtensions];
|
||||
if ([sqliteExtensions indexOfObject:pathExtension] != NSNotFound) {
|
||||
return [[FLEXSQLiteDatabaseManager alloc] initWithPath:path];
|
||||
}
|
||||
|
||||
NSArray *realmExtensions = [FLEXTableListViewController supportedRealmExtensions];
|
||||
if (realmExtensions != nil && [realmExtensions indexOfObject:pathExtension] != NSNotFound) {
|
||||
return [[FLEXRealmDatabaseManager alloc] initWithPath:path];
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (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];
|
||||
}
|
||||
|
||||
+ (BOOL)supportsExtension:(NSString *)extension
|
||||
{
|
||||
extension = extension.lowercaseString;
|
||||
|
||||
NSArray *sqliteExtensions = [FLEXTableListViewController supportedSQLiteExtensions];
|
||||
if (sqliteExtensions.count > 0 && [sqliteExtensions indexOfObject:extension] != NSNotFound) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
NSArray *realmExtensions = [FLEXTableListViewController supportedRealmExtensions];
|
||||
if (realmExtensions.count > 0 && [realmExtensions indexOfObject:extension] != NSNotFound) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
+ (NSArray *)supportedSQLiteExtensions
|
||||
{
|
||||
return @[@"db", @"sqlite", @"sqlite3"];
|
||||
}
|
||||
|
||||
+ (NSArray *)supportedRealmExtensions
|
||||
{
|
||||
if (NSClassFromString(@"RLMRealm") == nil) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return @[@"realm"];
|
||||
}
|
||||
|
||||
@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;
|
||||
|
||||
+152
-69
@@ -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,9 @@
|
||||
|
||||
- (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];
|
||||
NSString *pathExtension = [subpath pathExtension];
|
||||
|
||||
BOOL isDirectory = NO;
|
||||
BOOL stillExists = [[NSFileManager defaultManager] fileExistsAtPath:fullPath isDirectory:&isDirectory];
|
||||
@@ -215,28 +198,29 @@
|
||||
UIViewController *drillInViewController = nil;
|
||||
if (isDirectory) {
|
||||
drillInViewController = [[[self class] alloc] initWithPath:fullPath];
|
||||
} else if ([FLEXUtility isImagePathExtension:[fullPath pathExtension]]) {
|
||||
} else if ([FLEXUtility isImagePathExtension:pathExtension]) {
|
||||
UIImage *image = [UIImage imageWithContentsOfFile:fullPath];
|
||||
drillInViewController = [[FLEXImagePreviewViewController alloc] initWithImage:image];
|
||||
} else {
|
||||
// Special case keyed archives, json, and plists to get more readable data.
|
||||
NSString *prettyString = nil;
|
||||
if ([[subpath pathExtension] isEqual:@"archive"]) {
|
||||
if ([pathExtension isEqual:@"archive"] || [pathExtension isEqual:@"coded"]) {
|
||||
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];
|
||||
} else if ([[subpath pathExtension] isEqualToString:@"plist"]) {
|
||||
} else if ([pathExtension isEqualToString:@"json"]) {
|
||||
prettyString = [FLEXUtility prettyJSONStringFromData:[NSData dataWithContentsOfFile:fullPath]];
|
||||
} else if ([pathExtension isEqualToString:@"plist"]) {
|
||||
NSData *fileData = [NSData dataWithContentsOfFile:fullPath];
|
||||
prettyString = [[NSPropertyListSerialization propertyListWithData:fileData options:0 format:NULL error:NULL] description];
|
||||
}
|
||||
|
||||
if ([prettyString length] > 0) {
|
||||
drillInViewController = [[FLEXWebViewController alloc] initWithText:prettyString];
|
||||
} else if ([FLEXWebViewController supportsPathExtension:[subpath pathExtension]]) {
|
||||
} else if ([FLEXWebViewController supportsPathExtension:pathExtension]) {
|
||||
drillInViewController = [[FLEXWebViewController alloc] initWithURL:[NSURL fileURLWithPath:fullPath]];
|
||||
} else {
|
||||
} else if ([FLEXTableListViewController supportsExtension:subpath.pathExtension]) {
|
||||
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 +237,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,192 @@
|
||||
//
|
||||
// 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
|
||||
|
||||
+ (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];
|
||||
while ((aslMessage = asl_next(response))) {
|
||||
[logMessages addObject:[FLEXSystemLogMessage logMessageFromASLMessage:aslMessage]];
|
||||
}
|
||||
asl_release(response);
|
||||
|
||||
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,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
|
||||
@@ -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,44 @@
|
||||
//
|
||||
// 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;
|
||||
|
||||
/// Populated lazily. Handles both normal HTTPBody data and HTTPBodyStreams.
|
||||
@property (nonatomic, strong, readonly) NSData *cachedRequestBody;
|
||||
|
||||
+ (NSString *)readableStringFromTransactionState:(FLEXNetworkTransactionState)state;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,80 @@
|
||||
//
|
||||
// FLEXNetworkTransaction.m
|
||||
// Flipboard
|
||||
//
|
||||
// Created by Ryan Olson on 2/8/15.
|
||||
// Copyright (c) 2015 Flipboard. All rights reserved.
|
||||
//
|
||||
|
||||
#import "FLEXNetworkTransaction.h"
|
||||
|
||||
@interface FLEXNetworkTransaction ()
|
||||
|
||||
@property (nonatomic, strong, readwrite) NSData *cachedRequestBody;
|
||||
|
||||
@end
|
||||
|
||||
@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;
|
||||
}
|
||||
|
||||
- (NSData *)cachedRequestBody {
|
||||
if (!_cachedRequestBody) {
|
||||
if (self.request.HTTPBody != nil) {
|
||||
_cachedRequestBody = self.request.HTTPBody;
|
||||
} else if ([self.request.HTTPBodyStream conformsToProtocol:@protocol(NSCopying)]) {
|
||||
NSInputStream *bodyStream = [self.request.HTTPBodyStream copy];
|
||||
const NSUInteger bufferSize = 1024;
|
||||
uint8_t buffer[bufferSize];
|
||||
NSMutableData *data = [NSMutableData data];
|
||||
[bodyStream open];
|
||||
NSInteger readBytes = 0;
|
||||
do {
|
||||
readBytes = [bodyStream read:buffer maxLength:bufferSize];
|
||||
[data appendBytes:buffer length:readBytes];
|
||||
} while (readBytes > 0);
|
||||
[bodyStream close];
|
||||
_cachedRequestBody = data;
|
||||
}
|
||||
}
|
||||
return _cachedRequestBody;
|
||||
}
|
||||
|
||||
+ (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.cachedRequestBody length] > 0) {
|
||||
FLEXNetworkDetailRow *postBodySizeRow = [[FLEXNetworkDetailRow alloc] init];
|
||||
postBodySizeRow.title = @"Request Body Size";
|
||||
postBodySizeRow.detailText = [NSByteCountFormatter stringFromByteCount:[transaction.cachedRequestBody 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.cachedRequestBody 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.cachedRequestBody;
|
||||
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
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user