Compare commits

...

51 Commits

Author SHA1 Message Date
Tanner Bennett b908d66b94 Argument input view work 2020-10-18 17:09:47 -05:00
Tanner Bennett e6c324d761 FLEXMethod.imagePath should use the method's IMP
We can already tell which image the class comes from, and that is usually the same as the original method implementation. What is more useful to know is the image of the _current_ implementation.
2020-10-18 17:04:44 -05:00
Tanner Bennett 784e030e44 Silence new weird documentation warning… 2020-10-18 17:04:44 -05:00
Tanner Bennett 046eb49f7b Commit FLEXTests scheme 2020-10-18 17:04:44 -05:00
Tanner Bennett 6ec1814f2e Xcode 12 upgrade check for main scheme 2020-10-18 17:04:44 -05:00
Tanner Bennett f179545972 Add FLEX_EXIT_IF_NO_CTORS 2020-10-18 17:02:09 -05:00
Justin Lam d0d1632ca1 Add foundation header to ActivityStreamSPI. (#461)
* Add C++ ifdefs around C++ code.

The purpose of this change is to gate C++ code behind a cplusplus define so that FLEX can be made into a module for Swift code.  Swift modules do not allow for C++ code.  However this means if FLEX is used a Swift module, it will not have functionality of the excluded code. Ideally in the future, FLEX can be converted fully to support Swift without bridging headers.

* Revert "Add C++ ifdefs around C++ code."

This reverts commit 77c02207f9.

* Add include <Foundation/Foundation.h> to ActivityStreamSPI header

We use a different build setup internally, and we need all headers to be able to stand on their own. This file is using NS_ENUM, which is defined in Foundation, without importing it, so we're adding the import to resolve that.
2020-10-13 14:31:31 -05:00
matrush 79e22cf828 Remove a retain cylce in FLEXSystemLogViewController 2020-10-13 14:31:31 -05:00
Iulian Onofrei 563cb514a1 Fix crash when opening Keychain item without a password 2020-10-13 14:31:31 -05:00
matrush 84131d8533 Return empty array when the rows are nil in FLEXSQLResult 2020-10-13 14:31:31 -05:00
Evan Emelga 55cb7ebf8f Sort realm table names alphabetically (#453) 2020-10-12 23:07:18 -05:00
Anıl Taşkıran b9e2c1ebd9 fix baseResumeClass name for iOS14+ 2020-10-12 23:07:18 -05:00
Maxime Ollivier 7377399673 Skip keyboard shortcut override when setting defaults 2020-10-12 23:07:18 -05:00
Tanner Bennett 21199b2a56 Add sharedApplication property at runtime for iOS 9 2020-10-12 23:07:18 -05:00
matrush e72c6aa349 Add better debugging message for FLEXProperty and FLEXIvar 2020-10-12 23:06:55 -05:00
ph661 13c583d32b Add SceneKit to podspec frameworks 2020-10-12 19:19:34 -05:00
Chaoshuai Lü 129c291469 Move custom additions to the top (#439)
- Move custom additions to the top
- Fix header
2020-10-12 19:19:34 -05:00
Chris Ellsworth 67609b28a4 Add sorting in File Browser 2020-10-12 19:17:58 -05:00
matrush 4dc206eaa6 Add copy action for database browser row selection alert 2020-10-12 19:17:57 -05:00
matrush 28e91507db Remove retain cycles from selectionHandler in several files 2020-07-07 12:05:05 -05:00
matrush 5f74fb0d43 Remove redundant FLEXPointerIsTaggedPointer function 2020-07-07 12:04:34 -05:00
Tanner Bennett 09f5859feb Add InAppViewDebugger thanks to README 2020-06-30 16:25:12 -05:00
Hao Nguyen cee416889a use __typeof instead of typeof to compile for Cxx instead of GNUxx 2020-06-19 01:39:48 -05:00
Tanner Bennett e2a334384a Misc cleanup and bug fixes 2020-05-25 17:23:18 -05:00
Tanner Bennett a840e909a1 Add UIApplication shortcuts 2020-05-25 17:12:46 -05:00
Tanner Bennett 2f952c380f Automatically activate search bar in heap explorer 2020-04-27 17:53:34 -05:00
Tanner Bennett 5d919eb329 Don't access ivars on tagged pointers 2020-04-25 00:51:58 -05:00
Tanner Bennett 1d5d825135 Fix class properties not showing previews 2020-04-25 00:51:36 -05:00
opa334 2a8cdbdb84 Fix heap enumeration on arm64e 2020-04-23 22:04:41 -05:00
Tanner Bennett 5e2081b8f9 Various performance improvements
- Remove ARC from -[FLEXTypeEncodingParser canScanChar:]
- Make FLEXMethod.imagePath lazy
2020-04-23 22:04:41 -05:00
Bas Broek fbaeda1956 Remove parenthesis in Swift (#414) 2020-04-16 10:34:27 -05:00
Tanner Bennett c761865b9b Namespace all fishhook functions, fix #408
I know not all of them needed to be namespaced as the private functions are static, but I did it anyway for consistency.
2020-04-06 17:47:47 -05:00
Tanner Bennett 700c50af5d Bump version 2020-04-06 17:37:59 -05:00
Tanner Bennett b38cca06b1 Fix ProtocolMember instance field
The ProtocolMember instance field was not being populated in runtime exports
2020-04-06 17:32:43 -05:00
Tanner Bennett 6429573918 Rename some classes with excessively long names 2020-04-06 17:32:43 -05:00
Tanner Bennett f77f5ccdc9 Push straight to single instances in heap explorer
Take the user straight to the explorer itself if there is only one instance of the selected class
2020-04-06 17:32:43 -05:00
Tanner Bennett 7aeddcdb2c Add search bar to view controllers list 2020-04-06 17:32:11 -05:00
Tanner Bennett a25ef87a51 Make new JSON viewer and System Log behavior opt-in 2020-04-06 17:32:11 -05:00
Tanner Bennett fbeb1beca0 Namespace some category filenames 2020-04-06 17:32:11 -05:00
Tanner Bennett 059bde9711 Fix crash in flex_all* methods 2020-04-06 17:32:11 -05:00
Tanner Bennett 2ca563f570 Bug fix: iPad support for FLEXAlert action sheets 2020-04-06 17:32:11 -05:00
Tanner Bennett 88c7ca9373 Add option to disable property/ivar previews 2020-03-31 12:16:57 -05:00
Tanner Bennett 83486641aa Add < iOS 13 support to example project 2020-03-30 16:28:19 -05:00
Tanner Bennett 6bd0c87881 Fix import statement to work without modules 2020-03-30 16:28:19 -05:00
Tanner Bennett 1a64da70c9 Add FLEXRuntimeExporter
Allows you to export the contents of a bundle's objc metadata as an SQLite database

fix

fix
2020-03-30 16:28:19 -05:00
Tanner Bennett 87ea2bb147 FLEXRuntimeClient additions
- Move initializeWebKitLegacy to FLEXRuntimeClient
- Add -copySafeClassList and -copyProtocolList
2020-03-27 19:59:32 -05:00
Tanner Bennett d9e9be53d8 Various database browser related upgrades
- All database managers automatically open and close connections to the underlying database
- Allow getting last result and last rowid from FLEXSQLiteDatabaseManager
- Execute statements with arguments with FLEXSQLiteDatabaseManager
2020-03-27 19:59:32 -05:00
Tanner Bennett 142f037497 Runtime wrapper upgrades
- Make sure every object has an `imageName` property
- Expose more fine-grained metadata through FLEXProtocol
- Migrate flex_all* methods to top-level functions that the methods call into
2020-03-27 19:59:32 -05:00
Tanner Bennett 6cdb626d78 Various bug fixes 2020-03-27 19:59:32 -05:00
Tanner Bennett 6e81029b8b Show HTTP status code in commit screen error 2020-03-27 19:16:09 -05:00
Tanner Bennett 1c7048e710 Dispatch JSON viewer registration to main queue 2020-03-25 15:12:34 -05:00
131 changed files with 2809 additions and 767 deletions
@@ -25,7 +25,7 @@
@property (nonatomic, copy) NSArray<FLEXTableViewSection *> *allSections;
/// This computed property should filter \c allSections for assignment to \c sections
@property (nonatomic, readonly) NSArray<FLEXTableViewSection *> *nonemptySections;
@property (nonatomic, readonly, copy) NSArray<FLEXTableViewSection *> *nonemptySections;
/// This should be able to re-initialize \c allSections
- (NSArray<FLEXTableViewSection *> *)makeSections;
@@ -80,7 +80,7 @@
/// if using \c self as the \c filterDelegate, as is the default.
///
/// For example, the object explorer hides the description section when searching.
@property (nonatomic, readonly) NSArray<FLEXTableViewSection *> *nonemptySections;
@property (nonatomic, readonly, copy) NSArray<FLEXTableViewSection *> *nonemptySections;
/// If using \c self as the \c filterDelegate, as is the default,
/// subclasses should override to provide the sections for the table view.
@@ -8,7 +8,7 @@
#import "FLEXFilteringTableViewController.h"
#import "FLEXTableViewSection.h"
#import "NSArray+Functional.h"
#import "NSArray+FLEX.h"
@interface FLEXFilteringTableViewController ()
@@ -89,7 +89,7 @@ extern CGFloat const kFLEXDebounceForExpensiveIO;
/// or it will not be respsected. Use this instead.
/// Defaults to NO.
@property (nonatomic) BOOL pinSearchBar;
/// By default, we will show the search bar's cancel button when
/// By default, we will show the search bar's cancel button when
/// search becomes active and hide it when search is dismissed.
///
/// Do not set the showsCancelButton property on the searchController's
@@ -102,11 +102,11 @@ extern CGFloat const kFLEXDebounceForExpensiveIO;
/// Otherwise, this is the selected index of the carousel, or NSNotFound if using neither.
@property (nonatomic) NSInteger selectedScope;
/// self.searchController.searchBar.text
@property (nonatomic, readonly) NSString *searchText;
@property (nonatomic, readonly, copy) NSString *searchText;
/// A totally optional delegate to forward search results updater calls to.
/// If a delegate is set, updateSearchResults: is not called on this view controller.
@property (nonatomic, weak ) id<FLEXSearchResultsUpdating> searchResultsUpdater;
/// If a delegate is set, updateSearchResults: is not called on this view controller.
@property (nonatomic, weak) id<FLEXSearchResultsUpdating> searchResultsUpdater;
/// self.view.window as a \c FLEXWindow
@property (nonatomic, readonly) FLEXWindow *window;
@@ -140,7 +140,7 @@ extern CGFloat const kFLEXDebounceForExpensiveIO;
@property (nonatomic) BOOL showsShareToolbarItem;
/// Called when the share button is pressed.
/// Default implementation does nothign. Subclasses may override.
- (void)shareButtonPressed;
- (void)shareButtonPressed:(UIBarButtonItem *)sender;
/// Subclasses may call this to opt-out of all toolbar related behavior.
/// This is necessary if you want to disable the gesture which reveals the toolbar.
@@ -222,7 +222,7 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
self.tableView.dataSource = self;
self.tableView.delegate = self;
_shareToolbarItem = FLEXBarButtonItemSystem(Action, self, @selector(shareButtonPressed));
_shareToolbarItem = FLEXBarButtonItemSystem(Action, self, @selector(shareButtonPressed:));
_bookmarksToolbarItem = [UIBarButtonItem
itemWithImage:FLEXResources.bookmarksIcon target:self action:@selector(showBookmarks)
];
@@ -386,7 +386,7 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
[self setupToolbarItems];
}
- (void)shareButtonPressed {
- (void)shareButtonPressed:(UIBarButtonItem *)sender {
}
+2 -2
View File
@@ -8,7 +8,7 @@
#import <UIKit/UIKit.h>
#import "FLEXMacros.h"
#import "NSArray+Functional.h"
#import "NSArray+FLEX.h"
@class FLEXTableView;
NS_ASSUME_NONNULL_BEGIN
@@ -30,7 +30,7 @@ NS_ASSUME_NONNULL_BEGIN
/// A title to be displayed for the custom section.
/// Subclasses may override or use the \c _title ivar.
@property (nonatomic, readonly, nullable) NSString *title;
@property (nonatomic, readonly, nullable, copy) NSString *title;
/// The number of rows in this section. Subclasses must override.
/// This should not change until \c filterText is changed or \c reloadData is called.
@property (nonatomic, readonly) NSInteger numberOfRows;
@@ -11,7 +11,7 @@
#import "FLEXRuntimeUtility.h"
#import "FLEXTypeEncodingParser.h"
@interface FLEXArgumentInputStructView ()
@interface FLEXArgumentInputStructView () <FLEXArgumentInputViewDelegate>
@property (nonatomic) NSArray<FLEXArgumentInputView *> *argumentInputViews;
@@ -30,8 +30,11 @@
NSUInteger fieldIndex,
NSUInteger fieldOffset) {
FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:fieldTypeEncoding];
FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory
argumentInputViewForTypeEncoding:fieldTypeEncoding
];
inputView.targetSize = FLEXArgumentInputViewSizeSmall;
inputView.delegate = self;
if (fieldIndex < customTitles.count) {
inputView.title = customTitles[fieldIndex];
@@ -125,15 +128,45 @@
return boxedStruct;
}
- (BOOL)inputViewIsFirstResponder {
BOOL isFirstResponder = NO;
- (FLEXArgumentInputView *)firstResponderInputView {
for (FLEXArgumentInputView *inputView in self.argumentInputViews) {
if ([inputView inputViewIsFirstResponder]) {
isFirstResponder = YES;
break;
return inputView;
}
}
return isFirstResponder;
return nil;
}
- (BOOL)resignFirstResponder {
FLEXArgumentInputView *responder = [self firstResponderInputView];
if (responder) {
return [responder resignFirstResponder];
} else {
return [super resignFirstResponder];
}
}
#pragma mark - FLEXArgumentInputViewDelegate
- (void)argumentInputViewValueDidChange:(FLEXArgumentInputView *)inputView {
// Nothing to see here
}
- (void)argumentInputViewWantsNextAsFirstResponder:(FLEXArgumentInputView *)inputView {
if (self.argumentInputViews.lastObject == inputView) {
// If this is our last or only input view,
// notify the delegate or dismiss the keyboard
if (self.delegate) {
[self.delegate argumentInputViewWantsNextAsFirstResponder:self];
} else {
[inputView resignFirstResponder];
}
} else {
NSInteger idx = [self.argumentInputViews indexOfObject:inputView];
[self.argumentInputViews[idx+1] becomeFirstResponder];
}
}
@@ -8,11 +8,15 @@
#import "FLEXArgumentInputView.h"
NS_ASSUME_NONNULL_BEGIN
@interface FLEXArgumentInputTextView : FLEXArgumentInputView <UITextViewDelegate>
// For subclass eyes only
@property (nonatomic, readonly) UITextView *inputTextView;
@property (nonatomic) NSString *inputPlaceholderText;
@property (nonatomic, nullable) NSString *inputPlaceholderText;
@end
NS_ASSUME_NONNULL_END
@@ -31,7 +31,7 @@
self.inputTextView.autocapitalizationType = UITextAutocapitalizationTypeNone;
self.inputTextView.autocorrectionType = UITextAutocorrectionTypeNo;
self.inputTextView.delegate = self;
self.inputTextView.inputAccessoryView = [self createToolBar];
self.inputAccessoryView = [self createToolBar];
if (@available(iOS 11, *)) {
[self.inputTextView.layer setValue:@YES forKey:@"continuousCorners"];
} else {
@@ -51,6 +51,14 @@
return self;
}
- (UIToolbar *)inputAccessoryView {
return (id)self.inputTextView.inputAccessoryView;
}
- (void)setInputAccessoryView:(UIToolbar *)inputAccessoryView {
self.inputTextView.inputAccessoryView = inputAccessoryView;
}
#pragma mark - Private
- (UIToolbar *)createToolBar {
@@ -62,7 +70,7 @@
];
UIBarButtonItem *pasteItem = [[UIBarButtonItem alloc]
initWithTitle:@"Paste" style:UIBarButtonItemStyleDone
target:self.inputTextView action:@selector(paste:)
target:self action:@selector(paste:)
];
UIBarButtonItem *doneItem = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemDone
@@ -91,6 +99,16 @@
return self.placeholderLabel.text;
}
- (void)paste:(id)sender {
[self.inputTextView paste:sender];
if (self.delegate) {
[self.delegate argumentInputViewWantsNextAsFirstResponder:self];
} else {
[self.inputTextView resignFirstResponder];
}
}
#pragma mark - Superclass Overrides
@@ -98,6 +116,18 @@
return self.inputTextView.isFirstResponder;
}
- (BOOL)becomeFirstResponder {
return [self.inputTextView becomeFirstResponder];
}
- (BOOL)resignFirstResponder {
if (self.inputViewIsFirstResponder) {
return self.inputTextView.resignFirstResponder;
} else {
return [super resignFirstResponder];
}
}
#pragma mark - Layout and Sizing
@@ -19,6 +19,8 @@ typedef NS_ENUM(NSUInteger, FLEXArgumentInputViewSize) {
@protocol FLEXArgumentInputViewDelegate;
NS_ASSUME_NONNULL_BEGIN
@interface FLEXArgumentInputView : UIView
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding;
@@ -31,23 +33,25 @@ typedef NS_ENUM(NSUInteger, FLEXArgumentInputViewSize) {
/// Primitive types and structs should/will be boxed in NSValue containers.
/// Concrete subclasses should override both the setter and getter for this property.
/// Subclasses can call super.inputValue to access a backing store for the value.
@property (nonatomic) id inputValue;
@property (nonatomic, nullable) id inputValue;
/// Setting this value to large will make some argument input views increase the size of their input field(s).
/// Useful to increase the use of space if there is only one input view on screen (i.e. for property and ivar editing).
@property (nonatomic) FLEXArgumentInputViewSize targetSize;
/// Users of the input view can get delegate callbacks for incremental changes in user input.
@property (nonatomic, weak) id <FLEXArgumentInputViewDelegate> delegate;
@property (nonatomic, weak, nullable) id <FLEXArgumentInputViewDelegate> delegate;
// Subclasses can override
@property (nonatomic, nullable) UIToolbar *inputAccessoryView;
/// If the input view has one or more text views, returns YES when one of them is focused.
@property (nonatomic, readonly) BOOL inputViewIsFirstResponder;
/// For subclasses to indicate that they can handle editing a field the give type and value.
/// Used by FLEXArgumentInputViewFactory to create appropriate input views.
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value;
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(nullable id)value;
// For subclass eyes only
@@ -59,6 +63,11 @@ typedef NS_ENUM(NSUInteger, FLEXArgumentInputViewSize) {
@protocol FLEXArgumentInputViewDelegate <NSObject>
//- (void)
- (void)argumentInputViewValueDidChange:(FLEXArgumentInputView *)argumentInputView;
- (void)argumentInputViewWantsNextAsFirstResponder:(FLEXArgumentInputView *)argumentInputView;
@end
NS_ASSUME_NONNULL_END
@@ -46,6 +46,7 @@
];
inputView.backgroundColor = self.view.backgroundColor;
inputView.inputValue = currentValue;
inputView.delegate = self;
self.fieldEditorView.argumentInputViews = @[inputView];
}
@@ -118,8 +118,8 @@
[self exploreObjectOrPopViewController:self.currentValue];
}
- (void)argumentInputViewValueDidChange:(FLEXArgumentInputView *)argumentInputView {
if ([argumentInputView isKindOfClass:[FLEXArgumentInputSwitchView class]]) {
- (void)argumentInputViewValueDidChange:(FLEXArgumentInputView *)inputView {
if ([inputView isKindOfClass:[FLEXArgumentInputSwitchView class]]) {
[self actionButtonPressed:nil];
}
}
@@ -64,6 +64,7 @@
inputView.backgroundColor = self.view.backgroundColor;
inputView.title = methodComponent;
inputView.delegate = self;
[argumentInputViews addObject:inputView];
argumentIndex++;
}
@@ -6,19 +6,20 @@
// Copyright (c) 2020 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "FLEXArgumentInputView.h"
@class FLEXFieldEditorView;
@class FLEXArgumentInputView;
/// Provides a screen for editing or configuring one or more variables.
@interface FLEXVariableEditorViewController : UIViewController
@interface FLEXVariableEditorViewController : UIViewController <FLEXArgumentInputViewDelegate>
+ (instancetype)target:(id)target;
- (id)initWithTarget:(id)target;
// Convenience accessor since many subclasses only use one input view
@property (nonatomic, readonly) FLEXArgumentInputView *firstInputView;
// Also a convenience accessor
@property (nonatomic, readonly) NSArray<FLEXArgumentInputView *> *inputViews;
// For subclass use only.
@property (nonatomic, readonly) id target;
@@ -54,8 +54,8 @@
#pragma mark - UIViewController methods
- (void)keyboardDidShow:(NSNotification *)notification {
CGRect keyboardRectInWindow = [[[notification userInfo] objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
CGSize keyboardSize = [self.view convertRect:keyboardRectInWindow fromView:nil].size;
CGRect keyboardRect = [notification.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
CGSize keyboardSize = [self.view convertRect:keyboardRect fromView:nil].size;
UIEdgeInsets scrollInsets = self.scrollView.contentInset;
scrollInsets.bottom = keyboardSize.height;
self.scrollView.contentInset = scrollInsets;
@@ -114,7 +114,11 @@
#pragma mark - Public
- (FLEXArgumentInputView *)firstInputView {
return [self.fieldEditorView argumentInputViews].firstObject;
return self.fieldEditorView.argumentInputViews.firstObject;
}
- (NSArray<FLEXArgumentInputView *> *)inputViews {
return self.fieldEditorView.argumentInputViews;
}
- (void)actionButtonPressed:(id)sender {
@@ -134,4 +138,20 @@
}
}
#pragma mark - FLEXArgumentInputViewDelegate
- (void)argumentInputViewValueDidChange:(FLEXArgumentInputView *)inputView {
// Subclasses might want to do something here but we don't
}
- (void)argumentInputViewWantsNextAsFirstResponder:(FLEXArgumentInputView *)inputView {
if (self.inputViews.lastObject == inputView) {
// If this is our last or only input view, dismiss the keyboard
[inputView resignFirstResponder];
} else {
NSInteger idx = [self.inputViews indexOfObject:inputView];
[self.inputViews[idx+1] becomeFirstResponder];
}
}
@end
@@ -68,7 +68,7 @@
- (void)setupEditingBarItems {
self.navigationItem.rightBarButtonItem = nil;
self.toolbarItems = @[
[UIBarButtonItem itemWithTitle:@"Close All" target:self action:@selector(closeAllButtonPressed)],
[UIBarButtonItem itemWithTitle:@"Close All" target:self action:@selector(closeAllButtonPressed:)],
UIBarButtonItem.flex_flexibleSpace,
// We use a non-system done item because we change its title dynamically
[UIBarButtonItem doneStyleitemWithTitle:@"Done" target:self action:@selector(toggleEditing)]
@@ -149,7 +149,7 @@
}
}
- (void)closeAllButtonPressed {
- (void)closeAllButtonPressed:(UIBarButtonItem *)sender {
[FLEXAlert makeSheet:^(FLEXAlert *make) {
NSInteger count = self.bookmarks.count;
NSString *title = FLEXPluralFormatString(count, @"Remove %@ bookmarks", @"Remove %@ bookmark");
@@ -158,7 +158,7 @@
[self toggleEditing];
});
make.button(@"Cancel").cancelStyle();
} showFrom:self];
} showFrom:self source:sender];
}
- (void)closeAll {
@@ -42,6 +42,7 @@
[super viewDidLoad];
self.title = @"View Controllers at Tap";
self.showsSearchBar = YES;
[self disableToolbar];
}
@@ -109,7 +109,7 @@
self.toolbarItems = @[
UIBarButtonItem.flex_fixedSpace,
UIBarButtonItem.flex_flexibleSpace,
FLEXBarButtonItemSystem(Add, self, @selector(addTabButtonPressed)),
FLEXBarButtonItemSystem(Add, self, @selector(addTabButtonPressed:)),
UIBarButtonItem.flex_flexibleSpace,
FLEXBarButtonItemSystem(Edit, self, @selector(toggleEditing)),
];
@@ -121,7 +121,7 @@
- (void)setupEditingBarItems {
self.navigationItem.rightBarButtonItem = nil;
self.toolbarItems = @[
[UIBarButtonItem itemWithTitle:@"Close All" target:self action:@selector(closeAllButtonPressed)],
[UIBarButtonItem itemWithTitle:@"Close All" target:self action:@selector(closeAllButtonPressed:)],
UIBarButtonItem.flex_flexibleSpace,
[UIBarButtonItem disabledSystemItem:UIBarButtonSystemItemAdd],
UIBarButtonItem.flex_flexibleSpace,
@@ -193,7 +193,7 @@
}
}
- (void)addTabButtonPressed {
- (void)addTabButtonPressed:(UIBarButtonItem *)sender {
if (FLEXBookmarkManager.bookmarks.count) {
[FLEXAlert makeSheet:^(FLEXAlert *make) {
make.title(@"New Tab");
@@ -208,7 +208,7 @@
] animated:YES completion:nil];
});
make.button(@"Cancel").cancelStyle();
} showFrom:self];
} showFrom:self source:sender];
} else {
// No bookmarks, just open the main menu
[self addTabAndDismiss:[FLEXNavigationController
@@ -224,7 +224,7 @@
}];
}
- (void)closeAllButtonPressed {
- (void)closeAllButtonPressed:(UIBarButtonItem *)sender {
[FLEXAlert makeSheet:^(FLEXAlert *make) {
NSInteger count = self.openTabs.count;
NSString *title = FLEXPluralFormatString(count, @"Close %@ tabs", @"Close %@ tab");
@@ -233,7 +233,7 @@
[self toggleEditing];
});
make.button(@"Cancel").cancelStyle();
} showFrom:self];
} showFrom:self source:sender];
}
- (void)closeAll {
+3 -3
View File
@@ -17,11 +17,11 @@
#import <FLEX/UIMenu+FLEX.h>
#import <FLEX/UITextField+Range.h>
#import <FLEX/NSObject+Reflection.h>
#import <FLEX/NSArray+Functional.h>
#import <FLEX/NSObject+FLEX_Reflection.h>
#import <FLEX/NSArray+FLEX.h>
#import <FLEX/NSDictionary+ObjcRuntime.h>
#import <FLEX/NSString+ObjcRuntime.h>
#import <FLEX/NSString+FLEX.h>
#import <FLEX/NSUserDefaults+FLEX.h>
#import <FLEX/NSMapTable+FLEX_Subscripting.h>
#import <FLEX/NSTimer+Blocks.h>
#import <FLEX/NSTimer+FLEX.h>
@@ -8,7 +8,7 @@
#import "FLEXDBQueryRowCell.h"
#import "FLEXMultiColumnTableView.h"
#import "NSArray+Functional.h"
#import "NSArray+FLEX.h"
#import "UIFont+FLEX.h"
#import "FLEXColor.h"
@@ -14,14 +14,14 @@
#import <Foundation/Foundation.h>
#import "FLEXSQLResult.h"
/// Conformers should automatically open and close the database
@protocol FLEXDatabaseManager <NSObject>
@required
/// @return \c nil if the database couldn't be opened
+ (instancetype)managerForDatabase:(NSString *)path;
- (BOOL)open;
/// @return a list of all table names
- (NSArray<NSString *> *)queryAllTables;
- (NSArray<NSString *> *)queryAllColumnsOfTable:(NSString *)tableName;
@@ -7,7 +7,7 @@
//
#import "FLEXRealmDatabaseManager.h"
#import "NSArray+Functional.h"
#import "NSArray+FLEX.h"
#import "FLEXSQLResult.h"
#if __has_include(<Realm/Realm.h>)
@@ -20,7 +20,7 @@
@interface FLEXRealmDatabaseManager ()
@property (nonatomic, copy) NSString *path;
@property (nonatomic) RLMRealm * realm;
@property (nonatomic) RLMRealm *realm;
@end
@@ -43,6 +43,10 @@ static Class RLMRealmClass = nil;
self = [super init];
if (self) {
_path = path;
if (![self open]) {
return nil;
}
}
return self;
@@ -64,9 +68,11 @@ static Class RLMRealmClass = nil;
- (NSArray<NSString *> *)queryAllTables {
// Map each schema to its name
return [self.realm.schema.objectSchema flex_mapped:^id(RLMObjectSchema *schema, NSUInteger idx) {
NSArray<NSString *> *tableNames = [self.realm.schema.objectSchema flex_mapped:^id(RLMObjectSchema *schema, NSUInteger idx) {
return schema.className ?: nil;
}];
return [tableNames sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)];
}
- (NSArray<NSString *> *)queryAllColumnsOfTable:(NSString *)tableName {
@@ -14,6 +14,9 @@ NS_ASSUME_NONNULL_BEGIN
/// Describes the result of a non-select query, or an error of any kind of query
+ (instancetype)message:(NSString *)message;
/// Describes the result of a known failed execution
+ (instancetype)error:(NSString *)message;
/// @param rowData A list of rows, where each element in the row
/// corresponds to the column given in /c columnNames
+ (instancetype)columns:(NSArray<NSString *> *)columnNames
@@ -21,6 +24,10 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, readonly, nullable) NSString *message;
/// A value of YES means this is surely an error,
/// but it still might be an error even with a value of NO
@property (nonatomic, readonly) BOOL isError;
/// A list of column names
@property (nonatomic, readonly, nullable) NSArray<NSString *> *columns;
/// A list of rows, where each element in the row corresponds
@@ -7,7 +7,7 @@
//
#import "FLEXSQLResult.h"
#import "NSArray+Functional.h"
#import "NSArray+FLEX.h"
@implementation FLEXSQLResult
@synthesize keyedRows = _keyedRows;
@@ -16,6 +16,12 @@
return [[self alloc] initWithmessage:message columns:nil rows:nil];
}
+ (instancetype)error:(NSString *)message {
FLEXSQLResult *result = [self message:message];
result->_isError = YES;
return result;
}
+ (instancetype)columns:(NSArray<NSString *> *)columnNames rows:(NSArray<NSArray<NSString *> *> *)rowData {
return [[self alloc] initWithmessage:nil columns:columnNames rows:rowData];
}
@@ -13,7 +13,20 @@
#import <Foundation/Foundation.h>
#import "FLEXDatabaseManager.h"
#import "FLEXSQLResult.h"
@interface FLEXSQLiteDatabaseManager : NSObject <FLEXDatabaseManager>
/// Contains the result of the last operation, which may be an error
@property (nonatomic, readonly) FLEXSQLResult *lastResult;
/// Calls into \c sqlite3_last_insert_rowid()
@property (nonatomic, readonly) NSInteger lastRowID;
/// Given a statement like 'SELECT * from @table where @col = @val' and arguments
/// like { @"table": @"Album", @"col": @"year", @"val" @1 } this method will
/// invoke the statement and properly bind the given arguments to the statement.
///
/// You may pass NSStrings, NSData, NSNumbers, or NSNulls as values.
- (FLEXSQLResult *)executeStatement:(NSString *)statement arguments:(NSDictionary<NSString *, id> *)args;
@end
@@ -8,8 +8,8 @@
#import "FLEXSQLiteDatabaseManager.h"
#import "FLEXManager.h"
#import "NSArray+Functional.h"
#import "FLEXSQLResult.h"
#import "NSArray+FLEX.h"
#import "FLEXRuntimeConstants.h"
#import <sqlite3.h>
static NSString * const QUERY_TABLENAMES = @"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name";
@@ -36,6 +36,10 @@ static NSString * const QUERY_TABLENAMES = @"SELECT name FROM sqlite_master WHER
return self;
}
- (void)dealloc {
[self close];
}
- (BOOL)open {
if (self.db) {
return YES;
@@ -52,8 +56,7 @@ static NSString * const QUERY_TABLENAMES = @"SELECT name FROM sqlite_master WHER
#endif
if (err != SQLITE_OK) {
NSLog(@"error opening!: %d", err);
return NO;
return [self storeErrorForLastTask:@"Open"];
}
return YES;
@@ -81,7 +84,9 @@ static NSString * const QUERY_TABLENAMES = @"SELECT name FROM sqlite_master WHER
}
}
} else if (SQLITE_OK != rc) {
NSLog(@"error closing!: %d", rc);
[self storeErrorForLastTask:@"Close"];
self.db = nil;
return NO;
}
} while (retry);
@@ -89,36 +94,50 @@ static NSString * const QUERY_TABLENAMES = @"SELECT name FROM sqlite_master WHER
return YES;
}
- (NSInteger)lastRowID {
return (NSInteger)sqlite3_last_insert_rowid(self.db);
}
- (NSArray<NSString *> *)queryAllTables {
return [[self executeStatement:QUERY_TABLENAMES].rows flex_mapped:^id(NSArray *table, NSUInteger idx) {
return table.firstObject;
}];
}] ?: @[];
}
- (NSArray<NSString *> *)queryAllColumnsOfTable:(NSString *)tableName {
NSString *sql = [NSString stringWithFormat:@"PRAGMA table_info('%@')",tableName];
FLEXSQLResult *results = [self executeStatement:sql];
return [results.keyedRows flex_mapped:^id(NSDictionary *column, NSUInteger idx) {
return column[@"name"];
}];
}] ?: @[];
}
- (NSArray<NSArray *> *)queryAllDataInTable:(NSString *)tableName {
return [self executeStatement:[@"SELECT * FROM "
stringByAppendingString:tableName
]].rows;
]].rows ?: @[];
}
- (FLEXSQLResult *)executeStatement:(NSString *)sql {
return [self executeStatement:sql arguments:nil];
}
- (FLEXSQLResult *)executeStatement:(NSString *)sql arguments:(NSDictionary *)args {
[self open];
FLEXSQLResult *result = nil;
sqlite3_stmt *pstmt;
if (sqlite3_prepare_v2(_db, sql.UTF8String, -1, &pstmt, 0) == SQLITE_OK) {
int status;
if ((status = sqlite3_prepare_v2(_db, sql.UTF8String, -1, &pstmt, 0)) == SQLITE_OK) {
NSMutableArray<NSArray *> *rows = [NSMutableArray new];
// Bind parameters, if any
if (![self bindParameters:args toStatement:pstmt]) {
return self.lastResult;
}
// Grab columns
int columnCount = sqlite3_column_count(pstmt);
NSArray<NSString *> *columns = [NSArray flex_forEachUpTo:columnCount map:^id(NSUInteger i) {
@@ -126,7 +145,6 @@ static NSString * const QUERY_TABLENAMES = @"SELECT name FROM sqlite_master WHER
}];
// Execute statement
int status;
while ((status = sqlite3_step(pstmt)) == SQLITE_ROW) {
// Grab rows if this is a selection query
int dataCount = sqlite3_data_count(pstmt);
@@ -140,30 +158,111 @@ static NSString * const QUERY_TABLENAMES = @"SELECT name FROM sqlite_master WHER
if (status == SQLITE_DONE) {
if (rows.count) {
// We selected some rows
result = [FLEXSQLResult columns:columns rows:rows];
result = _lastResult = [FLEXSQLResult columns:columns rows:rows];
} else {
// We executed a query like INSERT, UDPATE, or DELETE
int rowsAffected = sqlite3_changes(_db);
NSString *message = [NSString stringWithFormat:@"%d row(s) affected", rowsAffected];
result = [FLEXSQLResult message:message];
result = _lastResult = [FLEXSQLResult message:message];
}
} else {
// An error occured executing the query
result = [FLEXSQLResult message:@(sqlite3_errmsg(_db) ?: "(Execution: empty error)")];
result = _lastResult = [self errorResult:@"Execution"];
}
} else {
// An error occurred creating the prepared statement
result = [FLEXSQLResult message:@(sqlite3_errmsg(_db) ?: "(Prepared statement: empty error)")];
result = _lastResult = [self errorResult:@"Prepared statement"];
}
sqlite3_finalize(pstmt);
[self close];
return result;
}
#pragma mark - Private
/// @return YES on success, NO if an error was encountered and stored in \c lastResult
- (BOOL)bindParameters:(NSDictionary *)args toStatement:(sqlite3_stmt *)pstmt {
for (NSString *param in args.allKeys) {
int status = SQLITE_OK, idx = sqlite3_bind_parameter_index(pstmt, param.UTF8String);
id value = args[param];
if (idx == 0) {
// No parameter matching that arg
@throw NSInternalInconsistencyException;
}
// Null
if ([value isKindOfClass:[NSNull class]]) {
status = sqlite3_bind_null(pstmt, idx);
}
// String params
else if ([value isKindOfClass:[NSString class]]) {
const char *str = [value UTF8String];
status = sqlite3_bind_text(pstmt, idx, str, (int)strlen(str), SQLITE_TRANSIENT);
}
// Data params
else if ([value isKindOfClass:[NSData class]]) {
const void *blob = [value bytes];
status = sqlite3_bind_blob64(pstmt, idx, blob, [value length], SQLITE_TRANSIENT);
}
// Primitive params
else if ([value isKindOfClass:[NSNumber class]]) {
FLEXTypeEncoding type = [value objCType][0];
switch (type) {
case FLEXTypeEncodingCBool:
case FLEXTypeEncodingChar:
case FLEXTypeEncodingUnsignedChar:
case FLEXTypeEncodingShort:
case FLEXTypeEncodingUnsignedShort:
case FLEXTypeEncodingInt:
case FLEXTypeEncodingUnsignedInt:
case FLEXTypeEncodingLong:
case FLEXTypeEncodingUnsignedLong:
case FLEXTypeEncodingLongLong:
case FLEXTypeEncodingUnsignedLongLong:
status = sqlite3_bind_int64(pstmt, idx, (sqlite3_int64)[value longValue]);
break;
case FLEXTypeEncodingFloat:
case FLEXTypeEncodingDouble:
status = sqlite3_bind_double(pstmt, idx, [value doubleValue]);
break;
default:
@throw NSInternalInconsistencyException;
break;
}
}
// Unsupported type
else {
@throw NSInternalInconsistencyException;
}
if (status != SQLITE_OK) {
return [self storeErrorForLastTask:
[NSString stringWithFormat:@"Binding param named '%@'", param]
];
}
}
return YES;
}
- (BOOL)storeErrorForLastTask:(NSString *)action {
_lastResult = [self errorResult:action];
return NO;
}
- (FLEXSQLResult *)errorResult:(NSString *)description {
const char *error = sqlite3_errmsg(_db);
NSString *message = error ? @(error) : [NSString
stringWithFormat:@"(%@: empty error", description
];
return [FLEXSQLResult error:message];
}
- (id)objectForColumnIndex:(int)columnIdx stmt:(sqlite3_stmt*)stmt {
int columnType = sqlite3_column_type(stmt, columnIdx);
@@ -113,7 +113,11 @@
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title([@"Row " stringByAppendingString:@(row).stringValue]);
make.message([fields componentsJoinedByString:@"\n\n"]);
NSString *message = [fields componentsJoinedByString:@"\n\n"];
make.message(message);
make.button(@"Copy").handler(^(NSArray<NSString *> *strings) {
UIPasteboard.generalPasteboard.string = message;
});
make.button(@"Dismiss").cancelStyle();
} showFrom:self];
}
@@ -12,7 +12,7 @@
#import "FLEXRealmDatabaseManager.h"
#import "FLEXTableContentViewController.h"
#import "FLEXMutableListSection.h"
#import "NSArray+Functional.h"
#import "NSArray+FLEX.h"
#import "FLEXAlert.h"
@interface FLEXTableListViewController ()
@@ -1,5 +1,5 @@
//
// FLEXCookiesTableViewController.h
// FLEXCookiesViewController.h
// FLEX
//
// Created by Rich Robinson on 19/10/2015.
@@ -9,6 +9,6 @@
#import "FLEXGlobalsEntry.h"
#import "FLEXFilteringTableViewController.h"
@interface FLEXCookiesTableViewController : FLEXFilteringTableViewController <FLEXGlobalsEntry>
@interface FLEXCookiesViewController : FLEXFilteringTableViewController <FLEXGlobalsEntry>
@end
@@ -1,22 +1,22 @@
//
// FLEXCookiesTableViewController.m
// FLEXCookiesViewController.m
// FLEX
//
// Created by Rich Robinson on 19/10/2015.
// Copyright © 2015 Flipboard. All rights reserved.
//
#import "FLEXCookiesTableViewController.h"
#import "FLEXCookiesViewController.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXMutableListSection.h"
#import "FLEXUtility.h"
@interface FLEXCookiesTableViewController ()
@interface FLEXCookiesViewController ()
@property (nonatomic, readonly) FLEXMutableListSection<NSHTTPCookie *> *cookies;
@property (nonatomic) NSString *headerTitle;
@end
@implementation FLEXCookiesTableViewController
@implementation FLEXCookiesViewController
#pragma mark - Overrides
@@ -1,5 +1,5 @@
//
// FLEXLiveObjectsTableViewController.h
// FLEXLiveObjectsController.h
// Flipboard
//
// Created by Ryan Olson on 5/28/14.
@@ -9,6 +9,6 @@
#import "FLEXTableViewController.h"
#import "FLEXGlobalsEntry.h"
@interface FLEXLiveObjectsTableViewController : FLEXTableViewController <FLEXGlobalsEntry>
@interface FLEXLiveObjectsController : FLEXTableViewController <FLEXGlobalsEntry>
@end
@@ -1,12 +1,12 @@
//
// FLEXLiveObjectsTableViewController.m
// FLEXLiveObjectsController.m
// Flipboard
//
// Created by Ryan Olson on 5/28/14.
// Copyright (c) 2020 Flipboard. All rights reserved.
//
#import "FLEXLiveObjectsTableViewController.h"
#import "FLEXLiveObjectsController.h"
#import "FLEXHeapEnumerator.h"
#import "FLEXObjectListViewController.h"
#import "FLEXUtility.h"
@@ -18,7 +18,7 @@ static const NSInteger kFLEXLiveObjectsSortAlphabeticallyIndex = 0;
static const NSInteger kFLEXLiveObjectsSortByCountIndex = 1;
static const NSInteger kFLEXLiveObjectsSortBySizeIndex = 2;
@interface FLEXLiveObjectsTableViewController ()
@interface FLEXLiveObjectsController ()
@property (nonatomic) NSDictionary<NSString *, NSNumber *> *instanceCountsForClassNames;
@property (nonatomic) NSDictionary<NSString *, NSNumber *> *instanceSizesForClassNames;
@@ -28,12 +28,13 @@ static const NSInteger kFLEXLiveObjectsSortBySizeIndex = 2;
@end
@implementation FLEXLiveObjectsTableViewController
@implementation FLEXLiveObjectsController
- (void)viewDidLoad {
[super viewDidLoad];
self.showsSearchBar = YES;
self.showSearchBarInitially = YES;
self.searchBarDebounceInterval = kFLEXDebounceInstant;
self.showsCarousel = YES;
self.carousel.items = @[@"A→Z", @"Count", @"Size"];
@@ -139,7 +140,7 @@ static const NSInteger kFLEXLiveObjectsSortBySizeIndex = 2;
}
+ (UIViewController *)globalsEntryViewController:(FLEXGlobalsRow)row {
FLEXLiveObjectsTableViewController *liveObjectsViewController = [self new];
FLEXLiveObjectsController *liveObjectsViewController = [self new];
liveObjectsViewController.title = [self globalsEntryTitle:row];
return liveObjectsViewController;
@@ -224,8 +225,8 @@ static const NSInteger kFLEXLiveObjectsSortBySizeIndex = 2;
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NSString *className = self.filteredClassNames[indexPath.row];
FLEXObjectListViewController *instancesViewController = [FLEXObjectListViewController instancesOfClassWithName:className];
[self.navigationController pushViewController:instancesViewController animated:YES];
UIViewController *instances = [FLEXObjectListViewController instancesOfClassWithName:className];
[self.navigationController pushViewController:instances animated:YES];
}
@end
@@ -10,7 +10,9 @@
@interface FLEXObjectListViewController : FLEXFilteringTableViewController
+ (instancetype)instancesOfClassWithName:(NSString *)className;
/// This will either return a list of the instances, or take you straight
/// to the explorer itself if there is only one instance.
+ (UIViewController *)instancesOfClassWithName:(NSString *)className;
+ (instancetype)subclassesOfClassWithName:(NSString *)className;
+ (instancetype)objectsWithReferencesToObject:(id)object;
@@ -15,7 +15,7 @@
#import "FLEXHeapEnumerator.h"
#import "FLEXObjectRef.h"
#import "NSString+FLEX.h"
#import "NSObject+Reflection.h"
#import "NSObject+FLEX_Reflection.h"
#import "FLEXTableViewCell.h"
#import <malloc/malloc.h>
@@ -111,7 +111,7 @@
return self;
}
+ (instancetype)instancesOfClassWithName:(NSString *)className {
+ (UIViewController *)instancesOfClassWithName:(NSString *)className {
const char *classNameCString = className.UTF8String;
NSMutableArray *instances = [NSMutableArray new];
[FLEXHeapEnumerator enumerateLiveObjectsUsingBlock:^(__unsafe_unretained id object, __unsafe_unretained Class actualClass) {
@@ -125,8 +125,14 @@
}
}
}];
NSArray<FLEXObjectRef *> *references = [FLEXObjectRef referencingAll:instances];
if (references.count == 1) {
return [FLEXObjectExplorerFactory
explorerViewControllerForObject:references.firstObject.object
];
}
FLEXObjectListViewController *controller = [[self alloc] initWithReferences:references];
controller.title = [NSString stringWithFormat:@"%@ (%lu)", className, (unsigned long)instances.count];
return controller;
@@ -139,7 +145,7 @@
controller.title = [NSString stringWithFormat:@"Subclasses of %@ (%lu)",
className, (unsigned long)classes.count
];
return controller;
}
@@ -152,7 +158,7 @@
SwiftObjectClass = NSClassFromString(@"Swift._SwiftObject");
}
});
NSMutableArray<FLEXObjectRef *> *instances = [NSMutableArray new];
[FLEXHeapEnumerator enumerateLiveObjectsUsingBlock:^(__unsafe_unretained id tryObject, __unsafe_unretained Class actualClass) {
// Get all the ivars on the object. Start with the class and and travel up the inheritance chain.
@@ -161,15 +167,15 @@
while (tryClass) {
unsigned int ivarCount = 0;
Ivar *ivars = class_copyIvarList(tryClass, &ivarCount);
for (unsigned int ivarIndex = 0; ivarIndex < ivarCount; ivarIndex++) {
Ivar ivar = ivars[ivarIndex];
NSString *typeEncoding = @(ivar_getTypeEncoding(ivar) ?: "");
if (typeEncoding.flex_typeIsObjectOrClass) {
ptrdiff_t offset = ivar_getOffset(ivar);
uintptr_t *fieldPointer = (__bridge void *)tryObject + offset;
if (*fieldPointer == (uintptr_t)(__bridge void *)object) {
NSString *ivarName = @(ivar_getName(ivar) ?: "???");
[instances addObject:[FLEXObjectRef referencing:tryObject ivar:ivarName]];
@@ -177,7 +183,7 @@
}
}
}
tryClass = class_getSuperclass(tryClass);
}
}];
@@ -200,7 +206,7 @@
- (void)viewDidLoad {
[super viewDidLoad];
self.showsSearchBar = YES;
}
@@ -218,7 +224,7 @@
- (NSArray *)buildSections:(NSArray<NSString *> *)titles predicates:(NSArray<NSPredicate *> *)predicates {
NSParameterAssert(titles.count == predicates.count);
NSParameterAssert(titles); NSParameterAssert(predicates);
return [NSArray flex_forEachUpTo:titles.count map:^id(NSUInteger i) {
NSArray *rows = [self.references filteredArrayUsingPredicate:predicates[i]];
return [self makeSection:rows title:titles[i]];
@@ -235,18 +241,22 @@
if (ref.summary && [ref.summary localizedCaseInsensitiveContainsString:filterText]) {
return YES;
}
return [ref.reference localizedCaseInsensitiveContainsString:filterText];
}
];
__weak __typeof(self) weakSelf = self;
section.selectionHandler = ^(__kindof UIViewController *host, FLEXObjectRef *ref) {
[self.navigationController pushViewController:[
FLEXObjectExplorerFactory explorerViewControllerForObject:ref.object
] animated:YES];
__strong __typeof(self) strongSelf = weakSelf;
if (strongSelf) {
[strongSelf.navigationController pushViewController:[
FLEXObjectExplorerFactory explorerViewControllerForObject:ref.object
] animated:YES];
}
};
section.customTitle = title;
section.customTitle = title;
return section;
}
+1 -1
View File
@@ -8,7 +8,7 @@
#import "FLEXObjectRef.h"
#import "FLEXRuntimeUtility.h"
#import "NSArray+Functional.h"
#import "NSArray+FLEX.h"
@interface FLEXObjectRef ()
@property (nonatomic, readonly) BOOL wantsSummary;
@@ -1,5 +1,5 @@
//
// FLEXFileBrowserTableViewController.h
// FLEXFileBrowserController.h
// Flipboard
//
// Created by Ryan Olson on 6/9/14.
@@ -10,7 +10,7 @@
#import "FLEXGlobalsEntry.h"
#import "FLEXFileBrowserSearchOperation.h"
@interface FLEXFileBrowserTableViewController : FLEXTableViewController <FLEXGlobalsEntry>
@interface FLEXFileBrowserController : FLEXTableViewController <FLEXGlobalsEntry>
+ (instancetype)path:(NSString *)path;
- (id)initWithPath:(NSString *)path;
@@ -1,12 +1,12 @@
//
// FLEXFileBrowserTableViewController.m
// FLEXFileBrowserController.m
// Flipboard
//
// Created by Ryan Olson on 6/9/14.
//
//
#import "FLEXFileBrowserTableViewController.h"
#import "FLEXFileBrowserController.h"
#import "FLEXUtility.h"
#import "FLEXWebViewController.h"
#import "FLEXImagePreviewViewController.h"
@@ -18,7 +18,13 @@
@interface FLEXFileBrowserTableViewCell : UITableViewCell
@end
@interface FLEXFileBrowserTableViewController () <FLEXFileBrowserSearchOperationDelegate>
typedef NS_ENUM(NSUInteger, FLEXFileBrowserSortAttribute) {
FLEXFileBrowserSortAttributeNone = 0,
FLEXFileBrowserSortAttributeName,
FLEXFileBrowserSortAttributeCreationDate,
};
@interface FLEXFileBrowserController () <FLEXFileBrowserSearchOperationDelegate>
@property (nonatomic, copy) NSString *path;
@property (nonatomic, copy) NSArray<NSString *> *childPaths;
@@ -27,10 +33,11 @@
@property (nonatomic) NSNumber *searchPathsSize;
@property (nonatomic) NSOperationQueue *operationQueue;
@property (nonatomic) UIDocumentInteractionController *documentController;
@property (nonatomic) FLEXFileBrowserSortAttribute sortAttribute;
@end
@implementation FLEXFileBrowserTableViewController
@implementation FLEXFileBrowserController
+ (instancetype)path:(NSString *)path {
return [[self alloc] initWithPath:path];
@@ -49,7 +56,7 @@
//computing path size
FLEXFileBrowserTableViewController *__weak weakSelf = self;
FLEXFileBrowserController *__weak weakSelf = self;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSFileManager *fileManager = NSFileManager.defaultManager;
NSDictionary<NSString *, id> *attributes = [fileManager attributesOfItemAtPath:path error:NULL];
@@ -66,7 +73,7 @@
}
dispatch_async(dispatch_get_main_queue(), ^{
FLEXFileBrowserTableViewController *__strong strongSelf = weakSelf;
FLEXFileBrowserController *__strong strongSelf = weakSelf;
strongSelf.recursiveSize = @(totalSize);
[strongSelf.tableView reloadData];
});
@@ -84,6 +91,39 @@
self.showsSearchBar = YES;
self.searchBarDebounceInterval = kFLEXDebounceForAsyncSearch;
[self addToolbarItems:@[
[[UIBarButtonItem alloc] initWithTitle:@"Sort"
style:UIBarButtonItemStylePlain
target:self
action:@selector(sortDidTouchUpInside:)]
]];
}
- (void)sortDidTouchUpInside:(UIBarButtonItem *)sortButton {
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Sort"
message:nil
preferredStyle:UIAlertControllerStyleActionSheet];
[alertController addAction:[UIAlertAction actionWithTitle:@"None"
style:UIAlertActionStyleCancel
handler:^(UIAlertAction * _Nonnull action) {
[self sortWithAttribute:FLEXFileBrowserSortAttributeNone];
}]];
[alertController addAction:[UIAlertAction actionWithTitle:@"Name"
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * _Nonnull action) {
[self sortWithAttribute:FLEXFileBrowserSortAttributeName];
}]];
[alertController addAction:[UIAlertAction actionWithTitle:@"Creation Date"
style:UIAlertActionStyleDefault
handler:^(UIAlertAction * _Nonnull action) {
[self sortWithAttribute:FLEXFileBrowserSortAttributeCreationDate];
}]];
[self presentViewController:alertController animated:YES completion:nil];
}
- (void)sortWithAttribute:(FLEXFileBrowserSortAttribute)attribute {
self.sortAttribute = attribute;
[self reloadDisplayedPaths];
}
#pragma mark - FLEXGlobalsEntry
@@ -321,7 +361,7 @@
#if FLEX_AT_LEAST_IOS13_SDK
- (UIContextMenuConfiguration *)tableView:(UITableView *)tableView contextMenuConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath point:(CGPoint)point __IOS_AVAILABLE(13.0) {
__weak typeof(self) weakSelf = self;
__weak __typeof__(self) weakSelf = self;
return [UIContextMenuConfiguration configurationWithIdentifier:nil
previewProvider:nil
actionProvider:^UIMenu * _Nullable(NSArray<UIMenuElement *> * _Nonnull suggestedActions) {
@@ -452,6 +492,27 @@
for (NSString *subpath in subpaths) {
[childPaths addObject:[self.path stringByAppendingPathComponent:subpath]];
}
if (self.sortAttribute != FLEXFileBrowserSortAttributeNone) {
[childPaths sortUsingComparator:^NSComparisonResult(NSString *path1, NSString *path2) {
switch (self.sortAttribute) {
case FLEXFileBrowserSortAttributeNone:
// invalid state
return NSOrderedSame;
case FLEXFileBrowserSortAttributeName:
return [path1 compare:path2];
case FLEXFileBrowserSortAttributeCreationDate: {
NSDictionary<NSFileAttributeKey, id> *path1Attributes = [NSFileManager.defaultManager attributesOfItemAtPath:path1
error:NULL];
NSDictionary<NSFileAttributeKey, id> *path2Attributes = [NSFileManager.defaultManager attributesOfItemAtPath:path2
error:NULL];
NSDate *path1Date = path1Attributes[NSFileCreationDate];
NSDate *path2Date = path2Attributes[NSFileCreationDate];
return [path1Date compare:path2Date];
}
}
}];
}
self.childPaths = childPaths;
}
@@ -7,7 +7,7 @@
//
#import "FLEXGlobalsSection.h"
#import "NSArray+Functional.h"
#import "NSArray+FLEX.h"
#import "UIFont+FLEX.h"
@interface FLEXGlobalsSection ()
@@ -10,6 +10,7 @@
@protocol FLEXGlobalsTableViewControllerDelegate;
typedef NS_ENUM(NSUInteger, FLEXGlobalsSectionKind) {
FLEXGlobalsSectionCustom,
/// NSProcessInfo, Network history, system log,
/// heap, address explorer, libraries, app classes
FLEXGlobalsSectionProcessAndEvents,
@@ -19,7 +20,6 @@ typedef NS_ENUM(NSUInteger, FLEXGlobalsSectionKind) {
FLEXGlobalsSectionAppShortcuts,
/// UIPasteBoard.general, UIScreen, UIDevice
FLEXGlobalsSectionMisc,
FLEXGlobalsSectionCustom,
FLEXGlobalsSectionCount
};
@@ -10,12 +10,12 @@
#import "FLEXUtility.h"
#import "FLEXRuntimeUtility.h"
#import "FLEXObjcRuntimeViewController.h"
#import "FLEXKeychainTableViewController.h"
#import "FLEXKeychainViewController.h"
#import "FLEXObjectExplorerViewController.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXLiveObjectsTableViewController.h"
#import "FLEXFileBrowserTableViewController.h"
#import "FLEXCookiesTableViewController.h"
#import "FLEXLiveObjectsController.h"
#import "FLEXFileBrowserController.h"
#import "FLEXCookiesViewController.h"
#import "FLEXGlobalsEntry.h"
#import "FLEXManager+Private.h"
#import "FLEXSystemLogViewController.h"
@@ -39,14 +39,14 @@
+ (NSString *)globalsTitleForSection:(FLEXGlobalsSectionKind)section {
switch (section) {
case FLEXGlobalsSectionCustom:
return @"Custom Additions";
case FLEXGlobalsSectionProcessAndEvents:
return @"Process and Events";
case FLEXGlobalsSectionAppShortcuts:
return @"App Shortcuts";
case FLEXGlobalsSectionMisc:
return @"Miscellaneous";
case FLEXGlobalsSectionCustom:
return @"Custom Additions";
default:
@throw NSInternalInconsistencyException;
@@ -56,18 +56,18 @@
+ (FLEXGlobalsEntry *)globalsEntryForRow:(FLEXGlobalsRow)row {
switch (row) {
case FLEXGlobalsRowAppKeychainItems:
return [FLEXKeychainTableViewController flex_concreteGlobalsEntry:row];
return [FLEXKeychainViewController flex_concreteGlobalsEntry:row];
case FLEXGlobalsRowAddressInspector:
return [FLEXAddressExplorerCoordinator flex_concreteGlobalsEntry:row];
case FLEXGlobalsRowBrowseRuntime:
return [FLEXObjcRuntimeViewController flex_concreteGlobalsEntry:row];
case FLEXGlobalsRowLiveObjects:
return [FLEXLiveObjectsTableViewController flex_concreteGlobalsEntry:row];
return [FLEXLiveObjectsController flex_concreteGlobalsEntry:row];
case FLEXGlobalsRowCookies:
return [FLEXCookiesTableViewController flex_concreteGlobalsEntry:row];
return [FLEXCookiesViewController flex_concreteGlobalsEntry:row];
case FLEXGlobalsRowBrowseBundle:
case FLEXGlobalsRowBrowseContainer:
return [FLEXFileBrowserTableViewController flex_concreteGlobalsEntry:row];
return [FLEXFileBrowserController flex_concreteGlobalsEntry:row];
case FLEXGlobalsRowSystemLog:
return [FLEXSystemLogViewController flex_concreteGlobalsEntry:row];
case FLEXGlobalsRowNetworkHistory:
@@ -104,11 +104,11 @@
}
+ (NSArray<FLEXGlobalsSection *> *)defaultGlobalSections {
static NSArray<FLEXGlobalsSection *> *sections = nil;
static NSMutableArray<FLEXGlobalsSection *> *sections = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSArray *rowsBySection = @[
@[
NSDictionary<NSNumber *, NSArray<FLEXGlobalsEntry *> *> *rowsBySection = @{
@(FLEXGlobalsSectionProcessAndEvents) : @[
[self globalsEntryForRow:FLEXGlobalsRowNetworkHistory],
[self globalsEntryForRow:FLEXGlobalsRowSystemLog],
[self globalsEntryForRow:FLEXGlobalsRowProcessInfo],
@@ -116,7 +116,7 @@
[self globalsEntryForRow:FLEXGlobalsRowAddressInspector],
[self globalsEntryForRow:FLEXGlobalsRowBrowseRuntime],
],
@[ // FLEXGlobalsSectionAppShortcuts
@(FLEXGlobalsSectionAppShortcuts) : @[
[self globalsEntryForRow:FLEXGlobalsRowBrowseBundle],
[self globalsEntryForRow:FLEXGlobalsRowBrowseContainer],
[self globalsEntryForRow:FLEXGlobalsRowMainBundle],
@@ -128,7 +128,7 @@
[self globalsEntryForRow:FLEXGlobalsRowRootViewController],
[self globalsEntryForRow:FLEXGlobalsRowCookies],
],
@[ // FLEXGlobalsSectionMisc
@(FLEXGlobalsSectionMisc) : @[
[self globalsEntryForRow:FLEXGlobalsRowPasteboard],
[self globalsEntryForRow:FLEXGlobalsRowMainScreen],
[self globalsEntryForRow:FLEXGlobalsRowCurrentDevice],
@@ -144,12 +144,13 @@
[self globalsEntryForRow:FLEXGlobalsRowMainThread],
[self globalsEntryForRow:FLEXGlobalsRowOperationQueue],
]
];
sections = [NSArray flex_forEachUpTo:rowsBySection.count map:^FLEXGlobalsSection *(NSUInteger i) {
};
sections = [NSMutableArray array];
for (FLEXGlobalsSectionKind i = FLEXGlobalsSectionCustom + 1; i < FLEXGlobalsSectionCount; ++i) {
NSString *title = [self globalsTitleForSection:i];
return [FLEXGlobalsSection title:title rows:rowsBySection[i]];
}];
[sections addObject:[FLEXGlobalsSection title:title rows:rowsBySection[@(i)]]];
}
});
return sections;
@@ -180,8 +181,7 @@
}
- (NSArray<FLEXGlobalsSection *> *)makeSections {
NSArray *sections = [self.class defaultGlobalSections];
NSMutableArray<FLEXGlobalsSection *> *sections = [NSMutableArray array];
// Do we have custom sections to add?
if (FLEXManager.sharedManager.userGlobalEntries.count) {
NSString *title = [[self class] globalsTitleForSection:FLEXGlobalsSectionCustom];
@@ -189,9 +189,11 @@
title:title
rows:FLEXManager.sharedManager.userGlobalEntries
];
sections = [sections arrayByAddingObject:custom];
[sections addObject:custom];
}
[sections addObjectsFromArray:[self.class defaultGlobalSections]];
return sections;
}
@@ -1,5 +1,5 @@
//
// FLEXKeychainTableViewController.h
// FLEXKeychainViewController.h
// FLEX
//
// Created by ray on 2019/8/17.
@@ -9,6 +9,6 @@
#import "FLEXGlobalsEntry.h"
#import "FLEXFilteringTableViewController.h"
@interface FLEXKeychainTableViewController : FLEXFilteringTableViewController <FLEXGlobalsEntry>
@interface FLEXKeychainViewController : FLEXFilteringTableViewController <FLEXGlobalsEntry>
@end
@@ -1,5 +1,5 @@
//
// FLEXKeychainTableViewController.m
// FLEXKeychainViewController.m
// FLEX
//
// Created by ray on 2019/8/17.
@@ -8,17 +8,18 @@
#import "FLEXKeychain.h"
#import "FLEXKeychainQuery.h"
#import "FLEXKeychainTableViewController.h"
#import "FLEXKeychainViewController.h"
#import "FLEXTableViewCell.h"
#import "FLEXMutableListSection.h"
#import "FLEXUtility.h"
#import "UIPasteboard+FLEX.h"
#import "UIBarButtonItem+FLEX.h"
@interface FLEXKeychainTableViewController ()
@interface FLEXKeychainViewController ()
@property (nonatomic, readonly) FLEXMutableListSection<NSDictionary *> *section;
@end
@implementation FLEXKeychainTableViewController
@implementation FLEXKeychainViewController
- (id)init {
return [self initWithStyle:UITableViewStyleGrouped];
@@ -30,12 +31,8 @@
[super viewDidLoad];
self.navigationItem.rightBarButtonItems = @[
[[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemTrash target:self action:@selector(trashPressed)
],
[[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(addPressed)
],
[UIBarButtonItem systemItem:UIBarButtonSystemItemTrash target:self action:@selector(trashPressed:)],
[UIBarButtonItem systemItem:UIBarButtonSystemItemAdd target:self action:@selector(addPressed)],
];
[self reloadData];
@@ -127,7 +124,7 @@
#pragma mark Buttons
- (void)trashPressed {
- (void)trashPressed:(UIBarButtonItem *)sender {
[FLEXAlert makeSheet:^(FLEXAlert *make) {
make.title(@"Clear Keychain");
make.message(@"This will remove all keychain items for this app.\n");
@@ -140,7 +137,7 @@
[self reloadData];
});
make.button(@"Cancel").cancelStyle();
} showFrom:self];
} showFrom:self source:sender];
}
- (void)addPressed {
@@ -170,7 +167,7 @@
}
+ (UIViewController *)globalsEntryViewController:(FLEXGlobalsRow)row {
FLEXKeychainTableViewController *viewController = [self new];
FLEXKeychainViewController *viewController = [self new];
viewController.title = [self globalsEntryTitle:row];
return viewController;
@@ -19,6 +19,17 @@
/// been loaded since this method was first called.
- (void)reloadLibrariesList;
/// You must call this method on the main thread
/// before you attempt to call \c copySafeClassList.
+ (void)initializeWebKitLegacy;
/// Do not call unless you absolutely need all classes. This will cause
/// every class in the runtime to initialize itself, which is not common.
/// Before you call this method, call \c initializeWebKitLegacy on the main thread.
- (NSArray<Class> *)copySafeClassList;
- (NSArray<Protocol *> *)copyProtocolList;
/// An array of strings representing the currently loaded libraries.
@property (nonatomic, readonly) NSArray<NSString *> *imageDisplayNames;
@@ -7,10 +7,11 @@
//
#import "FLEXRuntimeClient.h"
#import "NSObject+Reflection.h"
#import "NSObject+FLEX_Reflection.h"
#import "FLEXMethod.h"
#import "NSArray+Functional.h"
#import "NSArray+FLEX.h"
#import "FLEXRuntimeSafety.h"
#include <dlfcn.h>
#define Equals(a, b) ([a compare:b options:NSCaseInsensitiveSearch] == NSOrderedSame)
#define Contains(a, b) ([a rangeOfString:b options:NSCaseInsensitiveSearch].location != NSNotFound)
@@ -187,6 +188,38 @@ static inline NSString * TBWildcardMap(NSString *token, NSString *candidate, TBW
#pragma mark - Public
+ (void)initializeWebKitLegacy {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
void *handle = dlopen(
"/System/Library/PrivateFrameworks/WebKitLegacy.framework/WebKitLegacy",
RTLD_LAZY
);
void (*WebKitInitialize)() = dlsym(handle, "WebKitInitialize");
if (WebKitInitialize) {
NSAssert(NSThread.isMainThread,
@"WebKitInitialize can only be called on the main thread"
);
WebKitInitialize();
}
});
}
- (NSArray<Class> *)copySafeClassList {
unsigned int count = 0;
Class *classes = objc_copyClassList(&count);
return [NSArray flex_forEachUpTo:count map:^id(NSUInteger i) {
Class cls = classes[i];
return FLEXClassIsSafe(cls) ? cls : nil;
}];
}
- (NSArray<Protocol *> *)copyProtocolList {
unsigned int count = 0;
Protocol *__unsafe_unretained *protocols = objc_copyProtocolList(&count);
return [NSArray arrayWithObjects:protocols count:count];
}
- (NSMutableArray<NSString *> *)bundleNamesForToken:(FLEXSearchToken *)token {
if (self.imagePaths.count) {
TBWildcardOptions options = token.options;
@@ -8,7 +8,7 @@
#import "FLEXRuntimeKeyPath.h"
/// Wraps FLEXRuntimeClient and provides caching mechanisms
/// Wraps FLEXRuntimeClient and provides extra caching mechanisms
@interface FLEXRuntimeController : NSObject
/// @return An array of strings if the key path only evaluates
@@ -30,6 +30,7 @@
+ (NSString *)imagePathWithShortName:(NSString *)suffix;
/// Gives back short names. For example, "Foundation.framework"
+ (NSArray<NSString*> *)allBundleNames;
@end
@@ -0,0 +1,29 @@
//
// FLEXRuntimeExporter.h
// FLEX
//
// Created by Tanner Bennett on 3/26/20.
// Copyright (c) 2020 Flipboard. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/// A class for exporting all runtime metadata to an SQLite database.
//API_AVAILABLE(ios(10.0))
@interface FLEXRuntimeExporter : NSObject
+ (void)createRuntimeDatabaseAtPath:(NSString *)path
progressHandler:(void(^)(NSString *status))progress
completion:(void(^)(NSString *_Nullable error))completion;
+ (void)createRuntimeDatabaseAtPath:(NSString *)path
forImages:(nullable NSArray<NSString *> *)images
progressHandler:(void(^)(NSString *status))progress
completion:(void(^)(NSString *_Nullable error))completion;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,875 @@
//
// FLEXRuntimeExporter.m
// FLEX
//
// Created by Tanner Bennett on 3/26/20.
// Copyright (c) 2020 Flipboard. All rights reserved.
//
#import "FLEXRuntimeExporter.h"
#import "FLEXSQLiteDatabaseManager.h"
#import "NSObject+FLEX_Reflection.h"
#import "FLEXRuntimeController.h"
#import "FLEXRuntimeClient.h"
#import "NSArray+FLEX.h"
#import "FLEXTypeEncodingParser.h"
#import <sqlite3.h>
#import "FLEXProtocol.h"
#import "FLEXProperty.h"
#import "FLEXIvar.h"
#import "FLEXMethodBase.h"
#import "FLEXMethod.h"
#import "FLEXPropertyAttributes.h"
NSString * const kFREEnableForeignKeys = @"PRAGMA foreign_keys = ON;";
/// Loaded images
NSString * const kFRECreateTableMachOCommand = @"CREATE TABLE MachO( "
"id INTEGER PRIMARY KEY AUTOINCREMENT, "
"shortName TEXT, "
"imagePath TEXT, "
"bundleID TEXT "
");";
NSString * const kFREInsertImage = @"INSERT INTO MachO ( "
"shortName, imagePath, bundleID "
") VALUES ( "
"$shortName, $imagePath, $bundleID "
");";
/// Objc classes
NSString * const kFRECreateTableClassCommand = @"CREATE TABLE Class( "
"id INTEGER PRIMARY KEY AUTOINCREMENT, "
"className TEXT, "
"superclass INTEGER, "
"instanceSize INTEGER, "
"version INTEGER, "
"image INTEGER, "
"FOREIGN KEY(superclass) REFERENCES Class(id), "
"FOREIGN KEY(image) REFERENCES MachO(id) "
");";
NSString * const kFREInsertClass = @"INSERT INTO Class ( "
"className, instanceSize, version, image "
") VALUES ( "
"$className, $instanceSize, $version, $image "
");";
NSString * const kFREUpdateClassSetSuper = @"UPDATE Class SET superclass = $super WHERE id = $id;";
/// Unique objc selectors
NSString * const kFRECreateTableSelectorCommand = @"CREATE TABLE Selector( "
"id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "
"name text NOT NULL UNIQUE "
");";
NSString * const kFREInsertSelector = @"INSERT OR IGNORE INTO Selector (name) VALUES ($name);";
/// Unique objc type encodings
NSString * const kFRECreateTableTypeEncodingCommand = @"CREATE TABLE TypeEncoding( "
"id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "
"string text NOT NULL UNIQUE, "
"size integer "
");";
NSString * const kFREInsertTypeEncoding = @"INSERT OR IGNORE INTO TypeEncoding "
"(string, size) VALUES ($type, $size);";
/// Unique objc type signatures
NSString * const kFRECreateTableTypeSignatureCommand = @"CREATE TABLE TypeSignature( "
"id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "
"string text NOT NULL UNIQUE "
");";
NSString * const kFREInsertTypeSignature = @"INSERT OR IGNORE INTO TypeSignature "
"(string) VALUES ($type);";
NSString * const kFRECreateTableMethodSignatureCommand = @"CREATE TABLE MethodSignature( "
"id INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "
"typeEncoding TEXT, "
"argc INTEGER, "
"returnType INTEGER, "
"frameLength INTEGER, "
"FOREIGN KEY(returnType) REFERENCES TypeEncoding(id) "
");";
NSString * const kFREInsertMethodSignature = @"INSERT INTO MethodSignature ( "
"typeEncoding, argc, returnType, frameLength "
") VALUES ( "
"$typeEncoding, $argc, $returnType, $frameLength "
");";
NSString * const kFRECreateTableMethodCommand = @"CREATE TABLE Method( "
"id INTEGER PRIMARY KEY AUTOINCREMENT, "
"sel INTEGER, "
"class INTEGER, "
"instance INTEGER, " // 0 if class method, 1 if instance method
"signature INTEGER, "
"image INTEGER, "
"FOREIGN KEY(sel) REFERENCES Selector(id), "
"FOREIGN KEY(class) REFERENCES Class(id), "
"FOREIGN KEY(signature) REFERENCES MethodSignature(id), "
"FOREIGN KEY(image) REFERENCES MachO(id) "
");";
NSString * const kFREInsertMethod = @"INSERT INTO Method ( "
"sel, class, instance, signature, image "
") VALUES ( "
"$sel, $class, $instance, $signature, $image "
");";
NSString * const kFRECreateTablePropertyCommand = @"CREATE TABLE Property( "
"id INTEGER PRIMARY KEY AUTOINCREMENT, "
"name TEXT, "
"class INTEGER, "
"instance INTEGER, " // 0 if class prop, 1 if instance prop
"image INTEGER, "
"attributes TEXT, "
"customGetter INTEGER, "
"customSetter INTEGER, "
"type INTEGER, "
"ivar TEXT, "
"readonly INTEGER, "
"copy INTEGER, "
"retained INTEGER, "
"nonatomic INTEGER, "
"dynamic INTEGER, "
"weak INTEGER, "
"canGC INTEGER, "
"FOREIGN KEY(class) REFERENCES Class(id), "
"FOREIGN KEY(customGetter) REFERENCES Selector(id), "
"FOREIGN KEY(customSetter) REFERENCES Selector(id), "
"FOREIGN KEY(image) REFERENCES MachO(id) "
");";
NSString * const kFREInsertProperty = @"INSERT INTO Property ( "
"name, class, instance, attributes, image, "
"customGetter, customSetter, type, ivar, readonly, "
"copy, retained, nonatomic, dynamic, weak, canGC "
") VALUES ( "
"$name, $class, $instance, $attributes, $image, "
"$customGetter, $customSetter, $type, $ivar, $readonly, "
"$copy, $retained, $nonatomic, $dynamic, $weak, $canGC "
");";
NSString * const kFRECreateTableIvarCommand = @"CREATE TABLE Ivar( "
"id INTEGER PRIMARY KEY AUTOINCREMENT, "
"name TEXT, "
"offset INTEGER, "
"type INTEGER, "
"class INTEGER, "
"image INTEGER, "
"FOREIGN KEY(type) REFERENCES TypeEncoding(id), "
"FOREIGN KEY(class) REFERENCES Class(id), "
"FOREIGN KEY(image) REFERENCES MachO(id) "
");";
NSString * const kFREInsertIvar = @"INSERT INTO Ivar ( "
"name, offset, type, class, image "
") VALUES ( "
"$name, $offset, $type, $class, $image "
");";
NSString * const kFRECreateTableProtocolCommand = @"CREATE TABLE Protocol( "
"id INTEGER PRIMARY KEY AUTOINCREMENT, "
"name TEXT, "
"image INTEGER, "
"FOREIGN KEY(image) REFERENCES MachO(id) "
");";
NSString * const kFREInsertProtocol = @"INSERT INTO Protocol "
"(name, image) VALUES ($name, $image);";
NSString * const kFRECreateTableProtocolPropertyCommand = @"CREATE TABLE ProtocolMember( "
"id INTEGER PRIMARY KEY AUTOINCREMENT, "
"protocol INTEGER, "
"required INTEGER, "
"instance INTEGER, " // 0 if class member, 1 if instance member
// Only of the two below is used
"property TEXT, "
"method TEXT, "
"image INTEGER, "
"FOREIGN KEY(protocol) REFERENCES Protocol(id), "
"FOREIGN KEY(image) REFERENCES MachO(id) "
");";
NSString * const kFREInsertProtocolMember = @"INSERT INTO ProtocolMember ( "
"protocol, required, instance, property, method, image "
") VALUES ( "
"$protocol, $required, $instance, $property, $method, $image "
");";
/// For protocols conforming to other protocols
NSString * const kFRECreateTableProtocolConformanceCommand = @"CREATE TABLE ProtocolConformance( "
"protocol INTEGER, "
"conformance INTEGER, "
"FOREIGN KEY(protocol) REFERENCES Protocol(id), "
"FOREIGN KEY(conformance) REFERENCES Protocol(id) "
");";
NSString * const kFREInsertProtocolConformance = @"INSERT INTO ProtocolConformance "
"(protocol, conformance) VALUES ($protocol, $conformance);";
/// For classes conforming to protocols
NSString * const kFRECreateTableClassConformanceCommand = @"CREATE TABLE ClassConformance( "
"class INTEGER, "
"conformance INTEGER, "
"FOREIGN KEY(class) REFERENCES Class(id), "
"FOREIGN KEY(conformance) REFERENCES Protocol(id) "
");";
NSString * const kFREInsertClassConformance = @"INSERT INTO ClassConformance "
"(class, conformance) VALUES ($class, $conformance);";
@interface FLEXRuntimeExporter ()
@property (nonatomic, readonly) FLEXSQLiteDatabaseManager *db;
@property (nonatomic, copy) NSArray<NSString *> *loadedShortBundleNames;
@property (nonatomic, copy) NSArray<NSString *> *loadedBundlePaths;
@property (nonatomic, copy) NSArray<FLEXProtocol *> *protocols;
@property (nonatomic, copy) NSArray<Class> *classes;
@property (nonatomic) NSMutableDictionary<NSString *, NSNumber *> *bundlePathsToIDs;
@property (nonatomic) NSMutableDictionary<NSString *, NSNumber *> *protocolsToIDs;
@property (nonatomic) NSMutableDictionary<Class, NSNumber *> *classesToIDs;
@property (nonatomic) NSMutableDictionary<NSString *, NSNumber *> *typeEncodingsToIDs;
@property (nonatomic) NSMutableDictionary<NSString *, NSNumber *> *methodSignaturesToIDs;
@property (nonatomic) NSMutableDictionary<NSString *, NSNumber *> *selectorsToIDs;
@end
@implementation FLEXRuntimeExporter
+ (NSString *)tempFilename {
NSString *temp = NSTemporaryDirectory();
NSString *uuid = [NSUUID.UUID.UUIDString substringToIndex:8];
NSString *filename = [NSString stringWithFormat:@"FLEXRuntimeDatabase-%@.db", uuid];
return [temp stringByAppendingPathComponent:filename];
}
+ (void)createRuntimeDatabaseAtPath:(NSString *)path
progressHandler:(void(^)(NSString *status))progress
completion:(void (^)(NSString *))completion {
[self createRuntimeDatabaseAtPath:path forImages:nil progressHandler:progress completion:completion];
}
+ (void)createRuntimeDatabaseAtPath:(NSString *)path
forImages:(NSArray<NSString *> *)images
progressHandler:(void(^)(NSString *status))progress
completion:(void(^)(NSString *_Nullable error))completion {
__typeof(completion) callback = ^(NSString *error) {
dispatch_async(dispatch_get_main_queue(), ^{
completion(error);
});
};
// This must be called on the main thread first
if (NSThread.isMainThread) {
[FLEXRuntimeClient initializeWebKitLegacy];
} else {
dispatch_sync(dispatch_get_main_queue(), ^{
[FLEXRuntimeClient initializeWebKitLegacy];
});
}
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
NSError *error = nil;
NSString *errorMessage = nil;
// Get unused temp filename, remove existing database if any
NSString *tempPath = [self tempFilename];
if ([NSFileManager.defaultManager fileExistsAtPath:tempPath]) {
[NSFileManager.defaultManager removeItemAtPath:tempPath error:&error];
if (error) {
callback(error.localizedDescription);
return;
}
}
// Attempt to create and populate the database, abort if we fail
FLEXRuntimeExporter *exporter = [self new];
exporter.loadedBundlePaths = images;
if (![exporter createAndPopulateDatabaseAtPath:tempPath
progressHandler:progress
error:&errorMessage]) {
// Remove temp database if it was not moved
if ([NSFileManager.defaultManager fileExistsAtPath:tempPath]) {
[NSFileManager.defaultManager removeItemAtPath:tempPath error:nil];
}
callback(errorMessage);
return;
}
// Remove old database at given path
if ([NSFileManager.defaultManager fileExistsAtPath:path]) {
[NSFileManager.defaultManager removeItemAtPath:path error:&error];
if (error) {
callback(error.localizedDescription);
return;
}
}
// Move new database to desired path
[NSFileManager.defaultManager moveItemAtPath:tempPath toPath:path error:&error];
if (error) {
callback(error.localizedDescription);
}
// Remove temp database if it was not moved
if ([NSFileManager.defaultManager fileExistsAtPath:tempPath]) {
[NSFileManager.defaultManager removeItemAtPath:tempPath error:nil];
}
callback(nil);
});
}
- (id)init {
self = [super init];
if (self) {
_bundlePathsToIDs = [NSMutableDictionary new];
_protocolsToIDs = [NSMutableDictionary new];
_classesToIDs = [NSMutableDictionary new];
_typeEncodingsToIDs = [NSMutableDictionary new];
_methodSignaturesToIDs = [NSMutableDictionary new];
_selectorsToIDs = [NSMutableDictionary new];
_bundlePathsToIDs[NSNull.null] = (id)NSNull.null;
}
return self;
}
- (BOOL)createAndPopulateDatabaseAtPath:(NSString *)path
progressHandler:(void(^)(NSString *status))step
error:(NSString **)error {
_db = [FLEXSQLiteDatabaseManager managerForDatabase:path];
[self loadMetadata:step];
if ([self createTables] && [self addImages:step] && [self addProtocols:step] &&
[self addClasses:step] && [self setSuperclasses:step] &&
[self addProtocolConformances:step] && [self addClassConformances:step] &&
[self addIvars:step] && [self addMethods:step] && [self addProperties:step]) {
_db = nil; // Close the database
return YES;
}
*error = self.db.lastResult.message;
return NO;
}
- (void)loadMetadata:(void(^)(NSString *status))progress {
progress(@"Loading metadata…");
FLEXRuntimeClient *runtime = FLEXRuntimeClient.runtime;
// Only load metadata for the existing paths if any
if (self.loadedBundlePaths) {
// Images
self.loadedShortBundleNames = [self.loadedBundlePaths flex_mapped:^id(NSString *path, NSUInteger idx) {
return [runtime shortNameForImageName:path];
}];
// Classes
self.classes = [[runtime classesForToken:FLEXSearchToken.any
inBundles:self.loadedBundlePaths.mutableCopy
] flex_mapped:^id(NSString *cls, NSUInteger idx) {
return NSClassFromString(cls);
}];
} else {
// Images
self.loadedShortBundleNames = runtime.imageDisplayNames;
self.loadedBundlePaths = [self.loadedShortBundleNames flex_mapped:^id(NSString *name, NSUInteger idx) {
return [runtime imageNameForShortName:name];
}];
// Classes
self.classes = [runtime copySafeClassList];
}
// ...except protocols, because there's not a lot of them
// and there's no way load the protocols for a given image
self.protocols = [[runtime copyProtocolList] flex_mapped:^id(Protocol *proto, NSUInteger idx) {
return [FLEXProtocol protocol:proto];
}];
}
- (BOOL)createTables {
NSArray<NSString *> *commands = @[
kFREEnableForeignKeys,
kFRECreateTableMachOCommand,
kFRECreateTableClassCommand,
kFRECreateTableSelectorCommand,
kFRECreateTableTypeEncodingCommand,
kFRECreateTableTypeSignatureCommand,
kFRECreateTableMethodSignatureCommand,
kFRECreateTableMethodCommand,
kFRECreateTablePropertyCommand,
kFRECreateTableIvarCommand,
kFRECreateTableProtocolCommand,
kFRECreateTableProtocolPropertyCommand,
kFRECreateTableProtocolConformanceCommand,
kFRECreateTableClassConformanceCommand
];
for (NSString *command in commands) {
if (![self.db executeStatement:command]) {
return NO;
}
}
return YES;
}
- (BOOL)addImages:(void(^)(NSString *status))progress {
progress(@"Adding loaded images…");
FLEXSQLiteDatabaseManager *database = self.db;
NSArray *shortNames = self.loadedShortBundleNames;
NSArray *fullPaths = self.loadedBundlePaths;
NSParameterAssert(shortNames.count == fullPaths.count);
NSInteger count = shortNames.count;
for (NSInteger i = 0; i < count; i++) {
// Grab bundle ID
NSString *bundleID = [NSBundle
bundleWithPath:fullPaths[i]
].bundleIdentifier;
[database executeStatement:kFREInsertImage arguments:@{
@"$shortName": shortNames[i],
@"$imagePath": fullPaths[i],
@"$bundleID": bundleID ?: NSNull.null
}];
if (database.lastResult.isError) {
return NO;
} else {
self.bundlePathsToIDs[fullPaths[i]] = @(database.lastRowID);
}
}
return YES;
}
NS_INLINE BOOL FREInsertProtocolMember(FLEXSQLiteDatabaseManager *db,
id proto, id required, id instance,
id prop, id methSel, id image) {
return ![db executeStatement:kFREInsertProtocolMember arguments:@{
@"$protocol": proto,
@"$required": required,
@"$instance": instance ?: NSNull.null,
@"$property": prop ?: NSNull.null,
@"$method": methSel ?: NSNull.null,
@"$image": image
}].isError;
}
- (BOOL)addProtocols:(void(^)(NSString *status))progress {
progress([NSString stringWithFormat:@"Adding %@ protocols…", @(self.protocols.count)]);
FLEXSQLiteDatabaseManager *database = self.db;
NSDictionary *imageIDs = self.bundlePathsToIDs;
for (FLEXProtocol *proto in self.protocols) {
id imagePath = proto.imagePath ?: NSNull.null;
NSNumber *image = imageIDs[imagePath] ?: NSNull.null;
NSNumber *pid = nil;
// Insert protocol
BOOL failed = [database executeStatement:kFREInsertProtocol arguments:@{
@"$name": proto.name, @"$image": image
}].isError;
// Cache rowid
if (failed) {
return NO;
} else {
self.protocolsToIDs[proto.name] = pid = @(database.lastRowID);
}
// Insert its members //
// Required methods
for (FLEXMethodDescription *method in proto.requiredMethods) {
NSString *selector = NSStringFromSelector(method.selector);
if (!FREInsertProtocolMember(database, pid, @YES, method.instance, nil, selector, image)) {
return NO;
}
}
// Optional methods
for (FLEXMethodDescription *method in proto.optionalMethods) {
NSString *selector = NSStringFromSelector(method.selector);
if (!FREInsertProtocolMember(database, pid, @NO, method.instance, nil, selector, image)) {
return NO;
}
}
if (@available(iOS 10, *)) {
// Required properties
for (FLEXProperty *property in proto.requiredProperties) {
BOOL success = FREInsertProtocolMember(
database, pid, @YES, @(property.isClassProperty), property.name, NSNull.null, image
);
if (!success) return NO;
}
// Optional properties
for (FLEXProperty *property in proto.optionalProperties) {
BOOL success = FREInsertProtocolMember(
database, pid, @NO, @(property.isClassProperty), property.name, NSNull.null, image
);
if (!success) return NO;
}
} else {
// Just... properties.
for (FLEXProperty *property in proto.properties) {
BOOL success = FREInsertProtocolMember(
database, pid, nil, @(property.isClassProperty), property.name, NSNull.null, image
);
if (!success) return NO;
}
}
}
return YES;
}
- (BOOL)addProtocolConformances:(void(^)(NSString *status))progress {
progress(@"Adding protocol-to-protocol conformances…");
FLEXSQLiteDatabaseManager *database = self.db;
NSDictionary *protocolIDs = self.protocolsToIDs;
for (FLEXProtocol *proto in self.protocols) {
id protoID = protocolIDs[proto.name];
for (FLEXProtocol *conform in proto.protocols) {
BOOL failed = [database executeStatement:kFREInsertProtocolConformance arguments:@{
@"$protocol": protoID,
@"$conformance": protocolIDs[conform.name]
}].isError;
if (failed) {
return NO;
}
}
}
return YES;
}
- (BOOL)addClasses:(void(^)(NSString *status))progress {
progress([NSString stringWithFormat:@"Adding %@ classes…", @(self.classes.count)]);
FLEXSQLiteDatabaseManager *database = self.db;
NSDictionary *imageIDs = self.bundlePathsToIDs;
for (Class cls in self.classes) {
const char *imageName = class_getImageName(cls);
id image = imageName ? imageIDs[@(imageName)] : NSNull.null;
image = image ?: NSNull.null;
BOOL failed = [database executeStatement:kFREInsertClass arguments:@{
@"$className": NSStringFromClass(cls),
@"$instanceSize": @(class_getInstanceSize(cls)),
@"$version": @(class_getVersion(cls)),
@"$image": image
}].isError;
if (failed) {
return NO;
} else {
self.classesToIDs[(id)cls] = @(database.lastRowID);
}
}
return YES;
}
- (BOOL)setSuperclasses:(void(^)(NSString *status))progress {
progress(@"Setting superclasses…");
FLEXSQLiteDatabaseManager *database = self.db;
for (Class cls in self.classes) {
// Grab superclass ID
Class superclass = class_getSuperclass(cls);
NSNumber *superclassID = _classesToIDs[class_getSuperclass(cls)];
// ... or add the superclass and cache its ID if the
// superclass does not reside in the target image(s)
if (!superclassID) {
NSDictionary *args = @{ @"$className": NSStringFromClass(superclass) };
BOOL failed = [database executeStatement:kFREInsertClass arguments:args].isError;
if (failed) { return NO; }
_classesToIDs[(id)superclass] = superclassID = @(database.lastRowID);
}
if (superclass) {
BOOL failed = [database executeStatement:kFREUpdateClassSetSuper arguments:@{
@"$super": superclassID, @"$id": _classesToIDs[cls]
}].isError;
if (failed) {
return NO;
}
}
}
return YES;
}
- (BOOL)addClassConformances:(void(^)(NSString *status))progress {
progress(@"Adding class-to-protocol conformances…");
FLEXSQLiteDatabaseManager *database = self.db;
NSDictionary *protocolIDs = self.protocolsToIDs;
NSDictionary *classIDs = self.classesToIDs;
for (Class cls in self.classes) {
id classID = classIDs[(id)cls];
for (FLEXProtocol *conform in FLEXGetConformedProtocols(cls)) {
BOOL failed = [database executeStatement:kFREInsertClassConformance arguments:@{
@"$class": classID,
@"$conformance": protocolIDs[conform.name]
}].isError;
if (failed) {
return NO;
}
}
}
return YES;
}
- (BOOL)addIvars:(void(^)(NSString *status))progress {
progress(@"Adding ivars…");
FLEXSQLiteDatabaseManager *database = self.db;
NSDictionary *imageIDs = self.bundlePathsToIDs;
for (Class cls in self.classes) {
for (FLEXIvar *ivar in FLEXGetAllIvars(cls)) {
// Insert type first
if (![self addTypeEncoding:ivar.typeEncoding size:ivar.size]) {
return NO;
}
id imagePath = ivar.imagePath ?: NSNull.null;
NSNumber *image = imageIDs[imagePath] ?: NSNull.null;
BOOL failed = [database executeStatement:kFREInsertIvar arguments:@{
@"$name": ivar.name,
@"$offset": @(ivar.offset),
@"$type": _typeEncodingsToIDs[ivar.typeEncoding],
@"$class": _classesToIDs[cls],
@"$image": image
}].isError;
if (failed) {
return NO;
}
}
}
return YES;
}
- (BOOL)addMethods:(void(^)(NSString *status))progress {
progress(@"Adding methods…");
FLEXSQLiteDatabaseManager *database = self.db;
NSDictionary *imageIDs = self.bundlePathsToIDs;
// Loop over all classes
for (Class cls in self.classes) {
NSNumber *classID = _classesToIDs[(id)cls];
const char *imageName = class_getImageName(cls);
id image = imageName ? imageIDs[@(imageName)] : NSNull.null;
image = image ?: NSNull.null;
// Block used to process each message
BOOL (^insert)(FLEXMethod *, NSNumber *) = ^BOOL(FLEXMethod *method, NSNumber *instance) {
// Insert selector and signature first
if (![self addSelector:method.selectorString]) {
return NO;
}
if (![self addMethodSignature:method]) {
return NO;
}
return ![database executeStatement:kFREInsertMethod arguments:@{
@"$sel": self->_selectorsToIDs[method.selectorString],
@"$class": classID,
@"$instance": instance,
@"$signature": self->_methodSignaturesToIDs[method.signatureString],
@"$image": image
}].isError;
};
// Loop over all instance and class methods of that class //
for (FLEXMethod *method in FLEXGetAllMethods(cls, YES)) {
if (!insert(method, @YES)) {
return NO;
}
}
for (FLEXMethod *method in FLEXGetAllMethods(object_getClass(cls), NO)) {
if (!insert(method, @NO)) {
return NO;
}
}
}
return YES;
}
- (BOOL)addProperties:(void(^)(NSString *status))progress {
progress(@"Adding properties…");
FLEXSQLiteDatabaseManager *database = self.db;
NSDictionary *imageIDs = self.bundlePathsToIDs;
// Loop over all classes
for (Class cls in self.classes) {
NSNumber *classID = _classesToIDs[(id)cls];
// Block used to process each message
BOOL (^insert)(FLEXProperty *, NSNumber *) = ^BOOL(FLEXProperty *property, NSNumber *instance) {
FLEXPropertyAttributes *attrs = property.attributes;
NSString *customGetter = attrs.customGetterString;
NSString *customSetter = attrs.customSetterString;
// Insert selectors first
if (customGetter) {
if (![self addSelector:customGetter]) {
return NO;
}
}
if (customSetter) {
if (![self addSelector:customSetter]) {
return NO;
}
}
// Insert type encoding first
NSInteger size = [FLEXTypeEncodingParser
sizeForTypeEncoding:attrs.typeEncoding alignment:nil
];
if (![self addTypeEncoding:attrs.typeEncoding size:size]) {
return NO;
}
id imagePath = property.imagePath ?: NSNull.null;
id image = imageIDs[imagePath] ?: NSNull.null;
return ![database executeStatement:kFREInsertProperty arguments:@{
@"$name": property.name,
@"$class": classID,
@"$instance": instance,
@"$image": image,
@"$attributes": attrs.string,
@"$customGetter": self->_selectorsToIDs[customGetter] ?: NSNull.null,
@"$customSetter": self->_selectorsToIDs[customSetter] ?: NSNull.null,
@"$type": self->_typeEncodingsToIDs[attrs.typeEncoding] ?: NSNull.null,
@"$ivar": attrs.backingIvar ?: NSNull.null,
@"$readonly": @(attrs.isReadOnly),
@"$copy": @(attrs.isCopy),
@"$retained": @(attrs.isRetained),
@"$nonatomic": @(attrs.isNonatomic),
@"$dynamic": @(attrs.isDynamic),
@"$weak": @(attrs.isWeak),
@"$canGC": @(attrs.isGarbageCollectable),
}].isError;
};
// Loop over all instance and class methods of that class //
for (FLEXProperty *property in FLEXGetAllProperties(cls)) {
if (!insert(property, @YES)) {
return NO;
}
}
for (FLEXProperty *property in FLEXGetAllProperties(object_getClass(cls))) {
if (!insert(property, @NO)) {
return NO;
}
}
}
return YES;
}
- (BOOL)addSelector:(NSString *)sel {
return [self executeInsert:kFREInsertSelector args:@{
@"$name": sel
} key:sel cacheResult:_selectorsToIDs];
}
- (BOOL)addTypeEncoding:(NSString *)type size:(NSInteger)size {
return [self executeInsert:kFREInsertTypeEncoding args:@{
@"$type": type, @"$size": @(size)
} key:type cacheResult:_typeEncodingsToIDs];
}
- (BOOL)addMethodSignature:(FLEXMethod *)method {
NSString *signature = method.signatureString;
NSString *returnType = @((char *)method.returnType);
// Insert return type first
if (![self addTypeEncoding:returnType size:method.returnSize]) {
return NO;
}
return [self executeInsert:kFREInsertMethodSignature args:@{
@"$typeEncoding": signature,
@"$returnType": _typeEncodingsToIDs[returnType],
@"$argc": @(method.numberOfArguments),
@"$frameLength": @(method.signature.frameLength)
} key:signature cacheResult:_methodSignaturesToIDs];
}
- (BOOL)executeInsert:(NSString *)statement
args:(NSDictionary *)args
key:(NSString *)cacheKey
cacheResult:(NSMutableDictionary<NSString *, NSNumber *> *)rowids {
// Check if already inserted
if (rowids[cacheKey]) {
return YES;
}
// Insert
FLEXSQLiteDatabaseManager *database = _db;
[database executeStatement:statement arguments:args];
if (database.lastResult.isError) {
return NO;
}
// Cache rowid
rowids[cacheKey] = @(database.lastRowID);
return YES;
}
@end
@@ -10,9 +10,9 @@
#import "FLEXRuntimeKeyPathTokenizer.h"
#import "FLEXRuntimeController.h"
#import "NSString+FLEX.h"
#import "NSArray+Functional.h"
#import "NSArray+FLEX.h"
#import "UITextField+Range.h"
#import "NSTimer+Blocks.h"
#import "NSTimer+FLEX.h"
#import "FLEXTableView.h"
#import "FLEXUtility.h"
#import "FLEXObjectExplorerFactory.h"
@@ -7,7 +7,7 @@
//
#import "FLEXRuntimeKeyPath.h"
#include <dlfcn.h>
#import "FLEXRuntimeClient.h"
@interface FLEXRuntimeKeyPath () {
NSString *flex_description;
@@ -49,29 +49,12 @@
keyPath->flex_description = keyPathString;
if (bundle.isAny && cls.isAny && method.isAny) {
[self initializeWebKitLegacy];
[FLEXRuntimeClient initializeWebKitLegacy];
}
return keyPath;
}
+ (void)initializeWebKitLegacy {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
void *handle = dlopen(
"/System/Library/PrivateFrameworks/WebKitLegacy.framework/WebKitLegacy",
RTLD_LAZY
);
void (*WebKitInitialize)() = dlsym(handle, "WebKitInitialize");
if (WebKitInitialize) {
NSAssert(NSThread.isMainThread,
@"WebKitInitialize can only be called on the main thread"
);
WebKitInitialize();
}
});
}
- (NSString *)description {
return flex_description;
}
@@ -14,6 +14,8 @@
#ifndef ActivityStreamSPI_h
#define ActivityStreamSPI_h
#include <Foundation/Foundation.h>
#include <sys/time.h>
// #include <xpc/xpc.h>
@@ -42,6 +42,11 @@ static BOOL my_os_log_shim_enabled(void *addr) {
#pragma mark - Initialization
+ (void)load {
// User must opt-into disabling os_log
if (!NSUserDefaults.standardUserDefaults.flex_disableOSLog) {
return;
}
// Thanks to @Ram4096 on GitHub for telling me that
// os_log is conditionally enabled by the SDK version
void *addr = __builtin_return_address(0);
@@ -51,28 +56,28 @@ static BOOL my_os_log_shim_enabled(void *addr) {
return;
}
FLEXDidHookNSLog = rebind_symbols((struct rebinding[1]) {{
FLEXDidHookNSLog = flex_rebind_symbols((struct rebinding[1]) {{
"os_log_shim_enabled",
(void *)my_os_log_shim_enabled,
(void **)&orig_os_log_shim_enabled
}}, 1) == 0;
if (FLEXDidHookNSLog && orig_os_log_shim_enabled != nil) {
// Check if our rebinding worked
FLEXNSLogHookWorks = my_os_log_shim_enabled(addr) == NO;
}
// So, just because we rebind the lazily loaded symbol for
// this function doesn't mean it's even going to be used.
// While it seems to be sufficient for the simulator, for
// whatever reason it is not sufficient on-device. We need
// to actually hook the function with something like Substrate.
// Check if we have substrate, and if so use that instead
void *handle = dlopen("/usr/lib/libsubstrate.dylib", RTLD_LAZY);
if (handle) {
MSHookFunction = dlsym(handle, "MSHookFunction");
if (MSHookFunction) {
// Set the hook and check if it worked
void *unused;
@@ -91,16 +96,16 @@ static BOOL my_os_log_shim_enabled(void *addr) {
- (void)viewDidLoad {
[super viewDidLoad];
self.showsSearchBar = YES;
self.showSearchBarInitially = NO;
__weak __typeof(self) weakSelf = self;
id logHandler = ^(NSArray<FLEXSystemLogMessage *> *newMessages) {
__strong __typeof(weakSelf) self = weakSelf;
[self handleUpdateWithNewMessages:newMessages];
__strong __typeof(weakSelf) strongSelf = weakSelf;
[strongSelf handleUpdateWithNewMessages:newMessages];
};
if (FLEXOSLogAvailable() && !FLEXNSLogHookWorks) {
_logController = [FLEXOSLogController withUpdateHandler:logHandler];
} else {
@@ -108,10 +113,10 @@ static BOOL my_os_log_shim_enabled(void *addr) {
}
self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
self.title = @"Loading...";
self.title = @"Waiting for Logs...";
// Toolbar buttons //
UIBarButtonItem *scrollDown = [UIBarButtonItem
itemWithImage:FLEXResources.scrollToBottomIcon
target:self
@@ -122,12 +127,8 @@ static BOOL my_os_log_shim_enabled(void *addr) {
target:self
action:@selector(showLogSettings)
];
if (FLEXOSLogAvailable() && !FLEXNSLogHookWorks) {
[self addToolbarItems:@[scrollDown, settings]];
} else {
[self addToolbarItems:@[scrollDown]];
}
[self addToolbarItems:@[scrollDown, settings]];
}
- (void)viewWillAppear:(BOOL)animated {
@@ -137,26 +138,30 @@ static BOOL my_os_log_shim_enabled(void *addr) {
}
- (NSArray<FLEXTableViewSection *> *)makeSections {
__weak __typeof(self) weakSelf = self;
_logMessages = [FLEXMutableListSection list:@[]
cellConfiguration:^(FLEXSystemLogCell *cell, FLEXSystemLogMessage *message, NSInteger row) {
cell.logMessage = message;
cell.highlightedText = self.filterText;
if (row % 2 == 0) {
cell.backgroundColor = FLEXColor.primaryBackgroundColor;
} else {
cell.backgroundColor = FLEXColor.secondaryBackgroundColor;
__strong __typeof(self) strongSelf = weakSelf;
if (strongSelf) {
cell.logMessage = message;
cell.highlightedText = strongSelf.filterText;
if (row % 2 == 0) {
cell.backgroundColor = FLEXColor.primaryBackgroundColor;
} else {
cell.backgroundColor = FLEXColor.secondaryBackgroundColor;
}
}
} filterMatcher:^BOOL(NSString *filterText, FLEXSystemLogMessage *message) {
NSString *displayedText = [FLEXSystemLogCell displayedTextForLogMessage:message];
return [displayedText localizedCaseInsensitiveContainsString:filterText];
}
];
self.logMessages.cellRegistrationMapping = @{
kFLEXSystemLogCellIdentifier : [FLEXSystemLogCell class]
};
return @[self.logMessages];
}
@@ -191,17 +196,35 @@ static BOOL my_os_log_shim_enabled(void *addr) {
}
- (void)showLogSettings {
NSUserDefaults *defaults = NSUserDefaults.standardUserDefaults;
BOOL disableOSLog = defaults.flex_disableOSLog;
BOOL persistent = defaults.flex_cacheOSLogMessages;
NSString *aslToggle = disableOSLog ? @"Enable os_log (default)" : @"Disable os_log";
NSString *persistence = persistent ? @"Disable persistent logging" : @"Enable persistent logging";
NSString *title = @"System Log Settings";
NSString *body = @"In iOS 10 and up, ASL has been replaced by os_log. "
"The os_log API is much more limited. Below, you can opt-into the old behavior "
"if you want cleaner, more reliable logs within FLEX, but this will break "
"anything that expects os_log to be working, such as Console.app. "
"This setting requires the app to restart to take effect. \n\n"
"To get as close to the old behavior as possible with os_log enabled, logs must "
"be collected manually at launch and stored. This setting has no effect "
"on iOS 9 and below, or if os_log is disabled. "
"You should only enable persistent logging when you need it.";
FLEXOSLogController *logController = (FLEXOSLogController *)self.logController;
BOOL persistent = NSUserDefaults.standardUserDefaults.flex_cacheOSLogMessages;
NSString *toggle = persistent ? @"Disable" : @"Enable";
NSString *title = [@"Persistent logging: " stringByAppendingString:persistent ? @"ON" : @"OFF"];
NSString *body = @"In iOS 10 and up, ASL is gone. The OS Log API is much more limited. "
"To get as close to the old behavior as possible, logs must be collected manually at launch and stored.\n\n"
"Turn this feature on only when you need it.";
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(title).message(body).button(toggle).handler(^(NSArray<NSString *> *strings) {
NSUserDefaults.standardUserDefaults.flex_cacheOSLogMessages = !persistent;
make.title(title).message(body);
make.button(aslToggle).destructiveStyle().handler(^(NSArray<NSString *> *strings) {
[defaults toggleBoolForKey:kFLEXDefaultsDisableOSLogForceASLKey];
});
make.button(persistence).handler(^(NSArray<NSString *> *strings) {
[defaults toggleBoolForKey:kFLEXDefaultsiOSPersistentOSLogKey];
logController.persistent = !persistent;
[logController.messages addObjectsFromArray:self.logMessages.list];
});
+27 -17
View File
@@ -15,7 +15,7 @@
#import "FLEXExplorerViewController.h"
#import "FLEXNetworkMITMViewController.h"
#import "FLEXKeyboardHelpViewController.h"
#import "FLEXFileBrowserTableViewController.h"
#import "FLEXFileBrowserController.h"
#import "FLEXUtility.h"
@interface FLEXManager (ExtensibilityPrivate)
@@ -63,7 +63,7 @@
- (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];
[FLEXKeyboardShortcutManager.sharedManager registerSimulatorShortcutWithKey:key modifiers:modifiers action:action description:description allowOverride:YES];
#endif
}
@@ -81,37 +81,47 @@
#endif
}
#pragma mark - Shortcuts Defaults
- (void)registerDefaultSimulatorShortcutWithKey:(NSString *)key modifiers:(UIKeyModifierFlags)modifiers action:(dispatch_block_t)action description:(NSString *)description {
#if TARGET_OS_SIMULATOR
// Don't allow override to avoid changing keys registered by the app
[FLEXKeyboardShortcutManager.sharedManager registerSimulatorShortcutWithKey:key modifiers:modifiers action:action description:description allowOverride:NO];
#endif
}
- (void)registerDefaultSimulatorShortcuts {
[self registerSimulatorShortcutWithKey:@"f" modifiers:0 action:^{
[self registerDefaultSimulatorShortcutWithKey:@"f" modifiers:0 action:^{
[self toggleExplorer];
} description:@"Toggle FLEX toolbar"];
[self registerSimulatorShortcutWithKey:@"g" modifiers:0 action:^{
[self registerDefaultSimulatorShortcutWithKey:@"g" modifiers:0 action:^{
[self showExplorerIfNeeded];
[self.explorerViewController toggleMenuTool];
} description:@"Toggle FLEX globals menu"];
[self registerSimulatorShortcutWithKey:@"v" modifiers:0 action:^{
[self registerDefaultSimulatorShortcutWithKey:@"v" modifiers:0 action:^{
[self showExplorerIfNeeded];
[self.explorerViewController toggleViewsTool];
} description:@"Toggle view hierarchy menu"];
[self registerSimulatorShortcutWithKey:@"s" modifiers:0 action:^{
[self registerDefaultSimulatorShortcutWithKey:@"s" modifiers:0 action:^{
[self showExplorerIfNeeded];
[self.explorerViewController toggleSelectTool];
} description:@"Toggle select tool"];
[self registerSimulatorShortcutWithKey:@"m" modifiers:0 action:^{
[self registerDefaultSimulatorShortcutWithKey:@"m" modifiers:0 action:^{
[self showExplorerIfNeeded];
[self.explorerViewController toggleMoveTool];
} description:@"Toggle move tool"];
[self registerSimulatorShortcutWithKey:@"n" modifiers:0 action:^{
[self registerDefaultSimulatorShortcutWithKey:@"n" modifiers:0 action:^{
[self toggleTopViewControllerOfClass:[FLEXNetworkMITMViewController class]];
} description:@"Toggle network history view"];
// 't' is for testing: quickly present an object explorer for debugging
[self registerSimulatorShortcutWithKey:@"t" modifiers:0 action:^{
[self registerDefaultSimulatorShortcutWithKey:@"t" modifiers:0 action:^{
[self showExplorerIfNeeded];
[self.explorerViewController toggleToolWithViewControllerProvider:^UINavigationController *{
@@ -121,25 +131,25 @@
} completion:nil];
} description:@"Present an object explorer for debugging"];
[self registerSimulatorShortcutWithKey:UIKeyInputDownArrow modifiers:0 action:^{
[self registerDefaultSimulatorShortcutWithKey:UIKeyInputDownArrow modifiers:0 action:^{
if (self.isHidden || ![self.explorerViewController handleDownArrowKeyPressed]) {
[self tryScrollDown];
}
} description:@"Cycle view selection\n\t\tMove view down\n\t\tScroll down"];
[self registerSimulatorShortcutWithKey:UIKeyInputUpArrow modifiers:0 action:^{
[self registerDefaultSimulatorShortcutWithKey:UIKeyInputUpArrow modifiers:0 action:^{
if (self.isHidden || ![self.explorerViewController handleUpArrowKeyPressed]) {
[self tryScrollUp];
}
} description:@"Cycle view selection\n\t\tMove view up\n\t\tScroll up"];
[self registerSimulatorShortcutWithKey:UIKeyInputRightArrow modifiers:0 action:^{
[self registerDefaultSimulatorShortcutWithKey:UIKeyInputRightArrow modifiers:0 action:^{
if (!self.isHidden) {
[self.explorerViewController handleRightArrowKeyPressed];
}
} description:@"Move selected view right"];
[self registerSimulatorShortcutWithKey:UIKeyInputLeftArrow modifiers:0 action:^{
[self registerDefaultSimulatorShortcutWithKey:UIKeyInputLeftArrow modifiers:0 action:^{
if (self.isHidden) {
[self tryGoBack];
} else {
@@ -147,16 +157,16 @@
}
} description:@"Move selected view left"];
[self registerSimulatorShortcutWithKey:@"?" modifiers:0 action:^{
[self registerDefaultSimulatorShortcutWithKey:@"?" modifiers:0 action:^{
[self toggleTopViewControllerOfClass:[FLEXKeyboardHelpViewController class]];
} description:@"Toggle (this) help menu"];
[self registerSimulatorShortcutWithKey:UIKeyInputEscape modifiers:0 action:^{
[self registerDefaultSimulatorShortcutWithKey: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]];
[self registerDefaultSimulatorShortcutWithKey:@"o" modifiers:UIKeyModifierCommand|UIKeyModifierShift action:^{
[self toggleTopViewControllerOfClass:[FLEXFileBrowserController class]];
} description:@"Toggle file browser menu"];
}
+15 -11
View File
@@ -11,21 +11,25 @@
#import "FLEXNetworkObserver.h"
#import "FLEXNetworkRecorder.h"
#import "FLEXObjectExplorerFactory.h"
#import "NSUserDefaults+FLEX.h"
@implementation FLEXManager (Networking)
+ (void)load {
// Register array/dictionary viewer for JSON responses
[self.sharedManager setCustomViewerForContentType:@"application/json"
viewControllerFutureBlock:^UIViewController *(NSData *data) {
id jsonObject = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
if (jsonObject) {
return [FLEXObjectExplorerFactory explorerViewControllerForObject:jsonObject];
}
return nil;
}
];
if (NSUserDefaults.standardUserDefaults.flex_registerDictionaryJSONViewerOnLaunch) {
dispatch_async(dispatch_get_main_queue(), ^{
// Register array/dictionary viewer for JSON responses
[self.sharedManager setCustomViewerForContentType:@"application/json"
viewControllerFutureBlock:^UIViewController *(NSData *data) {
id jsonObject = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
if (jsonObject) {
return [FLEXObjectExplorerFactory explorerViewControllerForObject:jsonObject];
}
return nil;
}
];
});
}
}
- (BOOL)isNetworkDebuggingEnabled {
+1 -1
View File
@@ -11,7 +11,7 @@
#import "FLEXExplorerViewController.h"
#import "FLEXWindow.h"
#import "FLEXObjectExplorerViewController.h"
#import "FLEXFileBrowserTableViewController.h"
#import "FLEXFileBrowserController.h"
@interface FLEXManager () <FLEXWindowEventDelegate, FLEXExplorerViewControllerDelegate>
+10 -10
View File
@@ -12,8 +12,8 @@
#import "FLEXNetworkTransaction.h"
#import "FLEXNetworkRecorder.h"
#import "FLEXNetworkObserver.h"
#import "FLEXNetworkTransactionTableViewCell.h"
#import "FLEXNetworkTransactionDetailTableViewController.h"
#import "FLEXNetworkTransactionCell.h"
#import "FLEXNetworkTransactionDetailController.h"
#import "FLEXNetworkSettingsController.h"
#import "FLEXGlobalsViewController.h"
#import "UIBarButtonItem+FLEX.h"
@@ -61,11 +61,11 @@
]];
[self.tableView
registerClass:[FLEXNetworkTransactionTableViewCell class]
registerClass:[FLEXNetworkTransactionCell class]
forCellReuseIdentifier:kFLEXNetworkTransactionCellIdentifier
];
self.tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
self.tableView.rowHeight = FLEXNetworkTransactionTableViewCell.preferredCellHeight;
self.tableView.rowHeight = FLEXNetworkTransactionCell.preferredCellHeight;
[self registerForNotifications];
[self updateTransactions];
@@ -109,7 +109,7 @@
#pragma mark Button Actions
- (void)settingsButtonTapped:(id)sender {
- (void)settingsButtonTapped:(UIBarButtonItem *)sender {
UIViewController *settings = [FLEXNetworkSettingsController new];
settings.navigationItem.rightBarButtonItem = FLEXBarButtonItemSystem(
Done, self, @selector(settingsViewControllerDoneTapped:)
@@ -121,7 +121,7 @@
[self presentViewController:nav animated:YES completion:nil];
}
- (void)trashButtonTapped:(id)sender {
- (void)trashButtonTapped:(UIBarButtonItem *)sender {
[FLEXAlert makeSheet:^(FLEXAlert *make) {
make.title(@"Clear All Recorded Requests?");
make.message(@"This cannot be undone.");
@@ -130,7 +130,7 @@
make.button(@"Clear All").destructiveStyle().handler(^(NSArray *strings) {
[FLEXNetworkRecorder.defaultRecorder clearRecordedActivity];
});
} showFrom:self];
} showFrom:self source:sender];
}
- (void)settingsViewControllerDoneTapped:(id)sender {
@@ -311,7 +311,7 @@
FLEXNetworkTransaction *transaction = notification.userInfo[kFLEXNetworkRecorderUserInfoTransactionKey];
// Update both the main table view and search table view if needed.
for (FLEXNetworkTransactionTableViewCell *cell in [self.tableView visibleCells]) {
for (FLEXNetworkTransactionCell *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 unresponsive when lots of updates are streaming in.
@@ -352,7 +352,7 @@
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
FLEXNetworkTransactionTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kFLEXNetworkTransactionCellIdentifier forIndexPath:indexPath];
FLEXNetworkTransactionCell *cell = [tableView dequeueReusableCellWithIdentifier:kFLEXNetworkTransactionCellIdentifier forIndexPath:indexPath];
cell.transaction = [self transactionAtIndexPath:indexPath];
// Since we insert from the top, assign background colors bottom up to keep them consistent for each transaction.
@@ -367,7 +367,7 @@
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
FLEXNetworkTransactionDetailTableViewController *detailViewController = [FLEXNetworkTransactionDetailTableViewController new];
FLEXNetworkTransactionDetailController *detailViewController = [FLEXNetworkTransactionDetailController new];
detailViewController.transaction = [self transactionAtIndexPath:indexPath];
[self.navigationController pushViewController:detailViewController animated:YES];
}
@@ -11,6 +11,7 @@
#import "FLEXUtility.h"
#import "FLEXTableView.h"
#import "FLEXColor.h"
#import "NSUserDefaults+FLEX.h"
@interface FLEXNetworkSettingsController () <UIActionSheetDelegate>
@property (nonatomic) float cacheLimitValue;
@@ -18,6 +19,7 @@
@property (nonatomic, readonly) UISwitch *observerSwitch;
@property (nonatomic, readonly) UISwitch *cacheMediaSwitch;
@property (nonatomic, readonly) UISwitch *jsonViewerSwitch;
@property (nonatomic, readonly) UISlider *cacheLimitSlider;
@property (nonatomic) UILabel *cacheLimitLabel;
@@ -32,8 +34,11 @@
[self disableToolbar];
self.hostBlacklist = FLEXNetworkRecorder.defaultRecorder.hostBlacklist.mutableCopy;
NSUserDefaults *defaults = NSUserDefaults.standardUserDefaults;
_observerSwitch = [UISwitch new];
_cacheMediaSwitch = [UISwitch new];
_jsonViewerSwitch = [UISwitch new];
_cacheLimitSlider = [UISlider new];
self.observerSwitch.on = FLEXNetworkObserver.enabled;
@@ -48,6 +53,12 @@
forControlEvents:UIControlEventValueChanged
];
self.jsonViewerSwitch.on = defaults.flex_registerDictionaryJSONViewerOnLaunch;
[self.jsonViewerSwitch addTarget:self
action:@selector(jsonViewerSettingToggled:)
forControlEvents:UIControlEventValueChanged
];
[self.cacheLimitSlider addTarget:self
action:@selector(cacheLimitAdjusted:)
forControlEvents:UIControlEventValueChanged
@@ -84,12 +95,16 @@
FLEXNetworkRecorder.defaultRecorder.shouldCacheMediaResponses = sender.isOn;
}
- (void)jsonViewerSettingToggled:(UISwitch *)sender {
[NSUserDefaults.standardUserDefaults toggleBoolForKey:kFLEXDefaultsRegisterJSONExplorerKey];
}
- (void)cacheLimitAdjusted:(UISlider *)sender {
self.cacheLimitValue = sender.value;
}
#pragma mark - Table View
#pragma mark - Table View Data Source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return self.hostBlacklist.count ? 2 : 1;
@@ -97,7 +112,7 @@
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
switch (section) {
case 0: return 4;
case 0: return 5;
case 1: return self.hostBlacklist.count;
default: return 0;
}
@@ -111,6 +126,17 @@
}
}
- (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section {
if (section == 0) {
return @"By default, JSON is rendered in a webview. Turn on "
"\"View JSON as a dictionary/array\" to convert JSON payloads "
"to objects and view them in an object explorer. "
"This setting requires a restart of the app.";
}
return nil;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [self.tableView
dequeueReusableCellWithIdentifier:kFLEXDefaultCell forIndexPath:indexPath
@@ -132,10 +158,14 @@
cell.accessoryView = self.cacheMediaSwitch;
break;
case 2:
cell.textLabel.text = @"View JSON as a dictionary/array";
cell.accessoryView = self.jsonViewerSwitch;
break;
case 3:
cell.textLabel.text = @"Reset Host Blacklist";
cell.textLabel.textColor = tableView.tintColor;
break;
case 3:
case 4:
cell.textLabel.text = self.cacheLimitCellTitle;
self.cacheLimitLabel = cell.textLabel;
[self.cacheLimitSlider removeFromSuperview];
@@ -179,6 +209,8 @@
return cell;
}
#pragma mark - Table View Delegate
- (BOOL)tableView:(UITableView *)tableView shouldHighlightRowAtIndexPath:(NSIndexPath *)ip {
// Can only select the "Reset Host Blacklist" row
return ip.section == 0 && ip.row == 2;
@@ -1,5 +1,5 @@
//
// FLEXNetworkTransactionTableViewCell.h
// FLEXNetworkTransactionCell.h
// Flipboard
//
// Created by Ryan Olson on 2/8/15.
@@ -8,11 +8,11 @@
#import <UIKit/UIKit.h>
extern NSString *const kFLEXNetworkTransactionCellIdentifier;
extern NSString * const kFLEXNetworkTransactionCellIdentifier;
@class FLEXNetworkTransaction;
@interface FLEXNetworkTransactionTableViewCell : UITableViewCell
@interface FLEXNetworkTransactionCell : UITableViewCell
@property (nonatomic) FLEXNetworkTransaction *transaction;
@@ -1,5 +1,5 @@
//
// FLEXNetworkTransactionTableViewCell.m
// FLEXNetworkTransactionCell.m
// Flipboard
//
// Created by Ryan Olson on 2/8/15.
@@ -7,14 +7,14 @@
//
#import "FLEXColor.h"
#import "FLEXNetworkTransactionTableViewCell.h"
#import "FLEXNetworkTransactionCell.h"
#import "FLEXNetworkTransaction.h"
#import "FLEXUtility.h"
#import "FLEXResources.h"
NSString *const kFLEXNetworkTransactionCellIdentifier = @"kFLEXNetworkTransactionCellIdentifier";
@interface FLEXNetworkTransactionTableViewCell ()
@interface FLEXNetworkTransactionCell ()
@property (nonatomic) UIImageView *thumbnailImageView;
@property (nonatomic) UILabel *nameLabel;
@@ -23,7 +23,7 @@ NSString *const kFLEXNetworkTransactionCellIdentifier = @"kFLEXNetworkTransactio
@end
@implementation FLEXNetworkTransactionTableViewCell
@implementation FLEXNetworkTransactionCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
@@ -1,5 +1,5 @@
//
// FLEXNetworkTransactionDetailTableViewController.h
// FLEXNetworkTransactionDetailController.h
// Flipboard
//
// Created by Ryan Olson on 2/10/15.
@@ -10,7 +10,7 @@
@class FLEXNetworkTransaction;
@interface FLEXNetworkTransactionDetailTableViewController : UITableViewController
@interface FLEXNetworkTransactionDetailController : UITableViewController
@property (nonatomic) FLEXNetworkTransaction *transaction;
@@ -1,5 +1,5 @@
//
// FLEXNetworkTransactionDetailTableViewController.m
// FLEXNetworkTransactionDetailController.m
// Flipboard
//
// Created by Ryan Olson on 2/10/15.
@@ -7,7 +7,7 @@
//
#import "FLEXColor.h"
#import "FLEXNetworkTransactionDetailTableViewController.h"
#import "FLEXNetworkTransactionDetailController.h"
#import "FLEXNetworkCurlLogger.h"
#import "FLEXNetworkRecorder.h"
#import "FLEXNetworkTransaction.h"
@@ -44,13 +44,13 @@ typedef UIViewController *(^FLEXNetworkDetailRowSelectionFuture)(void);
@end
@interface FLEXNetworkTransactionDetailTableViewController ()
@interface FLEXNetworkTransactionDetailController ()
@property (nonatomic, copy) NSArray<FLEXNetworkDetailSection *> *sections;
@end
@implementation FLEXNetworkTransactionDetailTableViewController
@implementation FLEXNetworkTransactionDetailController
- (instancetype)initWithStyle:(UITableViewStyle)style {
// Force grouped style
@@ -15,7 +15,7 @@
#import "FLEXNetworkObserver.h"
#import "FLEXNetworkRecorder.h"
#import "FLEXUtility.h"
#import "NSObject+Reflection.h"
#import "NSObject+FLEX_Reflection.h"
#import "FLEXMethod.h"
#import <objc/runtime.h>
@@ -278,17 +278,18 @@ didBecomeDownloadTask:(NSURLSessionDownloadTask *)downloadTask delegate:(id<NSUR
// In iOS 7 resume lives in __NSCFLocalSessionTask
// In iOS 8 resume lives in NSURLSessionTask
// In iOS 9 resume lives in __NSCFURLSessionTask
// In iOS 14 resume lives in NSURLSessionTask
Class baseResumeClass = Nil;
if (![NSProcessInfo.processInfo respondsToSelector:@selector(operatingSystemVersion)]) {
// iOS ... 7
baseResumeClass = NSClassFromString(@"__NSCFLocalSessionTask");
} else {
NSInteger majorVersion = NSProcessInfo.processInfo.operatingSystemVersion.majorVersion;
if (majorVersion < 9) {
// iOS 8
if (majorVersion < 9 || majorVersion >= 14) {
// iOS 8 or iOS 14+
baseResumeClass = [NSURLSessionTask class];
} else {
// iOS 9+
// iOS 9 ... 13
baseResumeClass = NSClassFromString(@"__NSCFURLSessionTask");
}
}
@@ -8,10 +8,22 @@
#import "FLEXRuntime+UIKitHelpers.h"
/// Carries state about the current user defaults settings
@interface FLEXObjectExplorerDefaults : NSObject
+ (instancetype)canEdit:(BOOL)editable wantsPreviews:(BOOL)showPreviews;
/// Only \c YES for properties and ivars
@property (nonatomic, readonly) BOOL isEditable;
/// Only affects properties and ivars
@property (nonatomic, readonly) BOOL wantsDynamicPreviews;
@end
@interface FLEXObjectExplorer : NSObject
+ (instancetype)forObject:(id)objectOrClass;
+ (void)configureDefaultsForItems:(NSArray<id<FLEXObjectExplorerItem>> *)items;
@property (nonatomic, readonly) id object;
/// Subclasses can override to provide a more useful description
@property (nonatomic, readonly) NSString *objectDescription;
+36 -6
View File
@@ -9,14 +9,24 @@
#import "FLEXObjectExplorer.h"
#import "FLEXUtility.h"
#import "FLEXRuntimeUtility.h"
#import "NSObject+Reflection.h"
#import "NSObject+FLEX_Reflection.h"
#import "FLEXRuntime+Compare.h"
#import "FLEXRuntime+UIKitHelpers.h"
#import "FLEXPropertyAttributes.h"
#import "NSObject+Reflection.h"
#import "FLEXMetadataSection.h"
#import "NSUserDefaults+FLEX.h"
@implementation FLEXObjectExplorerDefaults
+ (instancetype)canEdit:(BOOL)editable wantsPreviews:(BOOL)showPreviews {
FLEXObjectExplorerDefaults *defaults = [self new];
defaults->_isEditable = editable;
defaults->_wantsDynamicPreviews = showPreviews;
return defaults;
}
@end
@interface FLEXObjectExplorer () {
NSMutableArray<NSArray<FLEXProperty *> *> *_allProperties;
NSMutableArray<NSArray<FLEXProperty *> *> *_allClassProperties;
@@ -55,6 +65,24 @@
#pragma mark - Public
+ (void)configureDefaultsForItems:(NSArray<id<FLEXObjectExplorerItem>> *)items {
BOOL hidePreviews = NSUserDefaults.standardUserDefaults.flex_explorerHidesVariablePreviews;
FLEXObjectExplorerDefaults *mutable = [FLEXObjectExplorerDefaults
canEdit:YES wantsPreviews:!hidePreviews
];
FLEXObjectExplorerDefaults *immutable = [FLEXObjectExplorerDefaults
canEdit:NO wantsPreviews:!hidePreviews
];
// .tag is used to cache whether the value of .isEditable;
// This could change at runtime so it is important that
// it is cached every time shortcuts are requeted and not
// just once at as shortcuts are initially registered
for (id<FLEXObjectExplorerItem> metadata in items) {
metadata.defaults = metadata.isEditable ? mutable : immutable;
}
}
- (NSString *)objectDescription {
if (!_objectDescription) {
// Hard-code UIColor description
@@ -222,11 +250,13 @@
// Set up UIKit helper data
// Really, we only need to call this on properties and ivars
// because no other metadata types support editing.
for (NSArray *matrix in @[_allProperties, _allIvars, /* _allMethods, _allClassMethods, _allConformedProtocols */]) {
NSArray<NSArray *>*metadatas = @[
_allProperties, _allClassProperties, _allIvars,
/* _allMethods, _allClassMethods, _allConformedProtocols */
];
for (NSArray *matrix in metadatas) {
for (NSArray *metadataByClass in matrix) {
for (id<FLEXRuntimeMetadata> metadata in metadataByClass) {
metadata.tag = metadata.isEditable ? @YES : nil;
}
[FLEXObjectExplorer configureDefaultsForItems:metadataByClass];
}
}
@@ -11,6 +11,7 @@
#import "FLEXClassShortcuts.h"
#import "FLEXViewShortcuts.h"
#import "FLEXViewControllerShortcuts.h"
#import "FLEXUIAppShortcuts.h"
#import "FLEXImageShortcuts.h"
#import "FLEXLayerShortcuts.h"
#import "FLEXColorPreviewSection.h"
@@ -35,6 +36,7 @@ static NSMutableDictionary<Class, Class> *classesToRegisteredSections = nil;
ClassKey(NSOrderedSet) : [FLEXCollectionContentSection class],
ClassKey(NSUserDefaults) : [FLEXDefaultsContentSection class],
ClassKey(UIViewController) : [FLEXViewControllerShortcuts class],
ClassKey(UIApplication) : [FLEXUIAppShortcuts class],
ClassKey(UIView) : [FLEXViewShortcuts class],
ClassKey(UIImage) : [FLEXImageShortcuts class],
ClassKey(CALayer) : [FLEXLayerShortcuts class],
@@ -73,6 +73,7 @@
kFLEXDefaultsHidePropertyIvarsKey,
kFLEXDefaultsHidePropertyMethodsKey,
kFLEXDefaultsHideMethodOverridesKey,
kFLEXDefaultsHideVariablePreviewsKey,
];
}
@@ -101,7 +102,7 @@
// ... button for extra options
[self addToolbarItems:@[[UIBarButtonItem
itemWithImage:FLEXResources.moreIcon target:self action:@selector(moreButtonPressed)
itemWithImage:FLEXResources.moreIcon target:self action:@selector(moreButtonPressed:)
]]];
// Swipe gestures to swipe between classes in the hierarchy
@@ -122,12 +123,7 @@
//
// "If your app targets iOS 9.0 and later or macOS 10.11 and later,
// you don't need to unregister an observer in its dealloc method."
NSArray<NSString *> *observedNotifications = @[
kFLEXDefaultsHidePropertyIvarsKey,
kFLEXDefaultsHidePropertyMethodsKey,
kFLEXDefaultsHideMethodOverridesKey,
];
for (NSString *pref in observedNotifications) {
for (NSString *pref in self.observedNotifications) {
[NSNotificationCenter.defaultCenter
addObserver:self
selector:@selector(fullyReloadData)
@@ -221,7 +217,7 @@
[super reloadData];
}
- (void)shareButtonPressed {
- (void)shareButtonPressed:(UIBarButtonItem *)sender {
[FLEXAlert makeSheet:^(FLEXAlert *make) {
make.button(@"Add to Bookmarks").handler(^(NSArray<NSString *> *strings) {
[FLEXBookmarkManager.bookmarks addObject:self.object];
@@ -233,7 +229,7 @@
UIPasteboard.generalPasteboard.string = [FLEXUtility addressOfObject:self.object];
});
make.button(@"Cancel").cancelStyle();
} showFrom:self];
} showFrom:self source:sender];
}
@@ -289,13 +285,14 @@
return YES;
}
- (void)moreButtonPressed {
- (void)moreButtonPressed:(UIBarButtonItem *)sender {
NSUserDefaults *defaults = NSUserDefaults.standardUserDefaults;
// Maps preference keys to a description of what they affect
NSDictionary<NSString *, NSString *> *explorerToggles = @{
kFLEXDefaultsHidePropertyIvarsKey: @"Property-Backing Ivars",
kFLEXDefaultsHidePropertyMethodsKey: @"Property-Backing Methods",
kFLEXDefaultsHideMethodOverridesKey: @"Method Overrides",
kFLEXDefaultsHidePropertyIvarsKey: @"Property-Backing Ivars",
kFLEXDefaultsHidePropertyMethodsKey: @"Property-Backing Methods",
kFLEXDefaultsHideMethodOverridesKey: @"Method Overrides",
kFLEXDefaultsHideVariablePreviewsKey: @"Variable Previews"
};
// Maps the key of the action itself to a map of a description
@@ -303,9 +300,10 @@
//
// So keys that are hidden by default have NO mapped to "Show"
NSDictionary<NSString *, NSDictionary *> *nextStateDescriptions = @{
kFLEXDefaultsHidePropertyIvarsKey: @{ @NO: @"Hide ", @YES: @"Show " },
kFLEXDefaultsHidePropertyMethodsKey: @{ @NO: @"Hide ", @YES: @"Show " },
kFLEXDefaultsHideMethodOverridesKey: @{ @NO: @"Show ", @YES: @"Hide " },
kFLEXDefaultsHidePropertyIvarsKey: @{ @NO: @"Hide ", @YES: @"Show " },
kFLEXDefaultsHidePropertyMethodsKey: @{ @NO: @"Hide ", @YES: @"Show " },
kFLEXDefaultsHideMethodOverridesKey: @{ @NO: @"Show ", @YES: @"Hide " },
kFLEXDefaultsHideVariablePreviewsKey: @{ @NO: @"Hide ", @YES: @"Show " },
};
[FLEXAlert makeSheet:^(FLEXAlert *make) {
@@ -323,7 +321,7 @@
}
make.button(@"Cancel").cancelStyle();
} showFrom:self];
} showFrom:self source:sender];
}
#pragma mark - Description
@@ -77,7 +77,7 @@ typedef id<FLEXCollection>(^FLEXCollectionContentFuture)(__kindof FLEXCollection
/// Defaults to \c NO
@property (nonatomic) BOOL hideSectionTitle;
/// Defaults to \c nil
@property (nonatomic) NSString *customTitle;
@property (nonatomic, copy) NSString *customTitle;
/// Defaults to \c NO
///
/// Settings this to \c NO will not display the element index for ordered collections.
@@ -13,7 +13,7 @@
#import "FLEXFieldEditorViewController.h"
#import "FLEXMethodCallingViewController.h"
#import "FLEXIvar.h"
#import "NSArray+Functional.h"
#import "NSArray+FLEX.h"
#import "FLEXRuntime+UIKitHelpers.h"
@interface FLEXMetadataSection ()
@@ -36,7 +36,7 @@ typedef void (^FLEXMutableListCellForElement)(__kindof UITableViewCell *cell, id
/// By default, rows are not selectable. If you want rows
/// to be selectable, provide a selection handler here.
@property (nonatomic) void (^selectionHandler)(__kindof UIViewController * host, id element);
@property (nonatomic, copy) void (^selectionHandler)(__kindof UIViewController *host, id element);
/// The objects representing all possible rows in the section.
@property (nonatomic) NSArray<ObjectType> *list;
@@ -29,12 +29,12 @@ configurationBlock:(FLEXMutableListCellForElement)cellConfig
self = [super init];
if (self) {
_configureCell = cellConfig;
self.list = list.mutableCopy;
self.customFilter = filterBlock;
self.hideSectionTitle = YES;
}
return self;
}
@@ -48,7 +48,7 @@ configurationBlock:(FLEXMutableListCellForElement)cellConfig
- (void)setList:(NSMutableArray *)list {
NSParameterAssert(list);
_collection = list;
[self reloadData];
}
@@ -79,11 +79,15 @@ configurationBlock:(FLEXMutableListCellForElement)cellConfig
- (void (^)(__kindof UIViewController *))didSelectRowAction:(NSInteger)row {
if (self.selectionHandler) {
__weak __typeof(self) weakSelf = self;
return ^(UIViewController *host) {
self.selectionHandler(host, self.filteredList[row]);
__strong __typeof(self) strongSelf = weakSelf;
if (strongSelf) {
strongSelf.selectionHandler(host, strongSelf.filteredList[row]);
}
};
}
return nil;
}
@@ -95,7 +99,7 @@ configurationBlock:(FLEXMutableListCellForElement)cellConfig
if (self.cellRegistrationMapping.count) {
return self.cellRegistrationMapping.allKeys.firstObject;
}
return [super reuseIdentifierForRow:row];
}
@@ -8,23 +8,110 @@
#import "FLEXBundleShortcuts.h"
#import "FLEXShortcut.h"
#import "FLEXFileBrowserTableViewController.h"
#import "FLEXAlert.h"
#import "FLEXRuntimeExporter.h"
#import "FLEXTableListViewController.h"
#import "FLEXFileBrowserController.h"
#pragma mark -
@implementation FLEXBundleShortcuts
#pragma mark Overrides
+ (instancetype)forObject:(NSBundle *)bundle {
__weak __typeof(self) weakSelf = self;
return [self forObject:bundle additionalRows:@[
[FLEXActionShortcut title:@"Browse Bundle Directory" subtitle:nil
viewer:^UIViewController *(id view) {
return [FLEXFileBrowserTableViewController path:bundle.bundlePath];
[FLEXActionShortcut
title:@"Browse Bundle Directory" subtitle:nil
viewer:^UIViewController *(NSBundle *bundle) {
return [FLEXFileBrowserController path:bundle.bundlePath];
}
accessoryType:^UITableViewCellAccessoryType(id view) {
accessoryType:^UITableViewCellAccessoryType(NSBundle *bundle) {
return UITableViewCellAccessoryDisclosureIndicator;
}
]
],
[FLEXActionShortcut title:@"Browse Bundle as Database…" subtitle:nil
selectionHandler:^(UIViewController *host, NSBundle *bundle) {
__strong __typeof(self) strongSelf = weakSelf;
if (strongSelf) {
[strongSelf promptToExportBundleAsDatabase:bundle host:host];
}
}
accessoryType:^UITableViewCellAccessoryType(NSBundle *bundle) {
return UITableViewCellAccessoryDisclosureIndicator;
}
],
]];
}
+ (void)promptToExportBundleAsDatabase:(NSBundle *)bundle host:(UIViewController *)host {
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(@"Save As…").message(
@"The database be saved in the Library folder. "
"Depending on the number of classes, it may take "
"10 minutes or more to finish exporting. 20,000 "
"classes takes about 7 minutes."
);
make.configuredTextField(^(UITextField *field) {
field.placeholder = @"FLEXRuntimeExport.objc.db";
field.text = [NSString stringWithFormat:
@"%@.objc.db", bundle.executablePath.lastPathComponent
];
});
make.button(@"Start").handler(^(NSArray<NSString *> *strings) {
[self browseBundleAsDatabase:bundle host:host name:strings[0]];
});
make.button(@"Cancel").cancelStyle();
} showFrom:host];
}
+ (void)browseBundleAsDatabase:(NSBundle *)bundle host:(UIViewController *)host name:(NSString *)name {
NSParameterAssert(name.length);
UIAlertController *progress = [FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(@"Generating Database");
// Some iOS version glitch out of there is
// no initial message and you add one later
make.message(@"");
}];
[host presentViewController:progress animated:YES completion:^{
// Generate path to store db
NSString *path = [NSSearchPathForDirectoriesInDomains(
NSLibraryDirectory, NSUserDomainMask, YES
)[0] stringByAppendingPathComponent:name];
progress.message = [path stringByAppendingString:@"\n\nCreating database…"];
// Generate db and show progress
[FLEXRuntimeExporter createRuntimeDatabaseAtPath:path
forImages:@[bundle.executablePath]
progressHandler:^(NSString *status) {
dispatch_async(dispatch_get_main_queue(), ^{
progress.message = [progress.message
stringByAppendingFormat:@"\n%@", status
];
[progress.view setNeedsLayout];
[progress.view layoutIfNeeded];
});
} completion:^(NSString *error) {
// Display error if any
if (error) {
progress.title = @"Error";
progress.message = error;
[progress addAction:[UIAlertAction
actionWithTitle:@"OK" style:UIAlertActionStyleCancel handler:nil]
];
}
// Browse database
else {
[progress dismissViewControllerAnimated:YES completion:nil];
[host.navigationController pushViewController:[
[FLEXTableListViewController alloc] initWithPath:path
] animated:YES];
}
}
];
}];
}
@end
@@ -10,7 +10,7 @@
#import "FLEXShortcut.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXObjectListViewController.h"
#import "NSObject+Reflection.h"
#import "NSObject+FLEX_Reflection.h"
@interface FLEXClassShortcuts ()
@property (nonatomic, readonly) Class cls;
@@ -8,57 +8,60 @@
#import "FLEXImageShortcuts.h"
#import "FLEXImagePreviewViewController.h"
#import "FLEXShortcut.h"
#import "FLEXAlert.h"
@interface FLEXImageShortcuts ()
@property (nonatomic, readonly) UIImage *image;
@interface UIAlertController (FLEXImageShortcuts)
- (void)flex_image:(UIImage *)image disSaveWithError:(NSError *)error :(void *)context;
@end
@implementation FLEXImageShortcuts
#pragma mark - Internal
- (UIImage *)image {
return self.object;
}
#pragma mark - Overrides
+ (instancetype)forObject:(UIImage *)image {
// These additional rows will appear at the beginning of the shortcuts section.
// The methods below are written in such a way that they will not interfere
// with properties/etc being registered alongside these
return [self forObject:image additionalRows:@[@"View Image", @"Save Image"]];
}
/// View image
- (UIViewController *)viewControllerToPushForRow:(NSInteger)row {
if (row == 0) {
return [FLEXImagePreviewViewController forImage:self.image];
}
return [super viewControllerToPushForRow:row];
}
/// Save image
- (void (^)(__kindof UIViewController *))didSelectRowAction:(NSInteger)row {
if (row == 1) {
return ^(UIViewController *host) {
UIImageWriteToSavedPhotosAlbum(self.image, nil, nil, nil);
};
}
return [super didSelectRowAction:row];
}
/// "Save Image" does not need a disclosure indicator
- (UITableViewCellAccessoryType)accessoryTypeForRow:(NSInteger)row {
switch (row) {
case 0: return UITableViewCellAccessoryDisclosureIndicator;
case 1: return UITableViewCellAccessoryNone;
default: return [super accessoryTypeForRow:row];
}
return [self forObject:image additionalRows:@[
[FLEXActionShortcut title:@"View Image" subtitle:nil
viewer:^UIViewController *(id image) {
return [FLEXImagePreviewViewController forImage:image];
}
accessoryType:^UITableViewCellAccessoryType(id image) {
return UITableViewCellAccessoryDisclosureIndicator;
}
],
[FLEXActionShortcut title:@"Save Image" subtitle:nil
selectionHandler:^(UIViewController *host, id image) {
// Present modal alerting user about saving
UIAlertController *alert = [FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(@"Saving Image…");
}];
[host presentViewController:alert animated:YES completion:nil];
// Save the image
UIImageWriteToSavedPhotosAlbum(
image, alert, @selector(flex_image:disSaveWithError::), nil
);
}
accessoryType:^UITableViewCellAccessoryType(id image) {
return UITableViewCellAccessoryDisclosureIndicator;
}
]
]];
}
@end
@implementation UIAlertController (FLEXImageShortcuts)
- (void)flex_image:(UIImage *)image disSaveWithError:(NSError *)error :(void *)context {
self.title = @"Image Saved";
flex_dispatch_after(1, dispatch_get_main_queue(), ^{
[self dismissViewControllerAnimated:YES completion:nil];
});
}
@end
@@ -6,7 +6,7 @@
// Copyright © 2019 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "FLEXObjectExplorer.h"
NS_ASSUME_NONNULL_BEGIN
@@ -18,7 +18,7 @@ NS_ASSUME_NONNULL_BEGIN
///
/// It is useful to make your own shortcuts to append/prepend
/// them to the existing list of shortcuts for a class.
@protocol FLEXShortcut <NSObject>
@protocol FLEXShortcut <FLEXObjectExplorerItem>
- (nonnull NSString *)titleWith:(id)object;
- (nullable NSString *)subtitleWith:(id)object;
@@ -33,6 +33,7 @@
@end
@implementation FLEXShortcut
@synthesize defaults = _defaults;
+ (id<FLEXShortcut>)shortcutFor:(id)item {
if ([item conformsToProtocol:@protocol(FLEXShortcut)]) {
@@ -85,7 +86,7 @@
- (NSString *)subtitleWith:(id)object {
if (self.metadataKind) {
return [self.metadata previewWithTarget:object] ?: @"nil";
return [self.metadata previewWithTarget:object];
}
// Item is probably a string; must return empty string since
@@ -124,6 +125,32 @@
return kFLEXMultilineCell;
}
#pragma mark FLEXObjectExplorerDefaults
- (void)setDefaults:(FLEXObjectExplorerDefaults *)defaults {
_defaults = defaults;
if (_metadataKind) {
self.metadata.defaults = defaults;
}
}
- (BOOL)isEditable {
if (_metadataKind) {
return self.metadata.isEditable;
}
return NO;
}
- (BOOL)isCallable {
if (_metadataKind) {
return self.metadata.isCallable;
}
return NO;
}
#pragma mark - Helpers
- (FLEXProperty *)property { return _item; }
@@ -145,6 +172,7 @@
@end
@implementation FLEXActionShortcut
@synthesize defaults = _defaults;
+ (instancetype)title:(NSString *)title
subtitle:(NSString *(^)(id))subtitle
@@ -186,7 +214,11 @@
}
- (NSString *)subtitleWith:(id)object {
return self.subtitleFuture(object);
if (self.defaults.wantsDynamicPreviews) {
return self.subtitleFuture(object);
}
return nil;
}
- (void (^)(UIViewController *))didSelectActionWith:(id)object {
@@ -216,4 +248,7 @@
return nil;
}
- (BOOL)isEditable { return NO; }
- (BOOL)isCallable { return NO; }
@end
@@ -8,6 +8,8 @@
#import "FLEXShortcutsSection.h"
@interface FLEXShortcutsFactory (UIApplication) @end
@interface FLEXShortcutsFactory (Views) @end
@interface FLEXShortcutsFactory (ViewControllers) @end
@@ -9,13 +9,38 @@
#import "FLEXShortcutsFactory+Defaults.h"
#import "FLEXShortcut.h"
#import "FLEXRuntimeUtility.h"
#import "NSObject+Reflection.h"
#import "NSObject+FLEX_Reflection.h"
#pragma mark - UIApplication
@implementation FLEXShortcutsFactory (UIApplication)
+ (void)load { FLEX_EXIT_IF_NO_CTORS()
// sharedApplication class property possibly not added
// as a literal class property until iOS 10
FLEXRuntimeUtilityTryAddObjectProperty(
2, sharedApplication, UIApplication.flex_metaclass, UIApplication, PropertyKey(ReadOnly)
);
self.append.classProperties(@[@"sharedApplication"]).forClass(UIApplication.flex_metaclass);
self.append.properties(@[
@"delegate", @"keyWindow", @"windows"
]).forClass(UIApplication.class);
if (@available(iOS 13, *)) {
self.append.properties(@[
@"connectedScenes", @"openSessions", @"supportsMultipleScenes"
]).forClass(UIApplication.class);
}
}
@end
#pragma mark - Views
@implementation FLEXShortcutsFactory (Views)
+ (void)load { FLEX_EXIT_IF_TESTING()
+ (void)load { FLEX_EXIT_IF_NO_CTORS()
// A quirk of UIView and some other classes: a lot of the `@property`s are
// not actually properties from the perspective of the runtime.
//
@@ -90,6 +115,13 @@
@"currentTitle", @"currentImage", @"enabled", @"frame",
@"superview", @"subviews"
]).forClass(UIButton.class);
// UIImageView
self.append.properties(@[
@"image", @"animationImages", @"frame", @"bounds", @"center",
@"transform", @"alpha", @"hidden", @"clipsToBounds",
@"userInteractionEnabled", @"layer", @"superview", @"subviews",
]).forClass(UIImageView.class);
}
@end
@@ -99,7 +131,7 @@
@implementation FLEXShortcutsFactory (ViewControllers)
+ (void)load { FLEX_EXIT_IF_TESTING()
+ (void)load { FLEX_EXIT_IF_NO_CTORS()
// toolbarItems is not really a property, make it one
FLEXRuntimeUtilityTryAddObjectProperty(3, toolbarItems, UIViewController.class, NSArray);
@@ -119,7 +151,7 @@
@implementation FLEXShortcutsFactory (UIImage)
+ (void)load { FLEX_EXIT_IF_TESTING()
+ (void)load { FLEX_EXIT_IF_NO_CTORS()
self.append.methods(@[
@"CGImage", @"CIImage"
]).properties(@[
@@ -128,7 +160,7 @@
]).forClass(UIImage.class);
if (@available(iOS 13, *)) {
self.append.properties(@[@"symbolImage"]);
self.append.properties(@[@"symbolImage"]).forClass(UIImage.class);
}
}
@@ -139,7 +171,7 @@
@implementation FLEXShortcutsFactory (NSBundle)
+ (void)load { FLEX_EXIT_IF_TESTING()
+ (void)load { FLEX_EXIT_IF_NO_CTORS()
self.append.properties(@[
@"bundleIdentifier", @"principalClass",
@"infoDictionary", @"bundlePath",
@@ -154,7 +186,7 @@
@implementation FLEXShortcutsFactory (Classes)
+ (void)load { FLEX_EXIT_IF_TESTING()
+ (void)load { FLEX_EXIT_IF_NO_CTORS()
self.append.classMethods(@[@"new", @"alloc"]).forClass(NSObject.flex_metaclass);
}
@@ -165,7 +197,7 @@
@implementation FLEXShortcutsFactory (Activities)
+ (void)load { FLEX_EXIT_IF_TESTING()
+ (void)load { FLEX_EXIT_IF_NO_CTORS()
// Property was added in iOS 10 but we want it on iOS 9 too
FLEXRuntimeUtilityTryAddNonatomicProperty(9, item, UIActivityItemProvider.class, id, PropertyKey(ReadOnly));
@@ -185,7 +217,7 @@
@implementation FLEXShortcutsFactory (Blocks)
+ (void)load { FLEX_EXIT_IF_TESTING()
+ (void)load { FLEX_EXIT_IF_NO_CTORS()
self.append.methods(@[@"invoke"]).forClass(NSClassFromString(@"NSBlock"));
}
@@ -195,7 +227,7 @@
@implementation FLEXShortcutsFactory (Foundation)
+ (void)load { FLEX_EXIT_IF_TESTING()
+ (void)load { FLEX_EXIT_IF_NO_CTORS()
self.append.properties(@[
@"configuration", @"delegate", @"delegateQueue", @"sessionDescription",
]).methods(@[
@@ -230,24 +262,6 @@
self.append.classProperties(@[
@"defaultTimeZone", @"systemTimeZone", @"localTimeZone"
]).forClass(NSTimeZone.class);
// self.append.<#type#>(@[@"<#value#>"]).forClass(NSURLSession.class);
//
//
// self.append.<#type#>(@[@"<#value#>"]).forClass(NSURLSession.class);
//
//
// self.append.<#type#>(@[@"<#value#>"]).forClass(NSURLSession.class);
//
//
// self.append.<#type#>(@[@"<#value#>"]).forClass(NSURLSession.class);
//
//
// self.append.<#type#>(@[@"<#value#>"]).forClass(NSURLSession.class);
//
//
// self.append.<#type#>(@[@"<#value#>"]).forClass(NSURLSession.class);
}
@end
@@ -59,6 +59,8 @@
/// Subclasses \e may override this to hide the disclosure indicator
/// for some rows. It is shown for all rows by default, unless
/// you initialize it with \c forObject:rowTitles:rowSubtitles:
///
/// When you hide the disclosure indicator, the row is not selectable.
- (UITableViewCellAccessoryType)accessoryTypeForRow:(NSInteger)row;
/// The number of lines for the title and subtitle labels. Defaults to 1.
@@ -16,6 +16,7 @@
#import "FLEXIvar.h"
#import "FLEXMethod.h"
#import "FLEXRuntime+UIKitHelpers.h"
#import "FLEXObjectExplorer.h"
#pragma mark Private
@@ -86,6 +87,7 @@
return [FLEXShortcut shortcutFor:obj];
}];
_numberOfLines = 1;
// Populate titles and subtitles
[self reloadData];
}
@@ -156,6 +158,8 @@
}
- (void)reloadData {
[FLEXObjectExplorer configureDefaultsForItems:self.allShortcuts];
// Generate all (sub)titles from shortcuts
if (self.allShortcuts) {
self.allTitles = [self.allShortcuts flex_mapped:^id(FLEXShortcut *s, NSUInteger idx) {
@@ -303,15 +307,8 @@ static RegistrationBuckets *mMethods = nil;
classKey = class_getSuperclass(classKey);
}
}
// .tag is used to cache whether the value of .isEditable;
// This could change at runtime so it is important that
// it is cached every time shortcuts are requeted and not
// just once at as shortcuts are initially registered
for (id<FLEXRuntimeMetadata> metadata in shortcuts) {
metadata.tag = metadata.isEditable ? @YES : nil;
}
[FLEXObjectExplorer configureDefaultsForItems:shortcuts];
return shortcuts;
}
@@ -0,0 +1,13 @@
//
// FLEXUIAppShortcuts.h
// FLEX
//
// Created by Tanner on 5/25/20.
// Copyright © 2020 Flipboard. All rights reserved.
//
#import "FLEXShortcutsSection.h"
@interface FLEXUIAppShortcuts : FLEXShortcutsSection
@end
@@ -0,0 +1,77 @@
//
// FLEXUIAppShortcuts.m
// FLEX
//
// Created by Tanner on 5/25/20.
// Copyright © 2020 Flipboard. All rights reserved.
//
#import "FLEXUIAppShortcuts.h"
#import "FLEXRuntimeUtility.h"
#import "FLEXShortcut.h"
#import "FLEXAlert.h"
@implementation FLEXUIAppShortcuts
#pragma mark - Overrides
+ (instancetype)forObject:(UIApplication *)application {
return [self forObject:application additionalRows:@[
[FLEXActionShortcut title:@"Open URL…"
subtitle:^NSString *(UIViewController *controller) {
return nil;
}
selectionHandler:^void(UIViewController *host, UIApplication *app) {
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(@"Open URL");
make.message(
@"This will call openURL: or openURL:options:completion: "
"with the string below. 'Open if Universal' will only open "
"the URL if it is a registered Universal Link."
);
make.textField(@"twitter://user?id=12345");
make.button(@"Open").handler(^(NSArray<NSString *> *strings) {
[self openURL:strings[0] inApp:app onlyIfUniveral:NO host:host];
});
make.button(@"Open if Universal").handler(^(NSArray<NSString *> *strings) {
[self openURL:strings[0] inApp:app onlyIfUniveral:YES host:host];
});
make.button(@"Cancel").cancelStyle();
} showFrom:host];
}
accessoryType:^UITableViewCellAccessoryType(UIViewController *controller) {
return UITableViewCellAccessoryDisclosureIndicator;
}
]
]];
}
+ (void)openURL:(NSString *)urlString
inApp:(UIApplication *)app
onlyIfUniveral:(BOOL)universalOnly
host:(UIViewController *)host {
NSURL *url = [NSURL URLWithString:urlString];
if (url) {
if (@available(iOS 10, *)) {
[app openURL:url options:@{
UIApplicationOpenURLOptionUniversalLinksOnly: @(universalOnly)
} completionHandler:^(BOOL success) {
if (!success) {
[FLEXAlert showAlert:@"No Universal Link Handler"
message:@"No installed application is registered to handle this link."
from:host
];
}
}];
} else {
[app openURL:url];
}
} else {
[FLEXAlert showAlert:@"Error" message:@"Invalid URL" from:host];
}
}
@end
@@ -13,34 +13,15 @@
#import "FLEXAlert.h"
@interface FLEXViewControllerShortcuts ()
@property (nonatomic, readonly) UIViewController *viewController;
@property (nonatomic, readonly) BOOL viewControllerIsInUse;
@end
@implementation FLEXViewControllerShortcuts
#pragma mark - Internal
- (UIViewController *)viewController {
return self.object;
}
/// A view controller is "in use" if it's view is in a window,
/// or if it belongs to a navigation stack which is in use.
- (BOOL)viewControllerIsInUse {
if (self.viewController.view.window) {
return YES;
}
return self.viewController.navigationController != nil;
}
#pragma mark - Overrides
+ (instancetype)forObject:(UIViewController *)viewController {
BOOL (^vcIsInuse)(UIViewController *) = ^BOOL(UIViewController *controller) {
if (controller.view.window) {
if (controller.viewIfLoaded.window) {
return YES;
}
@@ -12,15 +12,25 @@
#import "FLEXMethod.h"
#import "FLEXProtocol.h"
@protocol FLEXRuntimeMetadata <NSObject>
/// Used as the main title of the row
- (NSString *)description;
/// Used to compare metadata objects for uniqueness
@property (nonatomic, readonly) NSString *name;
@class FLEXObjectExplorerDefaults;
/// Model objects of an object explorer screen adopt this
/// protocol in order respond to user defaults changes
@protocol FLEXObjectExplorerItem <NSObject>
/// Current explorer settings. Set when settings change.
@property (nonatomic) FLEXObjectExplorerDefaults *defaults;
/// YES for properties and ivars which surely support editing, NO for all methods.
@property (nonatomic, readonly) BOOL isEditable;
/// NO for ivars, YES for supported methods and properties
@property (nonatomic, readonly) BOOL isCallable;
@end
@protocol FLEXRuntimeMetadata <FLEXObjectExplorerItem>
/// Used as the main title of the row
- (NSString *)description;
/// Used to compare metadata objects for uniqueness
@property (nonatomic, readonly) NSString *name;
/// For internal use
@property (nonatomic) id tag;
@@ -76,3 +86,7 @@ typedef NS_ENUM(NSUInteger, FLEXStaticMetadataRowStyle) {
+ (NSArray<FLEXStaticMetadata *> *)classHierarchy:(NSArray<Class> *)classes;
@end
/// This is assigned to the \c tag property of each metadata.
@@ -15,11 +15,21 @@
#import "FLEXMethodCallingViewController.h"
#import "FLEXTableView.h"
#import "FLEXUtility.h"
#import "NSArray+Functional.h"
#import "NSArray+FLEX.h"
#import "NSString+FLEX.h"
#define FLEXObjectExplorerDefaultsImpl \
- (FLEXObjectExplorerDefaults *)defaults { \
return self.tag; \
} \
\
- (void)setDefaults:(FLEXObjectExplorerDefaults *)defaults { \
self.tag = defaults; \
}
#pragma mark FLEXProperty
@implementation FLEXProperty (UIKitHelpers)
FLEXObjectExplorerDefaultsImpl
/// Decide whether to use potentialTarget or [potentialTarget class] to get or set property
- (id)appropriateTargetForPropertyType:(id)potentialTarget {
@@ -67,11 +77,13 @@
- (NSString *)previewWithTarget:(id)object {
if (object_isClass(object) && !self.isClassProperty) {
return self.attributes.fullDeclaration;
} else {
} else if (self.defaults.wantsDynamicPreviews) {
return [FLEXRuntimeUtility
summaryForObject:[self currentValueWithTarget:object]
];
}
return nil;
}
- (UIViewController *)viewerWithTarget:(id)object {
@@ -94,7 +106,7 @@
// We use .tag to store the cached value of .isEditable that is
// initialized by FLEXObjectExplorer in -reloadMetada
if ([self getPotentiallyUnboxedValue:targetForValueCheck]) {
if (self.tag) {
if (self.defaults.isEditable) {
// Editable non-nil value, both
return UITableViewCellAccessoryDetailDisclosureButton;
} else {
@@ -102,7 +114,7 @@
return UITableViewCellAccessoryDisclosureIndicator;
}
} else {
if (self.tag) {
if (self.defaults.isEditable) {
// Editable nil value, just (i)
return UITableViewCellAccessoryDetailButton;
} else {
@@ -144,7 +156,7 @@
if (targetNotNil) {
id value = [self currentValueBeforeUnboxingWithTarget:object];
[items addObjectsFromArray:@[
@"Value Preview", [self previewWithTarget:object],
@"Value Preview", [self previewWithTarget:object] ?: @"",
@"Value Address", returnsObject ? [FLEXUtility addressOfObject:value] : @"",
]];
}
@@ -177,6 +189,7 @@
#pragma mark FLEXIvar
@implementation FLEXIvar (UIKitHelpers)
FLEXObjectExplorerDefaultsImpl
- (BOOL)isEditable {
const FLEXTypeEncoding *typeEncoding = self.typeEncoding.UTF8String;
@@ -198,10 +211,13 @@
- (NSString *)previewWithTarget:(id)object {
if (object_isClass(object)) {
return self.details;
} else if (self.defaults.wantsDynamicPreviews) {
return [FLEXRuntimeUtility
summaryForObject:[self currentValueWithTarget:object]
];
}
return [FLEXRuntimeUtility
summaryForObject:[self currentValueWithTarget:object]
];
return nil;
}
- (UIViewController *)viewerWithTarget:(id)object {
@@ -222,7 +238,7 @@
// Could use .isEditable here, but we use .tag for speed since it is cached
if ([self getPotentiallyUnboxedValue:object]) {
if (self.tag) {
if (self.defaults.isEditable) {
// Editable non-nil value, both
return UITableViewCellAccessoryDetailDisclosureButton;
} else {
@@ -230,7 +246,7 @@
return UITableViewCellAccessoryDisclosureIndicator;
}
} else {
if (self.tag) {
if (self.defaults.isEditable) {
// Editable nil value, just (i)
return UITableViewCellAccessoryDetailButton;
} else {
@@ -301,6 +317,7 @@
#pragma mark FLEXMethod
@implementation FLEXMethodBase (UIKitHelpers)
FLEXObjectExplorerDefaultsImpl
- (BOOL)isEditable {
return NO;
@@ -402,6 +419,7 @@
#pragma mark FLEXProtocol
@implementation FLEXProtocol (UIKitHelpers)
FLEXObjectExplorerDefaultsImpl
- (BOOL)isEditable {
return NO;
@@ -444,7 +462,7 @@
NSString *conformances = [conformanceNames componentsJoinedByString:@"\n"];
return @[
@"Name", self.name ?: @"",
@"Conformances", conformances,
@"Conformances", conformances ?: @"",
];
}
@@ -475,6 +493,8 @@
@synthesize name = _name;
@synthesize tag = _tag;
FLEXObjectExplorerDefaultsImpl
+ (NSArray<FLEXStaticMetadata *> *)classHierarchy:(NSArray<Class> *)classes {
return [classes flex_mapped:^id(Class cls, NSUInteger idx) {
return [FLEXStaticMetadata_Class withClass:cls];
@@ -1,5 +1,5 @@
//
// NSArray+Functional.h
// NSArray+FLEX.h
// FLEX
//
// Created by Tanner Bennett on 9/25/19.
@@ -1,12 +1,12 @@
//
// NSArray+Functional.m
// NSArray+FLEX.m
// FLEX
//
// Created by Tanner Bennett on 9/25/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import "NSArray+Functional.h"
#import "NSArray+FLEX.h"
#define FLEXArrayClassIsMutable(me) ([[self class] isSubclassOfClass:[NSMutableArray class]])
@@ -13,7 +13,7 @@
@interface NSDictionary (ObjcRuntime)
/// \c kFLEXPropertyAttributeKeyTypeEncoding is the only required key.
/// Keys representing a boolean value should have a value of \c @YES instead of an empty string.
/// Keys representing a boolean value should have a value of \c YES instead of an empty string.
- (NSString *)propertyAttributesString;
+ (instancetype)attributesDictionaryForProperty:(objc_property_t)property;
@@ -1,5 +1,5 @@
//
// NSObject+Reflection.h
// NSObject+FLEX_Reflection.h
// FLEX
//
// Derived from MirrorKit.
@@ -25,6 +25,19 @@ NSArray<Class> *FLEXGetAllSubclasses(_Nullable Class cls, BOOL includeSelf);
NSArray<Class> *FLEXGetClassHierarchy(_Nullable Class cls, BOOL includeSelf);
NSArray<FLEXProtocol *> *FLEXGetConformedProtocols(_Nullable Class cls);
NSArray<FLEXIvar *> *FLEXGetAllIvars(_Nullable Class cls);
/// @param cls a class object to get instance properties,
/// or a metaclass object to get class properties
NSArray<FLEXProperty *> *FLEXGetAllProperties(_Nullable Class cls);
/// @param cls a class object to get instance methods,
/// or a metaclass object to get class methods
/// @param instance used to mark methods as instance methods or not.
/// Not used to determine whether to get instance or class methods.
NSArray<FLEXMethod *> *FLEXGetAllMethods(_Nullable Class cls, BOOL instance);
/// @param cls a class object to get all instance and class methods.
NSArray<FLEXMethod *> *FLEXGetAllInstanceAndClassMethods(_Nullable Class cls);
#pragma mark Reflection
@interface NSObject (Reflection)
@@ -1,5 +1,5 @@
//
// NSObject+Reflection.m
// NSObject+FLEX_Reflection.m
// FLEX
//
// Derived from MirrorKit.
@@ -7,7 +7,7 @@
// Copyright (c) 2015 Tanner Bennett. All rights reserved.
//
#import "NSObject+Reflection.h"
#import "NSObject+FLEX_Reflection.h"
#import "FLEXClassBuilder.h"
#import "FLEXMirror.h"
#import "FLEXProperty.h"
@@ -15,7 +15,7 @@
#import "FLEXIvar.h"
#import "FLEXProtocol.h"
#import "FLEXPropertyAttributes.h"
#import "NSArray+Functional.h"
#import "NSArray+FLEX.h"
#import "FLEXUtility.h"
@@ -101,13 +101,52 @@ NSArray<FLEXProtocol *> *FLEXGetConformedProtocols(Class cls) {
}];
}
NSArray<FLEXIvar *> *FLEXGetAllIvars(_Nullable Class cls) {
if (!cls) return nil;
unsigned int ivcount;
Ivar *objcivars = class_copyIvarList(cls, &ivcount);
NSArray *ivars = [NSArray flex_forEachUpTo:ivcount map:^id(NSUInteger i) {
return [FLEXIvar ivar:objcivars[i]];
}];
free(objcivars);
return ivars;
}
NSArray<FLEXProperty *> *FLEXGetAllProperties(_Nullable Class cls) {
if (!cls) return nil;
unsigned int pcount;
objc_property_t *objcproperties = class_copyPropertyList(cls, &pcount);
NSArray *properties = [NSArray flex_forEachUpTo:pcount map:^id(NSUInteger i) {
return [FLEXProperty property:objcproperties[i] onClass:cls];
}];
free(objcproperties);
return properties;
}
NSArray<FLEXMethod *> *FLEXGetAllMethods(_Nullable Class cls, BOOL instance) {
if (!cls) return nil;
unsigned int mcount;
Method *objcmethods = class_copyMethodList(cls, &mcount);
NSArray *methods = [NSArray flex_forEachUpTo:mcount map:^id(NSUInteger i) {
return [FLEXMethod method:objcmethods[i] isInstanceMethod:instance];
}];
free(objcmethods);
return methods;
}
#pragma mark NSProxy
@interface NSProxy (AnyObjectAdditions) @end
@implementation NSProxy (AnyObjectAdditions)
+ (void)load { FLEX_EXIT_IF_TESTING()
+ (void)load { FLEX_EXIT_IF_NO_CTORS()
// We need to get all of the methods in this file and add them to NSProxy.
// To do this we we need the class itself and it's metaclass.
// Edit: also add them to Swift._SwiftObject
@@ -135,6 +174,11 @@ NSArray<FLEXProtocol *> *FLEXGetConformedProtocols(Class cls) {
FLEXClassBuilder *swiftObjectMeta = [FLEXClassBuilder builderForClass:SwiftObject_meta];
[swiftObject addMethods:instanceMethods];
[swiftObjectMeta addMethods:classMethods];
// So we can put Swift objects into dictionaries...
[swiftObjectMeta addMethods:@[
[NSObject flex_classMethodNamed:@"copyWithZone:"]]
];
}
}
@@ -192,41 +236,17 @@ NSArray<FLEXProtocol *> *FLEXGetConformedProtocols(Class cls) {
@implementation NSObject (Methods)
+ (NSArray<FLEXMethod *> *)flex_allMethods {
NSMutableArray *instanceMethods = (id)self.flex_allInstanceMethods;
NSMutableArray *instanceMethods = self.flex_allInstanceMethods.mutableCopy;
[instanceMethods addObjectsFromArray:self.flex_allClassMethods];
return instanceMethods;
}
+ (NSArray<FLEXMethod *> *)flex_allInstanceMethods {
unsigned int mcount;
Method *objcmethods = class_copyMethodList([self class], &mcount);
NSMutableArray *methods = [NSMutableArray new];
for (int i = 0; i < mcount; i++) {
FLEXMethod *m = [FLEXMethod method:objcmethods[i] isInstanceMethod:YES];
if (m) {
[methods addObject:m];
}
}
free(objcmethods);
return methods;
return FLEXGetAllMethods(self, YES);
}
+ (NSArray<FLEXMethod *> *)flex_allClassMethods {
unsigned int mcount;
Method *objcmethods = class_copyMethodList(self.flex_metaclass, &mcount);
NSMutableArray *methods = [NSMutableArray new];
for (int i = 0; i < mcount; i++) {
FLEXMethod *m = [FLEXMethod method:objcmethods[i] isInstanceMethod:NO];
if (m) {
[methods addObject:m];
}
}
free(objcmethods);
return methods;
return FLEXGetAllMethods(self.flex_metaclass, NO);
}
+ (FLEXMethod *)flex_methodNamed:(NSString *)name {
@@ -292,16 +312,7 @@ NSArray<FLEXProtocol *> *FLEXGetConformedProtocols(Class cls) {
@implementation NSObject (Ivars)
+ (NSArray<FLEXIvar *> *)flex_allIvars {
unsigned int ivcount;
Ivar *objcivars = class_copyIvarList([self class], &ivcount);
NSMutableArray *ivars = [NSMutableArray new];
for (int i = 0; i < ivcount; i++) {
[ivars addObject:[FLEXIvar ivar:objcivars[i]]];
}
free(objcivars);
return ivars;
return FLEXGetAllIvars(self);
}
+ (FLEXIvar *)flex_ivarNamed:(NSString *)name {
@@ -373,36 +384,17 @@ NSArray<FLEXProtocol *> *FLEXGetConformedProtocols(Class cls) {
@implementation NSObject (Properties)
+ (NSArray<FLEXProperty *> *)flex_allProperties {
NSMutableArray *instanceProperties = (id)self.flex_allInstanceProperties;
NSMutableArray *instanceProperties = self.flex_allInstanceProperties.mutableCopy;
[instanceProperties addObjectsFromArray:self.flex_allClassProperties];
return instanceProperties;
}
+ (NSArray<FLEXProperty *> *)flex_allInstanceProperties {
unsigned int pcount;
objc_property_t *objcproperties = class_copyPropertyList(self, &pcount);
NSMutableArray *properties = [NSMutableArray new];
for (int i = 0; i < pcount; i++) {
[properties addObject:[FLEXProperty property:objcproperties[i] onClass:self]];
}
free(objcproperties);
return properties;
return FLEXGetAllProperties(self);
}
+ (NSArray<FLEXProperty *> *)flex_allClassProperties {
Class metaclass = self.flex_metaclass;
unsigned int pcount;
objc_property_t *objcproperties = class_copyPropertyList(metaclass, &pcount);
NSMutableArray *properties = [NSMutableArray new];
for (int i = 0; i < pcount; i++) {
[properties addObject:[FLEXProperty property:objcproperties[i] onClass:metaclass]];
}
free(objcproperties);
return properties;
return FLEXGetAllProperties(self.flex_metaclass);
}
+ (FLEXProperty *)flex_propertyNamed:(NSString *)name {
@@ -14,6 +14,8 @@
@property (nonatomic, readonly) BOOL flex_typeIsConst;
/// @return the first char in the type encoding that is not the const specifier
@property (nonatomic, readonly) FLEXTypeEncoding flex_firstNonConstType;
/// @return the first char in the type encoding after the pointer specifier, if it is a pointer
@property (nonatomic, readonly) FLEXTypeEncoding flex_pointeeType;
/// @return whether this type is an objc object of any kind, even if it's const
@property (nonatomic, readonly) BOOL flex_typeIsObjectOrClass;
/// @return the class named in this type encoding if it is of the form \c @"MYClass"
@@ -75,6 +75,16 @@
return [self characterAtIndex:(self.flex_typeIsConst ? 1 : 0)];
}
- (FLEXTypeEncoding)flex_pointeeType {
if (!self.length) return FLEXTypeEncodingNull;
if (self.flex_firstNonConstType == FLEXTypeEncodingPointer) {
return [self characterAtIndex:(self.flex_typeIsConst ? 2 : 1)];
}
return FLEXTypeEncodingNull;
}
- (BOOL)flex_typeIsObjectOrClass {
FLEXTypeEncoding type = self.flex_firstNonConstType;
return type == FLEXTypeEncodingObjcObject || type == FLEXTypeEncodingObjcClass;
@@ -12,7 +12,7 @@
@interface NSString (Utilities)
/// A dictionary of property attributes if the receiver is a valid property attributes string.
/// Values are either a string or \c @YES. Boolean attributes which are false will not be
/// Values are either a string or \c YES. Boolean attributes which are false will not be
/// present in the dictionary. See this link on how to construct a proper attributes string:
/// https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtPropertyIntrospection.html
///
@@ -8,6 +8,7 @@
#import <Foundation/Foundation.h>
typedef void (^VoidBlock)(void);
@interface NSTimer (Blocks)
+ (instancetype)fireSecondsFromNow:(NSTimeInterval)delay block:(VoidBlock)block;
@@ -5,7 +5,7 @@
// Created by Tanner on 3/23/17.
//
#import "NSTimer+Blocks.h"
#import "NSTimer+FLEX.h"
@interface Block : NSObject
- (void)invoke;
@@ -8,31 +8,37 @@
#import <Foundation/Foundation.h>
// Only use these if the getters and setters aren't good enough for whatever reaso
// Only use these if the getters and setters aren't good enough for whatever reason
extern NSString * const kFLEXDefaultsToolbarTopMarginKey;
extern NSString * const kFLEXDefaultsiOSPersistentOSLogKey;
extern NSString * const kFLEXDefaultsHidePropertyIvarsKey;
extern NSString * const kFLEXDefaultsHidePropertyMethodsKey;
extern NSString * const kFLEXDefaultsHideMethodOverridesKey;
extern NSString * const kFLEXDefaultsHideVariablePreviewsKey;
extern NSString * const kFLEXDefaultsNetworkHostBlacklistKey;
extern NSString * const kFLEXDefaultsDisableOSLogForceASLKey;
extern NSString * const kFLEXDefaultsRegisterJSONExplorerKey;
/// All BOOL preferences are NO by default
@interface NSUserDefaults (FLEX)
- (void)toggleBoolForKey:(NSString *)key;
@property (nonatomic) double flex_toolbarTopMargin;
/// NO by default
@property (nonatomic) BOOL flex_cacheOSLogMessages;
/// NO by default
@property (nonatomic) BOOL flex_explorerHidesPropertyIvars;
/// NO by default
@property (nonatomic) BOOL flex_explorerHidesPropertyMethods;
/// NO by default
@property (nonatomic) BOOL flex_explorerShowsMethodOverrides;
// Not actually stored in defaults, but written to a file
@property (nonatomic) NSArray<NSString *> *flex_networkHostBlacklist;
/// Whether or not to register the object explorer as a JSON viewer on launch
@property (nonatomic) BOOL flex_registerDictionaryJSONViewerOnLaunch;
/// Disable os_log and re-enable ASL. May break Console.app output.
@property (nonatomic) BOOL flex_disableOSLog;
@property (nonatomic) BOOL flex_cacheOSLogMessages;
@property (nonatomic) BOOL flex_explorerHidesPropertyIvars;
@property (nonatomic) BOOL flex_explorerHidesPropertyMethods;
@property (nonatomic) BOOL flex_explorerShowsMethodOverrides;
@property (nonatomic) BOOL flex_explorerHidesVariablePreviews;
@end
@@ -13,17 +13,22 @@ NSString * const kFLEXDefaultsiOSPersistentOSLogKey = @"com.flipborad.flex.enabl
NSString * const kFLEXDefaultsHidePropertyIvarsKey = @"com.flipboard.FLEX.hide_property_ivars";
NSString * const kFLEXDefaultsHidePropertyMethodsKey = @"com.flipboard.FLEX.hide_property_methods";
NSString * const kFLEXDefaultsHideMethodOverridesKey = @"com.flipboard.FLEX.hide_method_overrides";
NSString * const kFLEXDefaultsHideVariablePreviewsKey = @"com.flipboard.FLEX.hide_variable_previews";
NSString * const kFLEXDefaultsNetworkHostBlacklistKey = @"com.flipboard.FLEX.network_host_blacklist";
NSString * const kFLEXDefaultsDisableOSLogForceASLKey = @"com.flipboard.FLEX.try_disable_os_log";
NSString * const kFLEXDefaultsRegisterJSONExplorerKey = @"com.flipboard.FLEX.view_json_as_object";
#define FLEXDefaultsPathForFile(name) ({ \
NSArray *paths = NSSearchPathForDirectoriesInDomains( \
NSLibraryDirectory, NSUserDomainMask, NO \
NSLibraryDirectory, NSUserDomainMask, YES \
); \
[paths[0] stringByAppendingPathComponent:@"Preferences"]; \
})
@implementation NSUserDefaults (FLEX)
#pragma mark Internal
/// @param filename the name of a plist file without any extension
- (NSString *)flex_defaultsPathForFile:(NSString *)filename {
filename = [filename stringByAppendingPathExtension:@"plist"];
@@ -35,11 +40,15 @@ NSString * const kFLEXDefaultsNetworkHostBlacklistKey = @"com.flipboard.FLEX.net
return [preferences stringByAppendingPathComponent:filename];
}
#pragma mark Helper
- (void)toggleBoolForKey:(NSString *)key {
[self setBool:![self boolForKey:key] forKey:key];
[NSNotificationCenter.defaultCenter postNotificationName:key object:nil];
}
#pragma mark Misc
- (double)flex_toolbarTopMargin {
if ([self objectForKey:kFLEXDefaultsToolbarTopMarginKey]) {
return [self doubleForKey:kFLEXDefaultsToolbarTopMarginKey];
@@ -52,6 +61,37 @@ NSString * const kFLEXDefaultsNetworkHostBlacklistKey = @"com.flipboard.FLEX.net
[self setDouble:margin forKey:kFLEXDefaultsToolbarTopMarginKey];
}
- (NSArray<NSString *> *)flex_networkHostBlacklist {
return [NSArray arrayWithContentsOfFile:[
self flex_defaultsPathForFile:kFLEXDefaultsNetworkHostBlacklistKey
]] ?: @[];
}
- (void)setFlex_networkHostBlacklist:(NSArray<NSString *> *)blacklist {
NSParameterAssert(blacklist);
[blacklist writeToFile:[
self flex_defaultsPathForFile:kFLEXDefaultsNetworkHostBlacklistKey
] atomically:YES];
}
- (BOOL)flex_registerDictionaryJSONViewerOnLaunch {
return [self boolForKey:kFLEXDefaultsRegisterJSONExplorerKey];
}
- (void)setFlex_registerDictionaryJSONViewerOnLaunch:(BOOL)enable {
[self setBool:enable forKey:kFLEXDefaultsRegisterJSONExplorerKey];
}
#pragma mark System Log
- (BOOL)flex_disableOSLog {
return [self boolForKey:kFLEXDefaultsDisableOSLogForceASLKey];
}
- (void)setFlex_disableOSLog:(BOOL)disable {
[self setBool:disable forKey:kFLEXDefaultsDisableOSLogForceASLKey];
}
- (BOOL)flex_cacheOSLogMessages {
return [self boolForKey:kFLEXDefaultsiOSPersistentOSLogKey];
}
@@ -64,6 +104,8 @@ NSString * const kFLEXDefaultsNetworkHostBlacklistKey = @"com.flipboard.FLEX.net
];
}
#pragma mark Object Explorer
- (BOOL)flex_explorerHidesPropertyIvars {
return [self boolForKey:kFLEXDefaultsHidePropertyIvarsKey];
}
@@ -100,17 +142,16 @@ NSString * const kFLEXDefaultsNetworkHostBlacklistKey = @"com.flipboard.FLEX.net
];
}
- (NSArray<NSString *> *)flex_networkHostBlacklist {
return [NSArray arrayWithContentsOfFile:[
self flex_defaultsPathForFile:kFLEXDefaultsNetworkHostBlacklistKey
]] ?: @[];
- (BOOL)flex_explorerHidesVariablePreviews {
return [self boolForKey:kFLEXDefaultsHideVariablePreviewsKey];
}
- (void)setFlex_networkHostBlacklist:(NSArray<NSString *> *)blacklist {
NSParameterAssert(blacklist);
[blacklist writeToFile:[
self flex_defaultsPathForFile:kFLEXDefaultsNetworkHostBlacklistKey
] atomically:YES];
- (void)setFlex_explorerHidesVariablePreviews:(BOOL)hide {
[self setBool:hide forKey:kFLEXDefaultsHideVariablePreviewsKey];
[NSNotificationCenter.defaultCenter
postNotificationName:kFLEXDefaultsHideVariablePreviewsKey
object:nil
];
}
@end
+3 -1
View File
@@ -29,7 +29,9 @@ typedef FLEXAlertAction *(^FLEXAlertActionHandler)(void(^handler)(NSArray<NSStri
/// Construct and display an alert
+ (void)makeAlert:(FLEXAlertBuilder)block showFrom:(UIViewController *)viewController;
/// Construct and display an action sheet-style alert
+ (void)makeSheet:(FLEXAlertBuilder)block showFrom:(UIViewController *)viewController;
+ (void)makeSheet:(FLEXAlertBuilder)block
showFrom:(UIViewController *)viewController
source:(id)viewOrBarItem;
/// Construct an alert
+ (UIAlertController *)makeAlert:(FLEXAlertBuilder)block;
+33 -8
View File
@@ -62,17 +62,42 @@ NSAssert(!self._action, @"Cannot mutate action after retreiving underlying UIAle
return alert._controller;
}
+ (void)make:(FLEXAlertBuilder)block withStyle:(UIAlertControllerStyle)style showFrom:(UIViewController *)viewController {
+ (void)make:(FLEXAlertBuilder)block
withStyle:(UIAlertControllerStyle)style
showFrom:(UIViewController *)viewController
source:(id)viewOrBarItem {
UIAlertController *alert = [self make:block withStyle:style];
if ([viewOrBarItem isKindOfClass:[UIBarButtonItem class]]) {
alert.popoverPresentationController.barButtonItem = viewOrBarItem;
} else if ([viewOrBarItem isKindOfClass:[UIView class]]) {
alert.popoverPresentationController.sourceView = viewOrBarItem;
alert.popoverPresentationController.sourceRect = [viewOrBarItem bounds];
} else if (viewOrBarItem) {
NSParameterAssert(
[viewOrBarItem isKindOfClass:[UIBarButtonItem class]] ||
[viewOrBarItem isKindOfClass:[UIView class]] ||
!viewOrBarItem
);
}
[viewController presentViewController:alert animated:YES completion:nil];
}
+ (void)makeAlert:(FLEXAlertBuilder)block showFrom:(UIViewController *)viewController {
[self make:block withStyle:UIAlertControllerStyleAlert showFrom:viewController];
+ (void)makeAlert:(FLEXAlertBuilder)block showFrom:(UIViewController *)controller {
[self make:block withStyle:UIAlertControllerStyleAlert showFrom:controller source:nil];
}
+ (void)makeSheet:(FLEXAlertBuilder)block showFrom:(UIViewController *)viewController {
[self make:block withStyle:UIAlertControllerStyleActionSheet showFrom:viewController];
+ (void)makeSheet:(FLEXAlertBuilder)block showFrom:(UIViewController *)controller {
[self make:block withStyle:UIAlertControllerStyleActionSheet showFrom:controller source:nil];
}
/// Construct and display an action sheet-style alert
+ (void)makeSheet:(FLEXAlertBuilder)block
showFrom:(UIViewController *)controller
source:(id)viewOrBarItem {
[self make:block
withStyle:UIAlertControllerStyleActionSheet
showFrom:controller
source:viewOrBarItem];
}
+ (UIAlertController *)makeAlert:(FLEXAlertBuilder)block {
@@ -88,7 +113,7 @@ NSAssert(!self._action, @"Cannot mutate action after retreiving underlying UIAle
- (FLEXAlertStringProperty)title {
return ^FLEXAlert *(NSString *title) {
if (self._controller.title) {
self._controller.title = [self._controller.title stringByAppendingString:title];
self._controller.title = [self._controller.title stringByAppendingString:title ?: @""];
} else {
self._controller.title = title;
}
@@ -99,7 +124,7 @@ NSAssert(!self._action, @"Cannot mutate action after retreiving underlying UIAle
- (FLEXAlertStringProperty)message {
return ^FLEXAlert *(NSString *message) {
if (self._controller.message) {
self._controller.message = [self._controller.message stringByAppendingString:message];
self._controller.message = [self._controller.message stringByAppendingString:message ?: @""];
} else {
self._controller.message = message;
}
@@ -141,7 +166,7 @@ NSAssert(!self._action, @"Cannot mutate action after retreiving underlying UIAle
return ^FLEXAlertAction *(NSString *title) {
FLEXAlertActionMutationAssertion();
if (self._title) {
self._title = [self._title stringByAppendingString:title];
self._title = [self._title stringByAppendingString:title ?: @""];
} else {
self._title = title;
}
+5 -16
View File
@@ -7,6 +7,7 @@
//
#import "FLEXHeapEnumerator.h"
#import "FLEXObjcInternal.h"
#import <malloc/malloc.h>
#import <mach/mach.h>
#import <objc/runtime.h>
@@ -84,26 +85,14 @@ static kern_return_t reader(__unused task_t remote_task, vm_address_t remote_add
block(object, actualClass);
lock_zone(zone);
};
// The largest realistic memory address varies by platform.
// Only 48 bits are used by 64 bit machines while
// 32 bit machines use all bits.
//
// __LP64__ is defined as 1 for both arm64 and x86_64
// via: clang -dM -arch [arm64|x86_64] -E -x c /dev/null | grep LP
#if __LP64__
static uintptr_t MAX_REALISTIC_ADDRESS = 0x0000FFFFFFFFFFFF;
BOOL lockZoneValid = lock_zone != nil && (uintptr_t)lock_zone < MAX_REALISTIC_ADDRESS;
BOOL unlockZoneValid = unlock_zone != nil && (uintptr_t)unlock_zone < MAX_REALISTIC_ADDRESS;
#else
BOOL lockZoneValid = lock_zone != nil;
BOOL unlockZoneValid = unlock_zone != nil;
#endif
BOOL lockZoneValid = FLEXPointerIsReadable(lock_zone);
BOOL unlockZoneValid = FLEXPointerIsReadable(unlock_zone);
// There is little documentation on when and why
// any of these function pointers might be NULL
// or garbage, so we resort to checking for NULL
// and impossible memory addresses at least
// and whether the pointer is readable
if (introspection->enumerator && lockZoneValid && unlockZoneValid) {
lock_zone(zone);
introspection->enumerator(TASK_NULL, (void *)&callback, MALLOC_PTR_IN_USE_RANGE_TYPE, (vm_address_t)zone, reader, &range_callback);
+16 -2
View File
@@ -9,8 +9,18 @@
#ifndef FLEXMacros_h
#define FLEXMacros_h
// Used to prevent loading of pre-registered shortcuts and runtime categories in a test environment
#define FLEX_EXIT_IF_TESTING() if (NSClassFromString(@"XCTest")) return;
#define flex_keywordify class NSObject;
#define ctor flex_keywordify __attribute__((constructor)) void __flex_ctor_##__LINE__()
#define dtor flex_keywordify __attribute__((destructor)) void __flex_dtor_##__LINE__()
// A macro to check if we are running in a test environment
#define FLEX_IS_TESTING() (NSClassFromString(@"XCTest") != nil)
/// Whether we want the majority of constructors to run upon load or not.
extern BOOL FLEXConstructorsShouldRun();
/// A macro to return from the current procedure if we don't want to run constructors
#define FLEX_EXIT_IF_NO_CTORS() if (!FLEXConstructorsShouldRun()) return;
/// Rounds down to the nearest "point" coordinate
NS_INLINE CGFloat FLEXFloor(CGFloat x) {
@@ -71,4 +81,8 @@ NS_INLINE CGRect FLEXRectSetHeight(CGRect r, CGFloat height) {
stringWithFormat:(count == 1 ? singularFormat : pluralFormat), @(count) \
]
#define flex_dispatch_after(nSeconds, onQueue, block) \
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, \
(int64_t)(nSeconds * NSEC_PER_SEC)), onQueue, block)
#endif /* FLEXMacros_h */
+1 -1
View File
@@ -13,7 +13,7 @@
#import <objc/runtime.h>
#import "FLEXTypeEncodingParser.h"
#import "FLEXAlert.h"
#import "NSArray+Functional.h"
#import "NSArray+FLEX.h"
#import "UIFont+FLEX.h"
#import "NSMapTable+FLEX_Subscripting.h"
#import "FLEXMacros.h"
+15 -1
View File
@@ -11,8 +11,22 @@
#import "FLEXResources.h"
#import "FLEXWindow.h"
#import <ImageIO/ImageIO.h>
#import <zlib.h>
#import <objc/runtime.h>
#import <zlib.h>
BOOL FLEXConstructorsShouldRun() {
static BOOL _FLEXConstructorsShouldRun_storage = YES;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSString *key = @"FLEX_SKIP_INIT";
if (getenv(key.UTF8String) || [NSUserDefaults.standardUserDefaults boolForKey:key]) {
_FLEXConstructorsShouldRun_storage = NO;
}
});
return _FLEXConstructorsShouldRun_storage;
}
@implementation FLEXUtility

Some files were not shown because too many files have changed in this diff Show More