Compare commits

...

100 Commits

Author SHA1 Message Date
Tanner Bennett 6c0a3b43eb Well, this quickly became a pain.
This will be near impossible without importing thousands of lines of code from the compiler. Swift interop is already probably far too difficult to achieve without writing C++, and in Joe's own words:

"If you're writing code in C++ already then definitely just using the headers or MetadataReader as is will keep you better in sync with future additions to the runtime."

Another option to consider is libSwiftRemoteMirror.

This commit leaves the project in an incomplete and broken state.
2020-02-10 16:57:58 -06:00
Tanner Bennett b6914ff990 Skeleton files for SwiftObject support 2020-02-09 18:26:21 -06:00
Tanner Bennett f601018373 Fix crash in network screen while searching 2020-02-09 18:12:40 -06:00
Tanner Bennett a171eedc40 SwiftObject-related fixes 2020-02-08 22:16:09 -06:00
Tanner Bennett 8acbc804b4 Add window and scene managemenet screen
Also fix potential bugs in FLEXExplorerViewController
2020-02-06 19:01:22 -06:00
Tanner Bennett 30a9f5628d Add basic support for bookmarking objects 2020-02-06 19:01:22 -06:00
Tanner Bennett 7362870570 Fix bugs in collection and defaults content sections
FLEXCollectionContentSection and FLEXDefaultsContentSection
2020-02-06 19:01:22 -06:00
Tanner Bennett 26333cbb25 Fix bugs in FLEXNetworkHistoryTableViewController
Also, rename it to FLEXNetworkMITMViewController
2020-02-06 19:01:22 -06:00
Tanner Bennett 15377eabbc Change OS_ACTIVITY_MODE to OS_ACTIVITY_DT_MODE
Silences annoying internal crap in the console
2020-02-06 19:01:22 -06:00
Tanner Bennett b211a845d2 Add basic support for tabs
Other changes:
- Editor/caller view controllers use a toolbar for the call/set button now
- FLEXNavigationController adds the Done button to it's root view controller instead of FLEXExplorerViewController
- FLEXExplorerViewController now overrides presentViewController: and dismissViewControllerAnimated: to toggle its window's key status instead of using new methods to do it
- Adds a 't' simulator shortcut to quickly present an explorer screen for testing
2020-02-06 19:01:22 -06:00
Tanner Bennett 873e79d2c0 Change how simulator shortcuts present tools 2020-02-06 14:21:22 -06:00
Tanner Bennett 17e74a7b02 Fix crash in FLEXInstancesTableViewController
Also remove "table" from the name
2020-02-06 14:21:22 -06:00
Tanner Bennett 795cff68fd Clean up formatting in various files
- Braces on same line
- Comments and method calls curbed to be near or under 100 characters per line
2020-02-06 14:21:22 -06:00
Tanner Bennett 162bf48b5e FLEXGlobalsTableViewController → FLEXGlobalsView…r 2020-02-06 14:21:22 -06:00
Tanner Bennett 239afdbd7c Refactor keyWindow-related logic
First, we give FLEXUtility some methods to grab the app's keyWindow (and the active UIWindowScene on iOS 13).

Now, FLEXWindow will use this method to store the previous keyWindow as it becomes the new keyWindow. Other view controllers which need to reference the keyWindow will simply call self.window.previousKeyWindow (where self.window is a new property added to FLEXTableViewController).

Now, we don't need to go hunting for it anywhere else, and we don't need to hold a reference to it in FLEXGlobalsTableViewController.
2020-02-06 14:21:22 -06:00
Tanner Bennett e53e083507 Fix simulator up/down arrow scrolling shortcuts 2020-02-06 14:21:22 -06:00
Tanner Bennett 1acfa52c64 Fix "Views At Tap" not defaulting to correct tab 2020-02-06 14:21:22 -06:00
Tanner Bennett fa9eec08c1 Fix bug in FLEXNetworkHistoryTableViewController 2020-02-06 14:21:22 -06:00
Tanner Bennett 2160fb3c46 Add FLEXNavigationController
Provides a gesture which automatically hides or shows the toolbar on explorer screens as you scroll when you turn on `hidesBarsOnSwipe`.

UINavigationController provides this functionality by default, except that it also hides the navigation bar, and the toolbar is always shown if it has no items for some odd reason. This subclass overrides a private method (verified to exist since at least iOS 9, up to at least iOS 13) to change that.
2020-02-06 14:21:22 -06:00
Tanner Bennett 43e7938281 Refactor FLEXManager into categories for organization
window
2020-02-06 14:21:22 -06:00
Tanner Bennett b7baa219a4 Clean up FLEXImagePreviewViewController 2020-02-02 17:43:09 -06:00
Tanner Bennett 5d27613b38 Fix carousel + search bar behavior for pre-iOS 11 2020-02-02 17:41:52 -06:00
Tanner Bennett d8ff46853d Fix shortcuts for view controllers
Refactor FLEXShortcut and FLEXActionShortcut so that shortcuts can perform any arbitrary action when they are selected. View controllers will show a dialog when they cannot be pushed.
2020-01-30 19:17:16 -06:00
Tanner Bennett 804fd376b9 Better block introspection
blocks
2020-01-30 19:17:16 -06:00
Tanner Bennett 8ef3a78386 Fix shortcut related bugs 2020-01-30 18:13:51 -06:00
Tanner Bennett 05bd084174 Fix in FLEXPointerIsValidObjcObject
Fix one of many potential crashes. Added notes describing more potential crashes to fix in the future.
2020-01-30 18:13:51 -06:00
Tanner Bennett 920727e375 Add more shortcuts to UView and UIViewController 2020-01-30 18:13:51 -06:00
Tanner Bennett 3647c8deee Refactor FLEXHierarchyTableViewCell
- All view properties private
- Adjust padding between subviews
- Color indicator view now shows actual color and transparency
2020-01-30 15:55:01 -06:00
Tanner Bennett 43c15356d9 Improve behavior of TBKeyPathTokenizer 2020-01-30 14:31:37 -06:00
Tanner Bennett 7c28466ec8 Fix bug in FLEXMethod 2020-01-30 14:31:37 -06:00
Tanner Bennett edf5426e80 Add "Explore <class>" to context menu for props/ivars 2020-01-30 14:31:37 -06:00
Tanner Bennett 3fdaead2f7 Refactor presentation/dismissal code from toolbar
- Now expects UINavigationControllers to be presented
- The only view controller to add it's own done buttons now is the hierarchy screen because it needs to pass data back to the explorer upon dismissal
- Automatically adds a Done button to the navigationItem of the topViewController of presented navigation controllers if a button is not already present
- Remove FLEXGlobalsTableViewControllerDelegate, etc, and like methods
- Adopt iOS 13 APIs for detecting modal sheet dismissal by dragging
2020-01-30 14:31:37 -06:00
Tanner Bennett 1b2181d1f7 Preserve the app's UIMenuController items 2020-01-30 14:31:37 -06:00
Tanner Bennett ee2f5c6415 Fix window level code 2020-01-30 14:31:37 -06:00
Tanner Bennett 503ce499d1 Uncomment _heightForHeaderInSection override
I forgot but this removes a weird gap at the top of our table views on iOS 12 and below
2020-01-30 14:31:37 -06:00
Tanner Bennett 1ac04cd52c Fix visual bug when pushing explorers from some screens
When the search bar is pinned — like it was on the view hierarchy screen or the runtime browser screen — pushing another view controller causes it to have a really tall gap at the top of the table view momentarily. Unpinning it makes this gap and the animation associated with it vanish. This is the best I can do for now until I can really track down the cause, which is likely a UIKit bug or intentional.
2020-01-30 14:31:37 -06:00
Tanner Bennett c3801f2366 Work around SDK header typo 2020-01-29 18:10:59 -06:00
Tanner Bennett 7703757dab Refactor FLEXGlobals*
- Make FLEXGlobalsSection inherit from FLEXTableViewSection
- Adopt changes in FLEXGlobalsTableViewController
2020-01-29 14:54:21 -06:00
Tanner Bennett 756b8210b8 Refine FLEXTableViewSection documentation 2020-01-29 14:54:21 -06:00
Tanner Bennett 30ce841030 Reorganize project structure part 2
Rename FLEXExplorerSection → FLEXTableViewSection
2020-01-29 14:54:21 -06:00
Tanner Bennett 3036676a93 Reorganize project structure part 1
Rename FLEXTableViewSection → FLEXGlobalsSection
2020-01-29 14:54:21 -06:00
Tanner Bennett bf649ff1f6 Add context menus to metadata in the object explorer 2020-01-29 14:54:21 -06:00
Tanner Bennett 9f68011207 Adjust calculated height of description cell
The height calculation for the description cell did not previously take into account the table's separator insets, which may be adjusted for things like section index titles.
2020-01-28 17:11:02 -06:00
Tanner Bennett b5a95bacba Disable mutli-line rows pre iOS 11
iOS 11 is the first version to support UITableViewAutomaticDimension for default UITableViewCells and their default labels. A future release will backport multiline support to iOS 9-10.

Also cleans up +[FLEXMultilineTableViewCell preferredHeightWithAttributedText:…] and formats it's usages
2020-01-28 17:11:02 -06:00
Tanner Bennett 6ec780b679 Add class hierarchy back to object explorer screen 2020-01-27 16:24:48 -06:00
Tanner Bennett fab67cb5cb Fix crash in object explorer search screen 2020-01-23 18:42:10 -06:00
Tanner Bennett f24990d10f Add "misc" metadata to the bottom of the object explorer
Add FLEXKeyValueTableViewCell for the "instance size" row, which has the title on the left and the value on the right
2020-01-23 18:10:40 -06:00
Tanner Bennett 43a3f859e2 Center titles in no-subtitle shortcuts 2020-01-23 18:08:18 -06:00
Tanner Bennett f3129aaa90 Support class shortcuts 2020-01-23 18:07:56 -06:00
Tanner Bennett 3199063914 Monospace font for metadata
Minor font adjustments
2020-01-21 14:59:24 -06:00
Tanner Bennett 7d6cc33c2a ObjectExplorerFactory: fall back to ShortcutsSection 2020-01-21 14:59:24 -06:00
Tanner Bennett 0b914bb7c6 Add graphics resources 2020-01-21 14:59:24 -06:00
Tanner Bennett 5cff9ce3e8 Remove a warning 2020-01-21 14:59:24 -06:00
Tanner Bennett 73bc5a4443 Add image names and instance sizes to FLEXObjectExplorer
For later use in some kind of metadata section
2020-01-21 14:59:24 -06:00
Tanner Bennett 65e5bee4e3 Add swipe gesture to object explorer screen
Gesture allows you to swipe right or left to go up or down the class hierarchy without having to reach up to the top of the screen.

This commit also moves some methods around (nonemptySections, sectionHasActions:)
2020-01-21 14:59:24 -06:00
Tanner Bennett c02192cd71 Allow editing of Class ivars/properties 2020-01-21 14:59:24 -06:00
Tanner Bennett 37757d76e1 Add list of conformed protocols to object explorer 2020-01-21 14:59:24 -06:00
Tanner Bennett 3226cbc8c4 Add • indexes for quickly scrolling to explorer sections 2020-01-21 14:59:24 -06:00
Tanner Bennett ab720971de Clean up unsupported input appearance 2020-01-21 14:59:24 -06:00
Tanner Bennett 613a886604 Add support for char * and SEL string arguments 2020-01-21 14:59:24 -06:00
Tanner Bennett 3581267072 Add shortcuts for UIActivity related classes 2020-01-21 14:59:24 -06:00
Tanner Bennett 7481f7e098 Exclude unsafe ivars from evaluation 2020-01-21 14:59:24 -06:00
Tanner Bennett c44b251413 Fix FLEXCodeFontCell and use system monospace font 2020-01-21 14:58:48 -06:00
Tanner Bennett 2541c89b99 Add details to metadata for class views 2020-01-17 17:28:54 -06:00
Tanner Bennett bfacfcfe55 Add "paste" button to argument input text views 2020-01-17 17:28:54 -06:00
Tanner Bennett b221da7021 Add some polish to the 3D explorer
Also fix a bug in FLEXTableViewController where a section header would appear in plain tables with no header

fhs
2020-01-17 17:28:54 -06:00
Tanner Bennett 8d381ea020 Fix copying object description not working 2020-01-17 17:15:23 -06:00
Tanner Bennett 3bde42fd21 Add 3D view explorer 2020-01-17 17:15:23 -06:00
Tanner Bennett 9790c7b0bc Add FLEXActionShortcut , refactor FLEXViewShortcuts 2020-01-17 17:15:23 -06:00
Tanner Bennett b10807cd84 Add NSPhotoLibraryAddUsageDescription to sample app 2020-01-17 17:15:23 -06:00
Tanner Bennett 799e3b2a88 Comment out table view _heightForHeaderInSection 2020-01-17 17:15:23 -06:00
Tanner Bennett a95a31cf74 Add new icons and add missing 3x icons 2020-01-17 17:15:23 -06:00
Tanner Bennett f43b438be0 Refactor FLEXResources.m
Previously, almost every image was an array of bytes on a single line. When most or all of the images fit into the editor, this causes the editor to page the entire (massive) file into memory.

This commit splits each massive line up into lines of 16 bytes, so that the entire file is never loaded at once. This makes opening and editing the file much faster—it used to take almost 10 seconds on a 2019 MacBook Pro.
2020-01-17 17:15:23 -06:00
Tanner Bennett 6d4c7b5e0d Replace classes/libraries with "runtime browser" 2020-01-17 17:15:23 -06:00
Tanner Bennett bbc64efb12 Add files for runtime browser 2020-01-17 17:15:23 -06:00
Tanner Bennett c0cb5f6dcb Move FLEXUtility font methods to a UIFont category 2020-01-17 17:15:23 -06:00
Tanner Bennett fc9874ece6 Consolidate metadata row logic
Consolidate the logic between FLEXShortcut and FLEXMetadataSection for how metadata rows are displayed.

There was a lot of duplication between these two classes before this. Now there is one source of truth: the object itself will tell you how to display itself via our categories in FLEXRuntime+UIKitHelpers.
2020-01-17 17:15:23 -06:00
Tanner Bennett c098bb2c5e Add support for class properties 2020-01-17 17:15:23 -06:00
Tanner Bennett 605d35b364 Add some override methods to Person.m for testing 2020-01-17 17:15:22 -06:00
Tanner Bennett 10fcdfd886 UIView+FLEX_Layout additions
amend
2020-01-17 17:15:22 -06:00
Tanner Bennett 44aac90d41 Not sure what happened here; fix compile errors 2020-01-17 17:15:22 -06:00
Tanner Bennett f5cc3fd347 Delete view controllers now replaced by sections 2020-01-17 17:15:22 -06:00
Tanner Bennett b4e8574a2f Move FLEXObjectExplorerViewController 2020-01-17 17:15:22 -06:00
Tanner Bennett 191680a6b9 OS_ACTIVITY_MODE 2020-01-17 17:15:22 -06:00
Tanner Bennett 0f1134e25e FLEX*Utility bug fixes
- Handle case where protocol method descriptions don't have type information
- Hard-code support for `std::string` to avoid calling it and to make the type readable
2020-01-17 17:15:22 -06:00
Tanner Bennett d506fee663 Fix property attributes
`property_copyAttributeValue()` has a bug, and we had the same bug in our own code.

When the type encoding of a property has a comma in the name—such as a templated C++ object might, like std::string—property_copyAttributeValue does not copy the full type encoding out of the property attributes string. Instead it copies up to the first comma.

As an example, NSString has a private property `std::string stdString` and the type encoding for `std::string` has lots of commas in it. It's type encoding looks like this:

```
{basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >={__compressed_pair<std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >::__rep, std::__1::allocator<char> >={__rep}}}
```

When you call property_copyAttributeValue() for this particular property, it thinks the type is just `{basic_string<char` and that's all you get back.

Looking into the source code of `iteratePropertyAttributes` in the objc runtime, I can see all it does is search for the next comma instead of attempting to parse out a type encoding as it should—or at least, look for a closing brace if the type is a struct.
2020-01-17 17:15:22 -06:00
Tanner Bennett 11544b77b6 Move methods from RuntimeUtility to metadata classes 2020-01-17 17:15:22 -06:00
Tanner Bennett aa5b9d4e7f Adopt FLEXExplorerSection additions
# Conflicts:
#	Classes/ObjectExplorers/Controllers/FLEXObjectExplorerViewController.m

# Conflicts:
#	Classes/ObjectExplorers/FLEXObjectExplorerFactory.m
2020-01-17 17:15:22 -06:00
Tanner Bennett c04e0b606d Add some tests 2020-01-17 17:15:22 -06:00
Tanner Bennett eb1345dcb3 Move runtime @property additions to ViewShortcuts 2020-01-17 17:15:22 -06:00
Tanner Bennett 7186b58e80 Fix argument input screen colors 2020-01-17 17:15:22 -06:00
Tanner Bennett 5859017040 Fix argument input screen colors 2020-01-17 17:15:22 -06:00
Tanner Bennett d31e1aeb4e FLEX*EditorViewController overhaul
There was a lot of code duplication between the property and ivar editor classes. This fixes that.

- Renamed: FLEXFieldEditorViewController → FLEXVariableEditorViewController
- Renamed: FLEXMutableFieldEditorViewController → FLEXFieldEditorViewController
- Collapsed FLEXPropertyEditorViewController and FLEXIvarEditorViewController into their parent class, FLEXFieldEditorViewController
- Property/ivar editor now takes a FLEXProperty/FLEXIvar
- Property/ivar editor initializer is failable based on editability of property/ivar
- FLEXMethodCallingViewController now takes a FLEXMethod
- Argument input views will now generally allow editing of a nil value
2020-01-17 17:15:22 -06:00
Tanner Bennett 3d05e4fb6a Shortcuts and sections
These classes will be the direct data sources for explorer view controllers, and runtime metadata sections will pull their data from a `FLEXObjectExplorer` instance tied to an object explorer view controller
2020-01-17 17:15:22 -06:00
Tanner Bennett ba1de91f85 Add FLEXObjectExplorer
This class will become the data source for object explorer view controllers.
2020-01-17 17:15:22 -06:00
Tanner Bennett 1e9379dc02 Runtime utilities upgrades
An objc runtime wrapper derived from NSExceptional/MirrorKit has been added and will come to replace a few small classes and many of the methods in the FLEXRuntimeUtility and FLEXUtility classes.
2020-01-17 17:15:22 -06:00
Tanner Bennett 965419bd58 Refactor FLEXTableViewCell 2020-01-17 17:15:22 -06:00
Tanner Bennett b21fbabd67 Alert user if app delegate does not provide -window 2020-01-17 17:15:22 -06:00
Tanner Bennett 31446c01be globalsEntryRowAction: should take precedence in FLEXGlobalsEntry 2020-01-17 17:15:22 -06:00
Tanner Bennett 81a3336053 NSArray+Functional additions 2020-01-17 13:54:25 -06:00
395 changed files with 34577 additions and 6718 deletions
@@ -0,0 +1,19 @@
//
// FLEXNavigationController.h
// FLEX
//
// Created by Tanner on 1/30/20.
// Copyright © 2020 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface FLEXNavigationController : UINavigationController
+ (instancetype)withRootViewController:(UIViewController *)rootVC;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,89 @@
//
// FLEXNavigationController.m
// FLEX
//
// Created by Tanner on 1/30/20.
// Copyright © 2020 Flipboard. All rights reserved.
//
#import "FLEXNavigationController.h"
#import "FLEXExplorerViewController.h"
#import "FLEXTabList.h"
@interface UINavigationController (Private) <UIGestureRecognizerDelegate>
- (void)_gestureRecognizedInteractiveHide:(UIGestureRecognizer *)sender;
@end
@interface UIPanGestureRecognizer (Private)
- (void)_setDelegate:(id)delegate;
@end
@interface FLEXNavigationController ()
@property (nonatomic, readonly) BOOL toolbarWasHidden;
@property (nonatomic) BOOL waitingToAddTab;
@end
@implementation FLEXNavigationController
+ (instancetype)withRootViewController:(UIViewController *)rootVC {
FLEXNavigationController *instance = [[self alloc] initWithRootViewController:rootVC];
// Give root view controllers a Done button
UIBarButtonItem *done = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemDone
target:instance
action:@selector(dismissAnimated)
];
// Prepend the button if other buttons exist already
NSArray *existingItems = rootVC.navigationItem.rightBarButtonItems;
if (existingItems.count) {
rootVC.navigationItem.rightBarButtonItems = [@[done] arrayByAddingObjectsFromArray:existingItems];
} else {
rootVC.navigationItem.rightBarButtonItem = done;
}
return instance;
}
- (void)viewDidLoad {
[super viewDidLoad];
self.waitingToAddTab = YES;
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
if (self.waitingToAddTab) {
// Only add new tab if we're presented properly
if ([self.presentingViewController isKindOfClass:[FLEXExplorerViewController class]]) {
// New navigation controllers always add themselves as new tabs,
// tabs are closed by FLEXExplorerViewController
[FLEXTabList.sharedList addTab:self];
self.waitingToAddTab = NO;
}
}
}
- (void)dismissAnimated {
// TODO tabs not closed on swipe down gesture
[FLEXTabList.sharedList closeTab:self];
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
}
- (void)_gestureRecognizedInteractiveHide:(UIPanGestureRecognizer *)sender {
if (sender.state == UIGestureRecognizerStateRecognized) {
BOOL show = self.topViewController.toolbarItems.count;
CGFloat yTranslation = [sender translationInView:self.view].y;
CGFloat yVelocity = [sender velocityInView:self.view].y;
if (yVelocity > 2000) {
[self setToolbarHidden:YES animated:YES];
} else if (show && yTranslation > 20 && yVelocity > 250) {
[self setToolbarHidden:NO animated:YES];
} else if (yTranslation < -20) {
[self setToolbarHidden:YES animated:YES];
}
}
}
@end
@@ -7,7 +7,8 @@
//
#import <UIKit/UIKit.h>
@class FLEXScopeCarousel;
#import "FLEXTableView.h"
@class FLEXScopeCarousel, FLEXWindow;
typedef CGFloat FLEXDebounceInterval;
/// No delay, all events delivered
@@ -19,10 +20,17 @@ extern CGFloat const kFLEXDebounceForAsyncSearch;
/// The least frequent, at just over once per second; for I/O or other expensive operations
extern CGFloat const kFLEXDebounceForExpensiveIO;
@interface FLEXTableViewController : UITableViewController <UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate>
@protocol FLEXSearchResultsUpdating <NSObject>
- (void)updateSearchResults:(NSString *)newText;
@end
@interface FLEXTableViewController : UITableViewController <
UISearchResultsUpdating, UISearchControllerDelegate,
UISearchBarDelegate, FLEXSearchResultsUpdating
>
/// A grouped table view. Inset on iOS 13.
///
///
/// Simply calls into initWithStyle:
- (id)init;
@@ -36,7 +44,7 @@ extern CGFloat const kFLEXDebounceForExpensiveIO;
@property (nonatomic) FLEXScopeCarousel *carousel;
/// Defaults to NO.
///
///
/// Setting this to YES will initialize searchController and the view.
@property (nonatomic) BOOL showsSearchBar;
/// Defaults to NO.
@@ -46,22 +54,21 @@ extern CGFloat const kFLEXDebounceForExpensiveIO;
@property (nonatomic) BOOL showSearchBarInitially;
/// nil unless showsSearchBar is set to YES.
///
///
/// self is used as the default search results updater and delegate.
/// Make sure your subclass conforms to UISearchControllerDelegate.
/// The search bar will not dim the background or hide the navigation bar by default.
/// On iOS 11 and up, the search bar will appear in the navigation bar below the title.
@property (nonatomic) UISearchController *searchController;
/// Used to initialize the search controller. Defaults to nil.
@property (nonatomic) UIViewController *searchResultsController;
/// Defaults to "Fast"
///
///
/// Determines how often search bar results will be "debounced."
/// Empty query events are always sent instantly. Query events will
/// be sent when the user has not changed the query for this interval.
@property (nonatomic) FLEXDebounceInterval searchBarDebounceInterval;
/// Whether the search bar stays at the top of the view while scrolling.
///
///
/// Calls into self.navigationItem.hidesSearchBarWhenScrolling.
/// Do not change self.navigationItem.hidesSearchBarWhenScrolling directly,
/// or it will not be respsected. Use this instead.
@@ -69,7 +76,7 @@ extern CGFloat const kFLEXDebounceForExpensiveIO;
@property (nonatomic) BOOL pinSearchBar;
/// 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
/// searchBar manually. Set this property after turning on showsSearchBar.
///
@@ -78,12 +85,19 @@ extern CGFloat const kFLEXDebounceForExpensiveIO;
/// If using the scope bar, self.searchController.searchBar.selectedScopeButtonIndex.
/// Otherwise, this is the selected index of the carousel, or NSNotFound if using neither.
@property (nonatomic, readonly) NSInteger selectedScope;
@property (nonatomic) NSInteger selectedScope;
/// self.searchController.searchBar.text
@property (nonatomic, readonly) 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;
/// self.view.window as a \c FLEXWindow
@property (nonatomic, readonly) FLEXWindow *window;
/// Subclasses should override to handle search query update events.
///
///
/// searchBarDebounceInterval is used to reduce the frequency at which this method is called.
/// This method is also called when the search bar becomes the first responder,
/// and when the selected search bar scope index changes.
@@ -93,4 +107,14 @@ extern CGFloat const kFLEXDebounceForExpensiveIO;
/// in the background before updating the UI back on the main queue.
- (void)onBackgroundQueue:(NSArray *(^)(void))backgroundBlock thenOnMainQueue:(void(^)(NSArray *))mainBlock;
/// Whether or not to display the "share" icon in the middle of the toolbar. NO by default.
@property (nonatomic) BOOL showsShareToolbarItem;
/// Called when the share button is pressed.
/// Default implementation does nothign. Subclasses may override.
- (void)shareButtonPressed;
/// 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.
- (void)disableToolbar;
@end
@@ -0,0 +1,502 @@
//
// FLEXTableViewController.m
// FLEX
//
// Created by Tanner on 7/5/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import "FLEXTableViewController.h"
#import "FLEXExplorerViewController.h"
#import "FLEXBookmarksViewController.h"
#import "FLEXTabsViewController.h"
#import "FLEXScopeCarousel.h"
#import "FLEXTableView.h"
#import "FLEXUtility.h"
#import "UIBarButtonItem+FLEX.h"
#import <objc/runtime.h>
@interface Block : NSObject
- (void)invoke;
@end
CGFloat const kFLEXDebounceInstant = 0.f;
CGFloat const kFLEXDebounceFast = 0.05;
CGFloat const kFLEXDebounceForAsyncSearch = 0.15;
CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
@interface FLEXTableViewController ()
@property (nonatomic) NSTimer *debounceTimer;
@property (nonatomic) BOOL didInitiallyRevealSearchBar;
@property (nonatomic) UITableViewStyle style;
@property (nonatomic, readonly) UIView *tableHeaderViewContainer;
@end
@implementation FLEXTableViewController
@synthesize tableHeaderViewContainer = _tableHeaderViewContainer;
@synthesize automaticallyShowsSearchBarCancelButton = _automaticallyShowsSearchBarCancelButton;
#pragma mark - Initialization
- (id)init {
#if FLEX_AT_LEAST_IOS13_SDK
if (@available(iOS 13.0, *)) {
self = [self initWithStyle:UITableViewStyleInsetGrouped];
} else {
self = [self initWithStyle:UITableViewStyleGrouped];
}
#else
self = [self initWithStyle:UITableViewStyleGrouped];
#endif
return self;
}
- (id)initWithStyle:(UITableViewStyle)style {
self = [super initWithStyle:style];
if (self) {
_searchBarDebounceInterval = kFLEXDebounceFast;
_showSearchBarInitially = YES;
_style = style;
}
return self;
}
#pragma mark - Public
- (FLEXWindow *)window {
return (id)self.view.window;
}
- (void)setShowsSearchBar:(BOOL)showsSearchBar {
if (_showsSearchBar == showsSearchBar) return;
_showsSearchBar = showsSearchBar;
if (showsSearchBar) {
UIViewController *results = self.searchResultsController;
self.searchController = [[UISearchController alloc] initWithSearchResultsController:results];
self.searchController.searchBar.placeholder = @"Filter";
self.searchController.searchResultsUpdater = (id)self;
self.searchController.delegate = (id)self;
self.searchController.dimsBackgroundDuringPresentation = NO;
self.searchController.hidesNavigationBarDuringPresentation = NO;
/// Not necessary in iOS 13; remove this when iOS 13 is the minimum deployment target
self.searchController.searchBar.delegate = self;
self.automaticallyShowsSearchBarCancelButton = YES;
#if FLEX_AT_LEAST_IOS13_SDK
if (@available(iOS 13, *)) {
self.searchController.automaticallyShowsScopeBar = NO;
}
#endif
[self addSearchController:self.searchController];
} else {
// Search already shown and just set to NO, so remove it
[self removeSearchController:self.searchController];
}
}
- (void)setShowsCarousel:(BOOL)showsCarousel {
if (_showsCarousel == showsCarousel) return;
_showsCarousel = showsCarousel;
if (showsCarousel) {
_carousel = ({
__weak __typeof(self) weakSelf = self;
FLEXScopeCarousel *carousel = [FLEXScopeCarousel new];
carousel.selectedIndexChangedAction = ^(NSInteger idx) {
__typeof(self) self = weakSelf;
[self updateSearchResults:self.searchText];
};
// UITableView won't update the header size unless you reset the header view
[carousel registerBlockForDynamicTypeChanges:^(FLEXScopeCarousel *carousel) {
__typeof(self) self = weakSelf;
[self layoutTableHeaderIfNeeded];
}];
carousel;
});
[self addCarousel:_carousel];
} else {
// Carousel already shown and just set to NO, so remove it
[self removeCarousel:_carousel];
}
}
- (NSInteger)selectedScope {
if (self.searchController.searchBar.showsScopeBar) {
return self.searchController.searchBar.selectedScopeButtonIndex;
} else if (self.showsCarousel) {
return self.carousel.selectedIndex;
} else {
return NSNotFound;
}
}
- (void)setSelectedScope:(NSInteger)selectedScope {
if (self.searchController.searchBar.showsScopeBar) {
self.searchController.searchBar.selectedScopeButtonIndex = selectedScope;
} else if (self.showsCarousel) {
self.carousel.selectedIndex = selectedScope;
}
[self updateSearchResults:self.searchText];
}
- (NSString *)searchText {
return self.searchController.searchBar.text;
}
- (BOOL)automaticallyShowsSearchBarCancelButton {
#if FLEX_AT_LEAST_IOS13_SDK
if (@available(iOS 13, *)) {
return self.searchController.automaticallyShowsCancelButton;
}
#endif
return _automaticallyShowsSearchBarCancelButton;
}
- (void)setAutomaticallyShowsSearchBarCancelButton:(BOOL)value {
#if FLEX_AT_LEAST_IOS13_SDK
if (@available(iOS 13, *)) {
self.searchController.automaticallyShowsCancelButton = value;
}
#endif
_automaticallyShowsSearchBarCancelButton = value;
}
- (void)updateSearchResults:(NSString *)newText { }
- (void)onBackgroundQueue:(NSArray *(^)(void))backgroundBlock thenOnMainQueue:(void(^)(NSArray *))mainBlock {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSArray *items = backgroundBlock();
dispatch_async(dispatch_get_main_queue(), ^{
mainBlock(items);
});
});
}
- (void)setsShowsShareToolbarItem:(BOOL)showsShareToolbarItem {
_showsShareToolbarItem = showsShareToolbarItem;
if (self.isViewLoaded) {
[self setupToolbarItems];
}
}
- (void)disableToolbar {
self.navigationController.toolbarHidden = YES;
self.navigationController.hidesBarsOnSwipe = NO;
self.toolbarItems = nil;
}
#pragma mark - View Controller Lifecycle
- (void)loadView {
self.view = [FLEXTableView style:self.style];
self.tableView.dataSource = self;
self.tableView.delegate = self;
}
- (void)viewDidLoad {
[super viewDidLoad];
self.tableView.keyboardDismissMode = UIScrollViewKeyboardDismissModeOnDrag;
// Toolbar
self.navigationController.toolbarHidden = NO;
self.navigationController.hidesBarsOnSwipe = YES;
// On iOS 13, the root view controller shows it's search bar no matter what.
// Turning this off avoids some weird flash the navigation bar does when we
// toggle navigationItem.hidesSearchBarWhenScrolling on and off. The flash
// will still happen on subsequent view controllers, but we can at least
// avoid it for the root view controller
if (@available(iOS 13, *)) {
if (self.navigationController.viewControllers.firstObject == self) {
_showSearchBarInitially = NO;
}
}
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
// When going back, make the search bar reappear instead of hiding
if (@available(iOS 11.0, *)) {
if ((self.pinSearchBar || self.showSearchBarInitially) && !self.didInitiallyRevealSearchBar) {
self.navigationItem.hidesSearchBarWhenScrolling = NO;
}
}
[self setupToolbarItems];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
// Allow scrolling to collapse the search bar, only if we don't want it pinned
if (@available(iOS 11.0, *)) {
if (self.showSearchBarInitially && !self.pinSearchBar && !self.didInitiallyRevealSearchBar) {
// All this mumbo jumbo is necessary to work around a bug in iOS 13 up to 13.2
// wherein quickly toggling navigationItem.hidesSearchBarWhenScrolling to make
// the search bar appear initially results in a bugged search bar that
// becomes transparent and floats over the screen as you scroll
[UIView animateWithDuration:0.2 animations:^{
self.navigationItem.hidesSearchBarWhenScrolling = YES;
[self.navigationController.view setNeedsLayout];
[self.navigationController.view layoutIfNeeded];
}];
}
}
// We only want to reveal the search bar when the view controller first appears.
self.didInitiallyRevealSearchBar = YES;
}
- (void)willMoveToParentViewController:(UIViewController *)parent {
[super willMoveToParentViewController:parent];
// Reset this since we are re-appearing under a new
// parent view controller and need to show it again
self.didInitiallyRevealSearchBar = NO;
}
#pragma mark - Private
- (void)setupToolbarItems {
UIBarButtonItem *emptySpaceOrShare = UIBarButtonItem.flex_fixedSpace;
if (self.showsShareToolbarItem) {
emptySpaceOrShare = FLEXBarButtonItemSystem(Action, self, @selector(shareButtonPressed));
}
self.toolbarItems = @[
UIBarButtonItem.flex_fixedSpace,
UIBarButtonItem.flex_flexibleSpace,
UIBarButtonItem.flex_fixedSpace,
UIBarButtonItem.flex_flexibleSpace,
UIBarButtonItem.flex_fixedSpace,
UIBarButtonItem.flex_flexibleSpace,
emptySpaceOrShare,
UIBarButtonItem.flex_flexibleSpace,
FLEXBarButtonItemSystem(Bookmarks, self, @selector(showBookmarks)),
UIBarButtonItem.flex_flexibleSpace,
FLEXBarButtonItemSystem(Organize, self, @selector(showTabSwitcher)),
];
// Disable tabs entirely when not presented by FLEXExplorerViewController
UIViewController *presenter = self.navigationController.presentingViewController;
if (![presenter isKindOfClass:[FLEXExplorerViewController class]]) {
self.toolbarItems.lastObject.enabled = NO;
}
}
- (void)debounce:(void(^)(void))block {
[self.debounceTimer invalidate];
self.debounceTimer = [NSTimer
scheduledTimerWithTimeInterval:self.searchBarDebounceInterval
target:block
selector:@selector(invoke)
userInfo:nil
repeats:NO
];
}
- (void)layoutTableHeaderIfNeeded {
if (self.showsCarousel) {
self.carousel.frame = FLEXRectSetHeight(
self.carousel.frame, self.carousel.intrinsicContentSize.height
);
}
self.tableView.tableHeaderView = self.tableView.tableHeaderView;
[self.tableView layoutIfNeeded];
}
- (void)addCarousel:(FLEXScopeCarousel *)carousel {
if (@available(iOS 11.0, *)) {
self.tableView.tableHeaderView = carousel;
} else {
carousel.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleTopMargin;
CGRect frame = self.tableHeaderViewContainer.frame;
CGRect subviewFrame = carousel.frame;
subviewFrame.origin.y = 0;
// Put the carousel below the search bar if it's already there
if (self.showsSearchBar) {
carousel.frame = subviewFrame = FLEXRectSetY(
subviewFrame, self.searchController.searchBar.frame.size.height
);
frame.size.height += carousel.intrinsicContentSize.height;
} else {
frame.size.height = carousel.intrinsicContentSize.height;
}
self.tableHeaderViewContainer.frame = frame;
[self.tableHeaderViewContainer addSubview:carousel];
}
[self layoutTableHeaderIfNeeded];
}
- (void)removeCarousel:(FLEXScopeCarousel *)carousel {
[carousel removeFromSuperview];
if (@available(iOS 11.0, *)) {
self.tableView.tableHeaderView = nil;
} else {
if (self.showsSearchBar) {
[self removeSearchController:self.searchController];
[self addSearchController:self.searchController];
} else {
self.tableView.tableHeaderView = nil;
_tableHeaderViewContainer = nil;
}
}
}
- (void)addSearchController:(UISearchController *)controller {
if (@available(iOS 11.0, *)) {
self.navigationItem.searchController = controller;
} else {
controller.searchBar.autoresizingMask |= UIViewAutoresizingFlexibleBottomMargin;
[self.tableHeaderViewContainer addSubview:controller.searchBar];
CGRect subviewFrame = controller.searchBar.frame;
CGRect frame = self.tableHeaderViewContainer.frame;
frame.size.width = MAX(frame.size.width, subviewFrame.size.width);
frame.size.height = subviewFrame.size.height;
// Move the carousel down if it's already there
if (self.showsCarousel) {
self.carousel.frame = FLEXRectSetY(
self.carousel.frame, subviewFrame.size.height
);
frame.size.height += self.carousel.frame.size.height;
}
self.tableHeaderViewContainer.frame = frame;
[self layoutTableHeaderIfNeeded];
}
}
- (void)removeSearchController:(UISearchController *)controller {
if (@available(iOS 11.0, *)) {
self.navigationItem.searchController = nil;
} else {
[controller.searchBar removeFromSuperview];
if (self.showsCarousel) {
// self.carousel.frame = FLEXRectRemake(CGPointZero, self.carousel.frame.size);
[self removeCarousel:self.carousel];
[self addCarousel:self.carousel];
} else {
self.tableView.tableHeaderView = nil;
_tableHeaderViewContainer = nil;
}
}
}
- (UIView *)tableHeaderViewContainer {
if (!_tableHeaderViewContainer) {
_tableHeaderViewContainer = [UIView new];
self.tableView.tableHeaderView = self.tableHeaderViewContainer;
}
return _tableHeaderViewContainer;
}
- (void)showBookmarks {
UINavigationController *nav = [[UINavigationController alloc]
initWithRootViewController:[FLEXBookmarksViewController new]
];
[self presentViewController:nav animated:YES completion:nil];
}
- (void)showTabSwitcher {
UINavigationController *nav = [[UINavigationController alloc]
initWithRootViewController:[FLEXTabsViewController new]
];
[self presentViewController:nav animated:YES completion:nil];
}
- (void)shareButtonPressed {
}
#pragma mark - Search Bar
#pragma mark UISearchResultsUpdating
- (void)updateSearchResultsForSearchController:(UISearchController *)searchController {
[self.debounceTimer invalidate];
NSString *text = searchController.searchBar.text;
void (^updateSearchResults)() = ^{
if (self.searchResultsUpdater) {
[self.searchResultsUpdater updateSearchResults:text];
} else {
[self updateSearchResults:text];
}
};
// Only debounce if we want to, and if we have a non-empty string
// Empty string events are sent instantly
if (text.length && self.searchBarDebounceInterval > kFLEXDebounceInstant) {
[self debounce:updateSearchResults];
} else {
updateSearchResults();
}
}
#pragma mark UISearchControllerDelegate
- (void)willPresentSearchController:(UISearchController *)searchController {
// Manually show cancel button for < iOS 13
if (!@available(iOS 13, *) && self.automaticallyShowsSearchBarCancelButton) {
[searchController.searchBar setShowsCancelButton:YES animated:YES];
}
}
- (void)willDismissSearchController:(UISearchController *)searchController {
// Manually hide cancel button for < iOS 13
if (!@available(iOS 13, *) && self.automaticallyShowsSearchBarCancelButton) {
[searchController.searchBar setShowsCancelButton:NO animated:YES];
}
}
#pragma mark UISearchBarDelegate
/// Not necessary in iOS 13; remove this when iOS 13 is the deployment target
- (void)searchBar:(UISearchBar *)searchBar selectedScopeButtonIndexDidChange:(NSInteger)selectedScope {
[self updateSearchResultsForSearchController:self.searchController];
}
#pragma mark Table view
/// Not having a title in the first section looks weird with a rounded-corner table view style
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
if (@available(iOS 13, *)) {
if (self.style == UITableViewStyleInsetGrouped) {
return @" ";
}
}
return nil; // For plain/gropued style
}
@end
+28
View File
@@ -0,0 +1,28 @@
//
// FLEXSingleRowSection.h
// FLEX
//
// Created by Tanner Bennett on 9/25/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import "FLEXTableViewSection.h"
/// A section providing a specific single row.
///
/// You may optionally provide a view controller to push when the row
/// is selected, or an action to perform when it is selected.
/// Which one is used first is up to the table view data source.
@interface FLEXSingleRowSection : FLEXTableViewSection
/// @param reuseIdentifier if nil, kFLEXDefaultCell is used.
+ (instancetype)title:(NSString *)sectionTitle
reuse:(NSString *)reuseIdentifier
cell:(void(^)(__kindof UITableViewCell *cell))cellConfiguration;
@property (nonatomic) UIViewController *pushOnSelection;
@property (nonatomic) void (^selectionAction)(UIViewController *host);
/// Called to determine whether the single row should display itself or not.
@property (nonatomic) BOOL (^filterMatcher)(NSString *filterText);
@end
+87
View File
@@ -0,0 +1,87 @@
//
// FLEXSingleRowSection.m
// FLEX
//
// Created by Tanner Bennett on 9/25/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import "FLEXSingleRowSection.h"
#import "FLEXTableView.h"
@interface FLEXSingleRowSection ()
@property (nonatomic, readonly) NSString *reuseIdentifier;
@property (nonatomic, readonly) void (^cellConfiguration)(__kindof UITableViewCell *cell);
@property (nonatomic) NSString *lastTitle;
@property (nonatomic) NSString *lastSubitle;
@end
@implementation FLEXSingleRowSection
#pragma mark - Public
+ (instancetype)title:(NSString *)title
reuse:(NSString *)reuse
cell:(void (^)(__kindof UITableViewCell *))config {
return [[self alloc] initWithTitle:title reuse:reuse cell:config];
}
- (id)initWithTitle:(NSString *)sectionTitle
reuse:(NSString *)reuseIdentifier
cell:(void (^)(__kindof UITableViewCell *))cellConfiguration {
self = [super init];
if (self) {
_title = sectionTitle;
_reuseIdentifier = reuseIdentifier ?: kFLEXDefaultCell;
_cellConfiguration = cellConfiguration;
}
return self;
}
#pragma mark - Overrides
- (NSInteger)numberOfRows {
if (self.filterMatcher && self.filterText.length) {
return self.filterMatcher(self.filterText) ? 1 : 0;
}
return 1;
}
- (BOOL)canSelectRow:(NSInteger)row {
return self.pushOnSelection || self.selectionAction;
}
- (void (^)(__kindof UIViewController *))didSelectRowAction:(NSInteger)row {
return self.selectionAction;
}
- (UIViewController *)viewControllerToPushForRow:(NSInteger)row {
return self.pushOnSelection;
}
- (NSString *)reuseIdentifierForRow:(NSInteger)row {
return self.reuseIdentifier;
}
- (void)configureCell:(__kindof UITableViewCell *)cell forRow:(NSInteger)row {
cell.textLabel.text = nil;
cell.detailTextLabel.text = nil;
cell.accessoryType = UITableViewCellAccessoryNone;
self.cellConfiguration(cell);
self.lastTitle = cell.textLabel.text;
self.lastSubitle = cell.detailTextLabel.text;
}
- (NSString *)titleForRow:(NSInteger)row {
return self.lastTitle;
}
- (NSString *)subtitleForRow:(NSInteger)row {
return self.lastSubitle;
}
@end
-285
View File
@@ -1,285 +0,0 @@
//
// FLEXTableViewController.m
// FLEX
//
// Created by Tanner on 7/5/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import "FLEXTableViewController.h"
#import "FLEXScopeCarousel.h"
#import "FLEXTableView.h"
#import "FLEXUtility.h"
#import <objc/runtime.h>
@interface Block : NSObject
- (void)invoke;
@end
CGFloat const kFLEXDebounceInstant = 0.f;
CGFloat const kFLEXDebounceFast = 0.05;
CGFloat const kFLEXDebounceForAsyncSearch = 0.15;
CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
@interface FLEXTableViewController ()
@property (nonatomic) NSTimer *debounceTimer;
@property (nonatomic) BOOL didInitiallyRevealSearchBar;
@end
@implementation FLEXTableViewController
@synthesize automaticallyShowsSearchBarCancelButton = _automaticallyShowsSearchBarCancelButton;
#pragma mark - Public
- (id)init {
#if FLEX_AT_LEAST_IOS13_SDK
if (@available(iOS 13.0, *)) {
self = [self initWithStyle:UITableViewStyleInsetGrouped];
} else {
self = [self initWithStyle:UITableViewStyleGrouped];
}
#else
self = [self initWithStyle:UITableViewStyleGrouped];
#endif
return self;
}
- (id)initWithStyle:(UITableViewStyle)style {
self = [super initWithStyle:style];
if (self) {
_searchBarDebounceInterval = kFLEXDebounceFast;
_showSearchBarInitially = YES;
}
return self;
}
- (void)setShowsSearchBar:(BOOL)showsSearchBar {
if (_showsSearchBar == showsSearchBar) return;
_showsSearchBar = showsSearchBar;
UIViewController *results = self.searchResultsController;
self.searchController = [[UISearchController alloc] initWithSearchResultsController:results];
self.searchController.searchBar.placeholder = @"Filter";
self.searchController.searchResultsUpdater = (id)self;
self.searchController.delegate = (id)self;
self.searchController.dimsBackgroundDuringPresentation = NO;
self.searchController.hidesNavigationBarDuringPresentation = NO;
/// Not necessary in iOS 13; remove this when iOS 13 is the minimum deployment target
self.searchController.searchBar.delegate = self;
self.automaticallyShowsSearchBarCancelButton = YES;
#if FLEX_AT_LEAST_IOS13_SDK
if (@available(iOS 13, *)) {
self.searchController.automaticallyShowsScopeBar = NO;
}
#endif
if (@available(iOS 11.0, *)) {
self.navigationItem.searchController = self.searchController;
} else {
self.tableView.tableHeaderView = self.searchController.searchBar;
}
}
- (void)setShowsCarousel:(BOOL)showsCarousel {
if (_showsCarousel == showsCarousel) return;
_showsCarousel = showsCarousel;
_carousel = ({
__weak __typeof(self) weakSelf = self;
FLEXScopeCarousel *carousel = [FLEXScopeCarousel new];
carousel.selectedIndexChangedAction = ^(NSInteger idx) {
__typeof(self) self = weakSelf;
[self updateSearchResults:self.searchText];
};
self.tableView.tableHeaderView = carousel;
[self.tableView layoutIfNeeded];
// UITableView won't update the header size unless you reset the header view
[carousel registerBlockForDynamicTypeChanges:^(FLEXScopeCarousel *carousel) {
__typeof(self) self = weakSelf;
self.tableView.tableHeaderView = carousel;
[self.tableView layoutIfNeeded];
}];
carousel;
});
}
- (NSInteger)selectedScope {
if (self.searchController.searchBar.showsScopeBar) {
return self.searchController.searchBar.selectedScopeButtonIndex;
} else if (self.showsCarousel) {
return self.carousel.selectedIndex;
} else {
return NSNotFound;
}
}
- (NSString *)searchText {
return self.searchController.searchBar.text;
}
- (BOOL)automaticallyShowsSearchBarCancelButton {
#if FLEX_AT_LEAST_IOS13_SDK
if (@available(iOS 13, *)) {
return self.searchController.automaticallyShowsCancelButton;
}
#endif
return _automaticallyShowsSearchBarCancelButton;
}
- (void)setAutomaticallyShowsSearchBarCancelButton:(BOOL)value {
#if FLEX_AT_LEAST_IOS13_SDK
if (@available(iOS 13, *)) {
self.searchController.automaticallyShowsCancelButton = value;
}
#endif
_automaticallyShowsSearchBarCancelButton = value;
}
- (void)updateSearchResults:(NSString *)newText { }
- (void)onBackgroundQueue:(NSArray *(^)(void))backgroundBlock thenOnMainQueue:(void(^)(NSArray *))mainBlock {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSArray *items = backgroundBlock();
dispatch_async(dispatch_get_main_queue(), ^{
mainBlock(items);
});
});
}
#pragma mark - View Controller Lifecycle
- (void)viewDidLoad {
[super viewDidLoad];
self.tableView.keyboardDismissMode = UIScrollViewKeyboardDismissModeOnDrag;
// On iOS 13, the root view controller shows it's search bar no matter what.
// Turning this off avoids some weird flash the navigation bar does when we
// toggle navigationItem.hidesSearchBarWhenScrolling on and off. The flash
// will still happen on subsequent view controllers, but we can at least
// avoid it for the root view controller
if (@available(iOS 13, *)) {
if (self.navigationController.viewControllers.firstObject == self) {
_showSearchBarInitially = NO;
}
}
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
// When going back, make the search bar reappear instead of hiding
if (@available(iOS 11.0, *)) {
if ((self.pinSearchBar || self.showSearchBarInitially) && !self.didInitiallyRevealSearchBar) {
self.navigationItem.hidesSearchBarWhenScrolling = NO;
}
}
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
// Allow scrolling to collapse the search bar, only if we don't want it pinned
if (@available(iOS 11.0, *)) {
if (self.showSearchBarInitially && !self.pinSearchBar && !self.didInitiallyRevealSearchBar) {
// All this mumbo jumbo is necessary to work around a bug in iOS 13 up to 13.2
// wherein quickly toggling navigationItem.hidesSearchBarWhenScrolling to make
// the search bar appear initially results in a bugged search bar that
// becomes transparent and floats over the screen as you scroll
[UIView animateWithDuration:0.2 animations:^{
self.navigationItem.hidesSearchBarWhenScrolling = YES;
[self.navigationController.view setNeedsLayout];
[self.navigationController.view layoutIfNeeded];
}];
}
}
// We only want to reveal the search bar when the view controller first appears.
self.didInitiallyRevealSearchBar = YES;
}
- (void)willMoveToParentViewController:(UIViewController *)parent {
[super willMoveToParentViewController:parent];
// Reset this since we are re-appearing under a new
// parent view controller and need to show it again
self.didInitiallyRevealSearchBar = NO;
}
#pragma mark - Private
- (void)debounce:(void(^)(void))block {
[self.debounceTimer invalidate];
self.debounceTimer = [NSTimer
scheduledTimerWithTimeInterval:self.searchBarDebounceInterval
target:block
selector:@selector(invoke)
userInfo:nil
repeats:NO
];
}
#pragma mark - Search Bar
#pragma mark UISearchResultsUpdating
- (void)updateSearchResultsForSearchController:(UISearchController *)searchController
{
[self.debounceTimer invalidate];
NSString *text = searchController.searchBar.text;
// Only debounce if we want to, and if we have a non-empty string
// Empty string events are sent instantly
if (text.length && self.searchBarDebounceInterval > kFLEXDebounceInstant) {
[self debounce:^{
[self updateSearchResults:text];
}];
} else {
[self updateSearchResults:text];
}
}
#pragma mark UISearchControllerDelegate
- (void)willPresentSearchController:(UISearchController *)searchController {
// Manually show cancel button for < iOS 13
if (!@available(iOS 13, *) && self.automaticallyShowsSearchBarCancelButton) {
[searchController.searchBar setShowsCancelButton:YES animated:YES];
}
}
- (void)willDismissSearchController:(UISearchController *)searchController {
// Manually hide cancel button for < iOS 13
if (!@available(iOS 13, *) && self.automaticallyShowsSearchBarCancelButton) {
[searchController.searchBar setShowsCancelButton:NO animated:YES];
}
}
#pragma mark UISearchBarDelegate
/// Not necessary in iOS 13; remove this when iOS 13 is the deployment target
- (void)searchBar:(UISearchBar *)searchBar selectedScopeButtonIndexDidChange:(NSInteger)selectedScope {
[self updateSearchResultsForSearchController:self.searchController];
}
#pragma mark Table view
/// Not having a title in the first section looks weird with a rounded-corner table view style
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
if (@available(iOS 13, *)) {
return @" "; // For inset grouped style
}
return nil; // For plain/gropued style
}
@end
+113 -20
View File
@@ -2,38 +2,131 @@
// FLEXTableViewSection.h
// FLEX
//
// Created by Tanner Bennett on 7/11/19.
// Copyright © 2019 Flipboard. All rights reserved.
// Created by Tanner on 1/29/20.
// Copyright © 2020 Flipboard. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "FLEXUtility.h"
@class FLEXTableView;
NS_ASSUME_NONNULL_BEGIN
/// A protocol for arbitrary case-insensitive pattern matching
@protocol FLEXPatternMatching <NSObject>
/// @return YES if the receiver matches the query, case-insensitive
- (BOOL)matches:(NSString *)query;
@end
#pragma mark FLEXTableViewSection
@interface FLEXTableViewSection<__covariant ObjectType> : NSObject
/// An abstract base class for table view sections.
///
/// Many properties or methods here return nil or some logical equivalent by default.
/// Even so, most of the methods with defaults are intended to be overriden by subclasses.
/// Some methods are not implemented at all and MUST be implemented by a subclass.
@interface FLEXTableViewSection : NSObject {
@protected
/// Unused by default, use if you want
NSString *_title;
}
+ (instancetype)section:(NSInteger)section title:(NSString *)title rows:(NSArray<ObjectType<FLEXPatternMatching>> *)rows;
#pragma mark - Data
@property (nonatomic, readonly) NSInteger section;
@property (nonatomic, readonly) NSString *title;
@property (nonatomic, readonly) NSArray<ObjectType<FLEXPatternMatching>> *rows;
/// A title to be displayed for the custom section.
/// Subclasses may override or use the \c _title ivar.
@property (nonatomic, readonly, nullable) 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;
/// A map of reuse identifiers to \c UITableViewCell (sub)class objects.
/// Subclasses \e may override this as necessary, but are not required to.
/// See \c FLEXTableView.h for more information.
/// @return nil by default.
@property (nonatomic, readonly, nullable) NSDictionary<NSString *, Class> *cellRegistrationMapping;
@property (nonatomic, readonly) NSInteger count;
/// The section should filter itself based on the contents of this property
/// as it is set. If it is set to nil or an empty string, it should not filter.
/// Subclasses should override or observe this property and react to changes.
///
/// It is common practice to use two arrays for the underlying model:
/// One to hold all rows, and one to hold unfiltered rows. When \c setFilterText:
/// is called, call \c super to store the new value, and re-filter your model accordingly.
@property (nonatomic, nullable) NSString *filterText;
/// @return A new section containing only rows that match the string,
/// or nil if the section was empty and no rows matched the string.
- (nullable instancetype)newSectionWithRowsMatchingQuery:(NSString *)query;
/// Provides an avenue for the section to refresh data or change the number of rows.
///
/// This is called before reloading the table view itself. If your section pulls data
/// from an external data source, this is a good place to refresh that data entirely.
/// If your section does not, then it might be simpler for you to just override
/// \c setFilterText: to call \c super and call \c reloadData.
- (void)reloadData;
@end
#pragma mark - Row Selection
/// Whether the given row should be selectable, such as if tapping the cell
/// should take the user to a new screen or trigger an action.
/// Subclasses \e may override this as necessary, but are not required to.
/// @return \c NO by default
- (BOOL)canSelectRow:(NSInteger)row;
/// An action "future" to be triggered when the row is selected, if the row
/// supports being selected as indicated by \c canSelectRow:. Subclasses
/// must implement this in accordance with how they implement \c canSelectRow:
/// if they do not implement \c viewControllerToPushForRow:
/// @return This returns \c nil if no view controller is provided by
/// \c viewControllerToPushForRow: — otherwise it pushes that view controller
/// onto \c host.navigationController
- (nullable void(^)(__kindof UIViewController *host))didSelectRowAction:(NSInteger)row;
/// A view controller to display when the row is selected, if the row
/// supports being selected as indicated by \c canSelectRow:. Subclasses
/// must implement this in accordance with how they implement \c canSelectRow:
/// if they do not implement \c didSelectRowAction:
/// @return \c nil by default
- (nullable UIViewController *)viewControllerToPushForRow:(NSInteger)row;
/// Called when the accessory view's detail button is pressed.
/// @return \c nil by default.
- (nullable void(^)(__kindof UIViewController *host))didPressInfoButtonAction:(NSInteger)row;
#pragma mark - Context Menus
#if FLEX_AT_LEAST_IOS13_SDK
/// By default, this is the title of the row.
/// @return The title of the context menu, if any.
- (nullable NSString *)menuTitleForRow:(NSInteger)row API_AVAILABLE(ios(13.0));
/// Protected, not intended for public use. \c menuTitleForRow:
/// already includes the value returned from this method.
///
/// By default, this returns \c @"". Subclasses may override to
/// provide a detailed description of the target of the context menu.
- (NSString *)menuSubtitleForRow:(NSInteger)row API_AVAILABLE(ios(13.0));
/// The context menu items, if any. Subclasses may override.
/// By default, only inludes items for \c copyMenuItemsForRow:.
- (nullable NSArray<UIMenuElement *> *)menuItemsForRow:(NSInteger)row sender:(UIViewController *)sender API_AVAILABLE(ios(13.0));
/// Subclasses may override to return a list of copiable items.
///
/// Every two elements in the list compose a key-value pair, where the key
/// should be a description of what will be copied, and the values should be
/// the strings to copy. Return an empty string as a value to show a disabled action.
- (nullable NSArray<NSString *> *)copyMenuItemsForRow:(NSInteger)row API_AVAILABLE(ios(13.0));
#endif
#pragma mark - Cell Configuration
/// Provide a reuse identifier for the given row. Subclasses should override.
///
/// Custom reuse identifiers should be specified in \c cellRegistrationMapping.
/// You may return any of the identifiers in \c FLEXTableView.h
/// without including them in the \c cellRegistrationMapping.
/// @return \c kFLEXDefaultCell by default.
- (NSString *)reuseIdentifierForRow:(NSInteger)row;
/// Configure a cell for the given row. Subclasses must override.
- (void)configureCell:(__kindof UITableViewCell *)cell forRow:(NSInteger)row;
#pragma mark - External Convenience
/// For use by whatever view controller uses your section. Not required.
/// @return An optional title.
- (nullable NSString *)titleForRow:(NSInteger)row;
/// For use by whatever view controller uses your section. Not required.
/// @return An optional subtitle.
- (nullable NSString *)subtitleForRow:(NSInteger)row;
@interface FLEXTableViewSection<__covariant ObjectType> (Subscripting)
- (ObjectType)objectAtIndexedSubscript:(NSUInteger)idx;
@end
NS_ASSUME_NONNULL_END
+108 -30
View File
@@ -2,48 +2,126 @@
// FLEXTableViewSection.m
// FLEX
//
// Created by Tanner Bennett on 7/11/19.
// Copyright © 2019 Flipboard. All rights reserved.
// Created by Tanner on 1/29/20.
// Copyright © 2020 Flipboard. All rights reserved.
//
#import "FLEXTableViewSection.h"
#import "FLEXTableView.h"
#import "UIMenu+FLEX.h"
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wincomplete-implementation"
@implementation FLEXTableViewSection
+ (instancetype)section:(NSInteger)section title:(NSString *)title rows:(NSArray *)rows {
FLEXTableViewSection *s = [self new];
s->_section = section;
s->_title = title;
s->_rows = rows.copy;
return s;
- (NSInteger)numberOfRows {
return 0;
}
- (instancetype)newSectionWithRowsMatchingQuery:(NSString *)query {
// Find rows containing the search string
NSPredicate *containsString = [NSPredicate predicateWithBlock:^BOOL(id<FLEXPatternMatching> obj, NSDictionary *bindings) {
return [obj matches:query];
}];
NSArray *filteredRows = [self.rows filteredArrayUsingPredicate:containsString];
// Only return new section if not empty
if (filteredRows.count) {
return [[self class] section:self.section title:self.title rows:filteredRows];
}
- (void)reloadData { }
- (NSDictionary<NSString *,Class> *)cellRegistrationMapping {
return nil;
}
- (NSInteger)count {
return self.rows.count;
- (BOOL)canSelectRow:(NSInteger)row { return NO; }
- (void (^)(__kindof UIViewController *))didSelectRowAction:(NSInteger)row {
UIViewController *toPush = [self viewControllerToPushForRow:row];
if (toPush) {
return ^(UIViewController *host) {
[host.navigationController pushViewController:toPush animated:YES];
};
}
return nil;
}
- (UIViewController *)viewControllerToPushForRow:(NSInteger)row {
return nil;
}
- (void (^)(__kindof UIViewController *))didPressInfoButtonAction:(NSInteger)row {
return nil;
}
- (NSString *)reuseIdentifierForRow:(NSInteger)row {
return kFLEXDefaultCell;
}
#if FLEX_AT_LEAST_IOS13_SDK
- (NSString *)menuTitleForRow:(NSInteger)row {
NSString *title = [self titleForRow:row];
NSString *subtitle = [self menuSubtitleForRow:row];
if (subtitle.length) {
return [NSString stringWithFormat:@"%@\n\n%@", title, subtitle];
}
return title;
}
- (NSString *)menuSubtitleForRow:(NSInteger)row {
return @"";
}
- (NSArray<UIMenuElement *> *)menuItemsForRow:(NSInteger)row sender:(UIViewController *)sender {
NSArray<NSString *> *copyItems = [self copyMenuItemsForRow:row];
NSAssert(copyItems.count % 2 == 0, @"copyMenuItemsForRow: should return an even list");
if (copyItems.count) {
NSInteger numberOfActions = copyItems.count / 2;
BOOL collapseMenu = numberOfActions > 4;
UIImage *copyIcon = [UIImage systemImageNamed:@"doc.on.doc"];
NSMutableArray *actions = [NSMutableArray new];
for (NSInteger i = 0; i < copyItems.count; i += 2) {
NSString *key = copyItems[i], *value = copyItems[i+1];
NSString *title = collapseMenu ? key : [@"Copy " stringByAppendingString:key];
UIAction *copy = [UIAction
actionWithTitle:title
image:copyIcon
identifier:nil
handler:^(__kindof UIAction *action) {
UIPasteboard.generalPasteboard.string = value;
}
];
if (!value.length) {
copy.attributes = UIMenuElementAttributesDisabled;
}
[actions addObject:copy];
}
UIMenu *copyMenu = [UIMenu
inlineMenuWithTitle:@"Copy…"
image:copyIcon
children:actions
];
if (collapseMenu) {
return @[[copyMenu collapsed]];
} else {
return @[copyMenu];
}
}
return @[];
}
#endif
- (NSArray<NSString *> *)copyMenuItemsForRow:(NSInteger)row {
return nil;
}
- (NSString *)titleForRow:(NSInteger)row { return nil; }
- (NSString *)subtitleForRow:(NSInteger)row { return nil; }
@end
@implementation FLEXTableViewSection (Subscripting)
- (id)objectAtIndexedSubscript:(NSUInteger)idx {
return self.rows[idx];
}
@end
#pragma clang diagnostic pop
@@ -17,7 +17,6 @@ NSString * const kCarouselCellReuseIdentifier = @"kCarouselCellReuseIdentifier";
@interface FLEXScopeCarousel () <UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout>
@property (nonatomic, readonly) UICollectionView *collectionView;
@property (nonatomic, readonly) FLEXCarouselCell *sizingCell;
@property (nonatomic, readonly) NSLayoutConstraint *heightConstraint;
@property (nonatomic, readonly) id dynamicTypeObserver;
@property (nonatomic, readonly) NSMutableArray *dynamicTypeHandlers;
@@ -32,6 +31,7 @@ NSString * const kCarouselCellReuseIdentifier = @"kCarouselCellReuseIdentifier";
if (self) {
self.backgroundColor = [FLEXColor primaryBackgroundColor];
self.autoresizingMask = UIViewAutoresizingFlexibleWidth;
self.translatesAutoresizingMaskIntoConstraints = YES;
_dynamicTypeHandlers = [NSMutableArray new];
CGSize itemSize = CGSizeZero;
@@ -117,25 +117,22 @@ NSString * const kCarouselCellReuseIdentifier = @"kCarouselCellReuseIdentifier";
- (void)updateConstraints {
if (!self.constraintsInstalled) {
self.translatesAutoresizingMaskIntoConstraints = NO;
self.collectionView.translatesAutoresizingMaskIntoConstraints = NO;
[self.centerXAnchor constraintEqualToAnchor:self.superview.centerXAnchor].active = YES;
[self.widthAnchor constraintEqualToAnchor:self.superview.widthAnchor].active = YES;
[self.topAnchor constraintEqualToAnchor:self.superview.topAnchor].active = YES;
[self.collectionView pinEdgesToSuperview];
_heightConstraint = [self.heightAnchor constraintEqualToConstant:100];
self.heightConstraint.active = YES;
self.constraintsInstalled = YES;
}
self.heightConstraint.constant = [self.sizingCell systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
[super updateConstraints];
}
- (CGSize)intrinsicContentSize {
return CGSizeMake(
UIViewNoIntrinsicMetric,
[self.sizingCell systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height
);
}
#pragma mark - Public
- (void)setItems:(NSArray<NSString *> *)items {
@@ -0,0 +1,17 @@
//
// FLEXCodeFontCell.h
// FLEX
//
// Created by Tanner on 12/27/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import "FLEXMultilineTableViewCell.h"
NS_ASSUME_NONNULL_BEGIN
@interface FLEXCodeFontCell : FLEXMultilineDetailTableViewCell
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,34 @@
//
// FLEXCodeFontCell.m
// FLEX
//
// Created by Tanner on 12/27/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import "FLEXCodeFontCell.h"
#import "UIFont+FLEX.h"
@implementation FLEXCodeFontCell
- (void)postInit {
[super postInit];
self.titleLabel.font = UIFont.flex_codeFont;
self.subtitleLabel.font = UIFont.flex_codeFont;
self.titleLabel.adjustsFontSizeToFitWidth = YES;
self.titleLabel.minimumScaleFactor = 0.9;
self.subtitleLabel.adjustsFontSizeToFitWidth = YES;
self.subtitleLabel.minimumScaleFactor = 0.75;
// Disable mutli-line pre iOS 11
if (@available(iOS 11, *)) {
self.subtitleLabel.numberOfLines = 5;
} else {
self.titleLabel.numberOfLines = 1;
self.subtitleLabel.numberOfLines = 1;
}
}
@end
@@ -0,0 +1,13 @@
//
// FLEXKeyValueTableViewCell.h
// FLEX
//
// Created by Tanner Bennett on 1/23/20.
// Copyright © 2020 Flipboard. All rights reserved.
//
#import "FLEXTableViewCell.h"
@interface FLEXKeyValueTableViewCell : FLEXTableViewCell
@end
@@ -0,0 +1,17 @@
//
// FLEXKeyValueTableViewCell.m
// FLEX
//
// Created by Tanner Bennett on 1/23/20.
// Copyright © 2020 Flipboard. All rights reserved.
//
#import "FLEXKeyValueTableViewCell.h"
@implementation FLEXKeyValueTableViewCell
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
return [super initWithStyle:UITableViewCellStyleValue1 reuseIdentifier:reuseIdentifier];
}
@end
@@ -11,7 +11,10 @@
/// A cell with both labels set to be multi-line capable.
@interface FLEXMultilineTableViewCell : FLEXTableViewCell
+ (CGFloat)preferredHeightWithAttributedText:(NSAttributedString *)attributedText inTableViewWidth:(CGFloat)tableViewWidth style:(UITableViewStyle)style showsAccessory:(BOOL)showsAccessory;
+ (CGFloat)preferredHeightWithAttributedText:(NSAttributedString *)attributedText
maxWidth:(CGFloat)contentViewWidth
style:(UITableViewStyle)style
showsAccessory:(BOOL)showsAccessory;
@end
@@ -7,34 +7,33 @@
//
#import "FLEXMultilineTableViewCell.h"
#import "UIView+FLEX_Layout.h"
#import "FLEXUtility.h"
@interface FLEXMultilineTableViewCell ()
@property (nonatomic, readonly) UILabel *_titleLabel;
@property (nonatomic, readonly) UILabel *_subtitleLabel;
@property (nonatomic) BOOL constraintsUpdated;
@end
@implementation FLEXMultilineTableViewCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
self.textLabel.numberOfLines = 0;
self.detailTextLabel.numberOfLines = 0;
}
return self;
- (void)postInit {
[super postInit];
self.titleLabel.numberOfLines = 0;
self.subtitleLabel.numberOfLines = 0;
}
- (void)layoutSubviews
{
[super layoutSubviews];
self.textLabel.frame = UIEdgeInsetsInsetRect(self.contentView.bounds, [[self class] labelInsets]);
}
+ (UIEdgeInsets)labelInsets
{
+ (UIEdgeInsets)labelInsets {
return UIEdgeInsetsMake(10.0, 15.0, 10.0, 15.0);
}
+ (CGFloat)preferredHeightWithAttributedText:(NSAttributedString *)attributedText inTableViewWidth:(CGFloat)tableViewWidth style:(UITableViewStyle)style showsAccessory:(BOOL)showsAccessory
{
CGFloat labelWidth = tableViewWidth;
+ (CGFloat)preferredHeightWithAttributedText:(NSAttributedString *)attributedText
maxWidth:(CGFloat)contentViewWidth
style:(UITableViewStyle)style
showsAccessory:(BOOL)showsAccessory {
CGFloat labelWidth = contentViewWidth;
// Content view inset due to accessory view observed on iOS 8.1 iPhone 6.
if (showsAccessory) {
@@ -45,7 +44,12 @@
labelWidth -= (labelInsets.left + labelInsets.right);
CGSize constrainSize = CGSizeMake(labelWidth, CGFLOAT_MAX);
CGFloat preferredLabelHeight = ceil([attributedText boundingRectWithSize:constrainSize options:NSStringDrawingUsesLineFragmentOrigin context:nil].size.height);
CGRect boundingBox = [attributedText
boundingRectWithSize:constrainSize
options:NSStringDrawingUsesLineFragmentOrigin
context:nil
];
CGFloat preferredLabelHeight = FLEXFloor(boundingBox.size.height);
CGFloat preferredCellHeight = preferredLabelHeight + labelInsets.top + labelInsets.bottom + 1.0;
return preferredCellHeight;
@@ -53,10 +57,10 @@
@end
@implementation FLEXMultilineDetailTableViewCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
return [super initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:reuseIdentifier];
}
@@ -6,9 +6,9 @@
// Copyright © 2019 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "FLEXTableViewCell.h"
/// A cell initialized with \c UITableViewCellStyleSubtitle
@interface FLEXSubtitleTableViewCell : UITableViewCell
@interface FLEXSubtitleTableViewCell : FLEXTableViewCell
@end
@@ -10,8 +10,7 @@
@implementation FLEXSubtitleTableViewCell
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
return [super initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:reuseIdentifier];
}
@@ -0,0 +1,23 @@
//
// FLEXTableViewCell.h
// FLEX
//
// Created by Tanner on 4/17/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface FLEXTableViewCell : UITableViewCell
/// Use this instead of .textLabel
@property (nonatomic, readonly) UILabel *titleLabel;
/// Use this instead of .detailTextLabel
@property (nonatomic, readonly) UILabel *subtitleLabel;
/// Subclasses can override this instead of initializers to
/// perform additional initialization without lots of boilerplate.
/// Remember to call super!
- (void)postInit;
@end
@@ -8,6 +8,7 @@
#import "FLEXTableViewCell.h"
#import "FLEXUtility.h"
#import "FLEXColor.h"
#import "FLEXTableView.h"
@interface UITableView (Internal)
@@ -23,21 +24,37 @@
@implementation FLEXTableViewCell
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier
{
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
UIFont *cellFont = [FLEXUtility defaultTableViewCellLabelFont];
self.textLabel.font = cellFont;
self.detailTextLabel.font = cellFont;
self.detailTextLabel.textColor = UIColor.grayColor;
[self postInit];
}
return self;
}
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender
{
- (void)postInit {
UIFont *cellFont = UIFont.flex_defaultTableCellFont;
self.titleLabel.font = cellFont;
self.subtitleLabel.font = cellFont;
self.subtitleLabel.textColor = [FLEXColor deemphasizedTextColor];
self.titleLabel.lineBreakMode = NSLineBreakByTruncatingMiddle;
self.subtitleLabel.lineBreakMode = NSLineBreakByTruncatingMiddle;
self.titleLabel.numberOfLines = 1;
self.subtitleLabel.numberOfLines = 1;
}
- (UILabel *)titleLabel {
return self.textLabel;
}
- (UILabel *)subtitleLabel {
return self.detailTextLabel;
}
- (BOOL)canPerformAction:(SEL)action withSender:(id)sender {
return [self._tableView _canPerformAction:action forCell:self sender:sender];
}
@@ -50,8 +67,7 @@
/// sake. I see this as "fixing" a poorly designed API.
/// That approach would require lots of boilerplate to
/// make the menu appear above this cell.
- (void)forwardInvocation:(NSInvocation *)invocation
{
- (void)forwardInvocation:(NSInvocation *)invocation {
// Must be unretained to avoid over-releasing
__unsafe_unretained id sender;
[invocation getArgument:&sender atIndex:2];
@@ -65,8 +81,7 @@
[invocation invokeWithTarget:self._tableView];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector
{
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
if ([self canPerformAction:selector withSender:nil]) {
return [self._tableView methodSignatureForSelector:@selector(_performAction:forCell:sender:)];
}
@@ -12,7 +12,7 @@
typedef NSString * FLEXTableViewCellReuseIdentifier;
/// A regular \c UITableViewCell initialized with \c UITableViewCellStyleDefault
/// A regular \c FLEXTableViewCell initialized with \c UITableViewCellStyleDefault
extern FLEXTableViewCellReuseIdentifier const kFLEXDefaultCell;
/// A \c FLEXSubtitleTableViewCell initialized with \c UITableViewCellStyleSubtitle
extern FLEXTableViewCellReuseIdentifier const kFLEXDetailCell;
@@ -20,6 +20,10 @@ extern FLEXTableViewCellReuseIdentifier const kFLEXDetailCell;
extern FLEXTableViewCellReuseIdentifier const kFLEXMultilineCell;
/// A \c FLEXMultilineTableViewCell initialized with \c UITableViewCellStyleSubtitle
extern FLEXTableViewCellReuseIdentifier const kFLEXMultilineDetailCell;
/// A \c FLEXTableViewCell initialized with \c UITableViewCellStyleValue1
extern FLEXTableViewCellReuseIdentifier const kFLEXKeyValueCell;
/// A \c FLEXSubtitleTableViewCell which uses monospaced fonts for both labels
extern FLEXTableViewCellReuseIdentifier const kFLEXCodeFontCell;
#pragma mark - FLEXTableView
@interface FLEXTableView : UITableView
@@ -27,6 +31,7 @@ extern FLEXTableViewCellReuseIdentifier const kFLEXMultilineDetailCell;
+ (instancetype)flexDefaultTableView;
+ (instancetype)groupedTableView;
+ (instancetype)plainTableView;
+ (instancetype)style:(UITableViewStyle)style;
/// You do not need to register classes for any of the default reuse identifiers above
/// (annotated as \c FLEXTableViewCellReuseIdentifier types) unless you wish to provide
@@ -10,11 +10,15 @@
#import "FLEXUtility.h"
#import "FLEXSubtitleTableViewCell.h"
#import "FLEXMultilineTableViewCell.h"
#import "FLEXKeyValueTableViewCell.h"
#import "FLEXCodeFontCell.h"
FLEXTableViewCellReuseIdentifier const kFLEXDefaultCell = @"kFLEXDefaultCell";
FLEXTableViewCellReuseIdentifier const kFLEXDetailCell = @"kFLEXDetailCell";
FLEXTableViewCellReuseIdentifier const kFLEXMultilineCell = @"kFLEXMultilineCell";
FLEXTableViewCellReuseIdentifier const kFLEXMultilineDetailCell = @"kFLEXMultilineDetailCell";
FLEXTableViewCellReuseIdentifier const kFLEXKeyValueCell = @"kFLEXKeyValueCell";
FLEXTableViewCellReuseIdentifier const kFLEXCodeFontCell = @"kFLEXCodeFontCell";
#pragma mark Private
@@ -39,12 +43,22 @@ FLEXTableViewCellReuseIdentifier const kFLEXMultilineDetailCell = @"kFLEXMultili
- (CGFloat)_heightForHeaderInSection:(NSInteger)section {
CGFloat height = [super _heightForHeaderInSection:section];
if (section == 0 && self.tableHeaderView) {
if (section == 0) {
NSString *title = [self _titleForHeaderInSection:section];
if (!@available(iOS 13, *)) {
return height - self.tableHeaderView.frame.size.height + 8;
} else if ([title isEqualToString:@" "]) {
return height - self.tableHeaderView.frame.size.height + 5;
if (self.tableHeaderView) {
if (!@available(iOS 13, *)) {
return height - self.tableHeaderView.frame.size.height + 8;
}
// On iOS 13, returning an empty title for the table view
// messes with the height of the table view later on.
// We return a space to work around this.
else if ([title isEqualToString:@" "]) {
return height - self.tableHeaderView.frame.size.height + 5;
}
} else {
if (@available(iOS 13, *) && [title isEqualToString:@" "]) {
return 5;
}
}
}
@@ -69,6 +83,10 @@ FLEXTableViewCellReuseIdentifier const kFLEXMultilineDetailCell = @"kFLEXMultili
return [[self alloc] initWithFrame:CGRectZero style:UITableViewStylePlain];
}
+ (id)style:(UITableViewStyle)style {
return [[self alloc] initWithFrame:CGRectZero style:style];
}
- (id)initWithFrame:(CGRect)frame style:(UITableViewStyle)style {
self = [super initWithFrame:frame style:style];
if (self) {
@@ -76,13 +94,16 @@ FLEXTableViewCellReuseIdentifier const kFLEXMultilineDetailCell = @"kFLEXMultili
kFLEXDefaultCell : [FLEXTableViewCell class],
kFLEXDetailCell : [FLEXSubtitleTableViewCell class],
kFLEXMultilineCell : [FLEXMultilineTableViewCell class],
kFLEXMultilineDetailCell : [FLEXMultilineDetailTableViewCell class]
kFLEXMultilineDetailCell : [FLEXMultilineDetailTableViewCell class],
kFLEXKeyValueCell : [FLEXKeyValueTableViewCell class],
kFLEXCodeFontCell : [FLEXCodeFontCell class],
}];
}
return self;
}
#pragma mark - Public
- (void)registerCells:(NSDictionary<NSString*, Class> *)registrationMapping {
@@ -30,18 +30,16 @@
@implementation FLEXColorComponentInputView
- (id)initWithFrame:(CGRect)frame
{
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.slider = [UISlider new];
self.slider.backgroundColor = self.backgroundColor;
[self.slider addTarget:self action:@selector(sliderChanged:) forControlEvents:UIControlEventValueChanged];
[self addSubview:self.slider];
self.valueLabel = [UILabel new];
self.valueLabel.backgroundColor = self.backgroundColor;
self.valueLabel.font = [FLEXUtility defaultFontOfSize:14.0];
self.valueLabel.font = [UIFont systemFontOfSize:14.0];
self.valueLabel.textAlignment = NSTextAlignmentRight;
[self addSubview:self.valueLabel];
@@ -50,15 +48,13 @@
return self;
}
- (void)setBackgroundColor:(UIColor *)backgroundColor
{
- (void)setBackgroundColor:(UIColor *)backgroundColor {
[super setBackgroundColor:backgroundColor];
self.slider.backgroundColor = backgroundColor;
self.valueLabel.backgroundColor = backgroundColor;
}
- (void)layoutSubviews
{
- (void)layoutSubviews {
[super layoutSubviews];
const CGFloat kValueLabelWidth = 50.0;
@@ -73,19 +69,16 @@
self.valueLabel.frame = CGRectMake(valueLabelOriginX, valueLabelOriginY, kValueLabelWidth, self.valueLabel.frame.size.height);
}
- (void)sliderChanged:(id)sender
{
- (void)sliderChanged:(id)sender {
[self.delegate colorComponentInputViewValueDidChange:self];
[self updateValueLabel];
}
- (void)updateValueLabel
{
- (void)updateValueLabel {
self.valueLabel.text = [NSString stringWithFormat:@"%.3f", self.slider.value];
}
- (CGSize)sizeThatFits:(CGSize)size
{
- (CGSize)sizeThatFits:(CGSize)size {
CGFloat height = [self.slider sizeThatFits:size].height;
return CGSizeMake(size.width, height);
}
@@ -102,8 +95,7 @@
@implementation FLEXColorPreviewBox
- (id)initWithFrame:(CGRect)frame
{
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.layer.borderWidth = 1.0;
@@ -118,18 +110,15 @@
return self;
}
- (void)setColor:(UIColor *)color
{
- (void)setColor:(UIColor *)color {
self.colorOverlayView.backgroundColor = color;
}
- (UIColor *)color
{
- (UIColor *)color {
return self.colorOverlayView.backgroundColor;
}
+ (UIImage *)backgroundPatternImage
{
+ (UIImage *)backgroundPatternImage {
const CGFloat kSquareDimension = 5.0;
CGSize squareSize = CGSizeMake(kSquareDimension, kSquareDimension);
CGSize imageSize = CGSizeMake(2.0 * kSquareDimension, 2.0 * kSquareDimension);
@@ -164,8 +153,7 @@
@implementation FLEXArgumentInputColorView
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
{
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
self = [super initWithArgumentTypeEncoding:typeEncoding];
if (self) {
self.colorPreviewBox = [FLEXColorPreviewBox new];
@@ -174,7 +162,7 @@
self.hexLabel = [UILabel new];
self.hexLabel.backgroundColor = [UIColor colorWithWhite:1.0 alpha:0.9];
self.hexLabel.textAlignment = NSTextAlignmentCenter;
self.hexLabel.font = [FLEXUtility defaultFontOfSize:12.0];
self.hexLabel.font = [UIFont systemFontOfSize:12.0];
[self addSubview:self.hexLabel];
self.alphaInput = [FLEXColorComponentInputView new];
@@ -200,8 +188,7 @@
return self;
}
- (void)setBackgroundColor:(UIColor *)backgroundColor
{
- (void)setBackgroundColor:(UIColor *)backgroundColor {
[super setBackgroundColor:backgroundColor];
self.alphaInput.backgroundColor = backgroundColor;
self.redInput.backgroundColor = backgroundColor;
@@ -209,8 +196,7 @@
self.blueInput.backgroundColor = backgroundColor;
}
- (void)layoutSubviews
{
- (void)layoutSubviews {
[super layoutSubviews];
CGFloat runningOriginY = 0;
@@ -236,8 +222,7 @@
}
}
- (void)setInputValue:(id)inputValue
{
- (void)setInputValue:(id)inputValue {
if ([inputValue isKindOfClass:[UIColor class]]) {
[self updateWithColor:inputValue];
} else if ([inputValue isKindOfClass:[NSValue class]]) {
@@ -248,21 +233,20 @@
UIColor *color = [[UIColor alloc] initWithCGColor:colorRef];
[self updateWithColor:color];
}
} else {
[self updateWithColor:[UIColor clearColor]];
}
}
- (id)inputValue
{
- (id)inputValue {
return [UIColor colorWithRed:self.redInput.slider.value green:self.greenInput.slider.value blue:self.blueInput.slider.value alpha:self.alphaInput.slider.value];
}
- (void)colorComponentInputViewValueDidChange:(FLEXColorComponentInputView *)colorComponentInputView
{
- (void)colorComponentInputViewValueDidChange:(FLEXColorComponentInputView *)colorComponentInputView {
[self updateColorPreview];
}
- (void)updateWithColor:(UIColor *)color
{
- (void)updateWithColor:(UIColor *)color {
CGFloat red, green, blue, white, alpha;
if ([color getRed:&red green:&green blue:&blue alpha:&alpha]) {
self.alphaInput.slider.value = alpha;
@@ -286,8 +270,7 @@
[self updateColorPreview];
}
- (void)updateColorPreview
{
- (void)updateColorPreview {
self.colorPreviewBox.color = self.inputValue;
unsigned char redByte = self.redInput.slider.value * 255;
unsigned char greenByte = self.greenInput.slider.value * 255;
@@ -296,8 +279,7 @@
[self setNeedsLayout];
}
- (CGSize)sizeThatFits:(CGSize)size
{
- (CGSize)sizeThatFits:(CGSize)size {
CGFloat height = 0;
height += [[self class] colorPreviewBoxHeight];
height += [[self class] inputViewVerticalPadding];
@@ -311,19 +293,19 @@
return CGSizeMake(size.width, height);
}
+ (CGFloat)inputViewVerticalPadding
{
+ (CGFloat)inputViewVerticalPadding {
return 10.0;
}
+ (CGFloat)colorPreviewBoxHeight
{
+ (CGFloat)colorPreviewBoxHeight {
return 40.0;
}
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
{
return (type && (strcmp(type, @encode(CGColorRef)) == 0 || strcmp(type, FLEXEncodeClass(UIColor)) == 0)) || [value isKindOfClass:[UIColor class]];
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
NSParameterAssert(type);
// We don't care if currentValue is a color or not; we will default to +clearColor
return (strcmp(type, @encode(CGColorRef)) == 0) || (strcmp(type, FLEXEncodeClass(UIColor)) == 0);
}
@end
@@ -17,8 +17,7 @@
@implementation FLEXArgumentInputDateView
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
{
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
self = [super initWithArgumentTypeEncoding:typeEncoding];
if (self) {
self.datePicker = [UIDatePicker new];
@@ -31,33 +30,29 @@
return self;
}
- (void)setInputValue:(id)inputValue
{
- (void)setInputValue:(id)inputValue {
if ([inputValue isKindOfClass:[NSDate class]]) {
self.datePicker.date = inputValue;
}
}
- (id)inputValue
{
- (id)inputValue {
return self.datePicker.date;
}
- (void)layoutSubviews
{
- (void)layoutSubviews {
[super layoutSubviews];
self.datePicker.frame = self.bounds;
}
- (CGSize)sizeThatFits:(CGSize)size
{
- (CGSize)sizeThatFits:(CGSize)size {
CGFloat height = [self.datePicker sizeThatFits:size].height;
return CGSizeMake(size.width, height);
}
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
{
return (type && (strcmp(type, FLEXEncodeClass(NSDate)) == 0)) || [value isKindOfClass:[NSDate class]];
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
NSParameterAssert(type);
return strcmp(type, FLEXEncodeClass(NSDate)) == 0;
}
@end
@@ -20,18 +20,15 @@
@implementation FLEXArgumentInputFontView
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
{
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
self = [super initWithArgumentTypeEncoding:typeEncoding];
if (self) {
self.fontNameInput = [[FLEXArgumentInputFontsPickerView alloc] initWithArgumentTypeEncoding:FLEXEncodeClass(NSString)];
self.fontNameInput.backgroundColor = self.backgroundColor;
self.fontNameInput.targetSize = FLEXArgumentInputViewSizeSmall;
self.fontNameInput.title = @"Font Name:";
[self addSubview:self.fontNameInput];
self.pointSizeInput = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:@encode(CGFloat)];
self.pointSizeInput.backgroundColor = self.backgroundColor;
self.pointSizeInput.targetSize = FLEXArgumentInputViewSizeSmall;
self.pointSizeInput.title = @"Point Size:";
[self addSubview:self.pointSizeInput];
@@ -39,15 +36,13 @@
return self;
}
- (void)setBackgroundColor:(UIColor *)backgroundColor
{
- (void)setBackgroundColor:(UIColor *)backgroundColor {
[super setBackgroundColor:backgroundColor];
self.fontNameInput.backgroundColor = backgroundColor;
self.pointSizeInput.backgroundColor = backgroundColor;
}
- (void)setInputValue:(id)inputValue
{
- (void)setInputValue:(id)inputValue {
if ([inputValue isKindOfClass:[UIFont class]]) {
UIFont *font = (UIFont *)inputValue;
self.fontNameInput.inputValue = font.fontName;
@@ -55,8 +50,7 @@
}
}
- (id)inputValue
{
- (id)inputValue {
CGFloat pointSize = 0;
if ([self.pointSizeInput.inputValue isKindOfClass:[NSValue class]]) {
NSValue *pointSizeValue = (NSValue *)self.pointSizeInput.inputValue;
@@ -67,16 +61,14 @@
return [UIFont fontWithName:self.fontNameInput.inputValue size:pointSize];
}
- (BOOL)inputViewIsFirstResponder
{
- (BOOL)inputViewIsFirstResponder {
return [self.fontNameInput inputViewIsFirstResponder] || [self.pointSizeInput inputViewIsFirstResponder];
}
#pragma mark - Layout and Sizing
- (void)layoutSubviews
{
- (void)layoutSubviews {
[super layoutSubviews];
CGFloat runningOriginY = self.topInputFieldVerticalLayoutGuide;
@@ -89,13 +81,11 @@
self.pointSizeInput.frame = CGRectMake(0, runningOriginY, pointSizeFitSize.width, pointSizeFitSize.height);
}
+ (CGFloat)verticalPaddingBetweenFields
{
+ (CGFloat)verticalPaddingBetweenFields {
return 10.0;
}
- (CGSize)sizeThatFits:(CGSize)size
{
- (CGSize)sizeThatFits:(CGSize)size {
CGSize fitSize = [super sizeThatFits:size];
CGSize constrainSize = CGSizeMake(size.width, CGFLOAT_MAX);
@@ -111,11 +101,9 @@
#pragma mark -
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
{
BOOL supported = type && strcmp(type, FLEXEncodeClass(UIFont)) == 0;
supported = supported || (value && [value isKindOfClass:[UIFont class]]);
return supported;
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
NSParameterAssert(type);
return strcmp(type, FLEXEncodeClass(UIFont)) == 0;
}
@end
@@ -18,8 +18,7 @@
@implementation FLEXArgumentInputFontsPickerView
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
{
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
self = [super initWithArgumentTypeEncoding:typeEncoding];
if (self) {
self.targetSize = FLEXArgumentInputViewSizeSmall;
@@ -29,8 +28,7 @@
return self;
}
- (void)setInputValue:(id)inputValue
{
- (void)setInputValue:(id)inputValue {
self.inputTextView.text = inputValue;
if ([self.availableFonts indexOfObject:inputValue] == NSNotFound) {
[self.availableFonts insertObject:inputValue atIndex:0];
@@ -38,15 +36,13 @@
[(UIPickerView *)self.inputTextView.inputView selectRow:[self.availableFonts indexOfObject:inputValue] inComponent:0 animated:NO];
}
- (id)inputValue
{
- (id)inputValue {
return self.inputTextView.text.length > 0 ? [self.inputTextView.text copy] : nil;
}
#pragma mark - private
- (UIPickerView*)createFontsPicker
{
- (UIPickerView*)createFontsPicker {
UIPickerView *fontsPicker = [UIPickerView new];
fontsPicker.dataSource = self;
fontsPicker.delegate = self;
@@ -54,8 +50,7 @@
return fontsPicker;
}
- (void)createAvailableFonts
{
- (void)createAvailableFonts {
NSMutableArray<NSString *> *unsortedFontsArray = [NSMutableArray array];
for (NSString *eachFontFamily in [UIFont familyNames]) {
for (NSString *eachFontName in [UIFont fontNamesForFamilyName:eachFontFamily]) {
@@ -67,20 +62,17 @@
#pragma mark - UIPickerViewDataSource
- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView
{
- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView {
return 1;
}
- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component
{
- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component {
return self.availableFonts.count;
}
#pragma mark - UIPickerViewDelegate
- (UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(UIView *)view
{
- (UIView *)pickerView:(UIPickerView *)pickerView viewForRow:(NSInteger)row forComponent:(NSInteger)component reusingView:(UIView *)view {
UILabel *fontLabel;
if (!view) {
fontLabel = [UILabel new];
@@ -97,8 +89,7 @@
return fontLabel;
}
- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component
{
- (void)pickerView:(UIPickerView *)pickerView didSelectRow:(NSInteger)row inComponent:(NSInteger)component {
self.inputTextView.text = self.availableFonts[row];
}
@@ -7,16 +7,16 @@
//
#import "FLEXArgumentInputNotSupportedView.h"
#import "FLEXColor.h"
@implementation FLEXArgumentInputNotSupportedView
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
{
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
self = [super initWithArgumentTypeEncoding:typeEncoding];
if (self) {
self.inputTextView.userInteractionEnabled = NO;
self.inputTextView.backgroundColor = [UIColor colorWithWhite:0.8 alpha:1.0];
self.inputTextView.text = @"nil";
self.inputTextView.backgroundColor = [FLEXColor secondaryGroupedBackgroundColorWithAlpha:0.5];
self.inputPlaceholderText = @"nil (type not supported)";
self.targetSize = FLEXArgumentInputViewSizeSmall;
}
return self;
@@ -11,8 +11,7 @@
@implementation FLEXArgumentInputNumberView
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
{
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
self = [super initWithArgumentTypeEncoding:typeEncoding];
if (self) {
self.inputTextView.keyboardType = UIKeyboardTypeNumbersAndPunctuation;
@@ -21,20 +20,19 @@
return self;
}
- (void)setInputValue:(id)inputValue
{
- (void)setInputValue:(id)inputValue {
if ([inputValue respondsToSelector:@selector(stringValue)]) {
self.inputTextView.text = [inputValue stringValue];
}
}
- (id)inputValue
{
- (id)inputValue {
return [FLEXRuntimeUtility valueForNumberWithObjCType:self.typeEncoding.UTF8String fromInputString:self.inputTextView.text];
}
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
{
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
NSParameterAssert(type);
static NSArray<NSString *> *primitiveTypes = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
@@ -25,8 +25,7 @@ typedef NS_ENUM(NSUInteger, FLEXArgInputObjectType) {
@implementation FLEXArgumentInputObjectView
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
{
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
self = [super initWithArgumentTypeEncoding:typeEncoding];
if (self) {
// Start with the numbers and punctuation keyboard since quotes, curly braces, or
@@ -46,8 +45,7 @@ typedef NS_ENUM(NSUInteger, FLEXArgInputObjectType) {
return self;
}
- (void)didChangeType
{
- (void)didChangeType {
self.inputType = self.objectTypeSegmentControl.selectedSegmentIndex;
if (super.inputValue) {
@@ -61,8 +59,7 @@ typedef NS_ENUM(NSUInteger, FLEXArgInputObjectType) {
}
}
- (void)setInputType:(FLEXArgInputObjectType)inputType
{
- (void)setInputType:(FLEXArgInputObjectType)inputType {
if (_inputType == inputType) return;
_inputType = inputType;
@@ -98,14 +95,12 @@ typedef NS_ENUM(NSUInteger, FLEXArgInputObjectType) {
[self.superview setNeedsLayout];
}
- (void)setInputValue:(id)inputValue
{
- (void)setInputValue:(id)inputValue {
super.inputValue = inputValue;
[self populateTextAreaFromValue:inputValue];
}
- (id)inputValue
{
- (id)inputValue {
switch (self.inputType) {
case FLEXArgInputObjectTypeJSON:
return [FLEXRuntimeUtility objectValueFromEditableJSONString:self.inputTextView.text];
@@ -122,8 +117,7 @@ typedef NS_ENUM(NSUInteger, FLEXArgInputObjectType) {
}
}
- (void)populateTextAreaFromValue:(id)value
{
- (void)populateTextAreaFromValue:(id)value {
if (!value) {
self.inputTextView.text = nil;
} else {
@@ -138,16 +132,14 @@ typedef NS_ENUM(NSUInteger, FLEXArgInputObjectType) {
[self textViewDidChange:self.inputTextView];
}
- (CGSize)sizeThatFits:(CGSize)size
{
- (CGSize)sizeThatFits:(CGSize)size {
CGSize fitSize = [super sizeThatFits:size];
fitSize.height += [self.objectTypeSegmentControl sizeThatFits:size].height + kSegmentInputMargin;
return fitSize;
}
- (void)layoutSubviews
{
- (void)layoutSubviews {
CGFloat segmentHeight = [self.objectTypeSegmentControl sizeThatFits:self.frame.size].height;
self.objectTypeSegmentControl.frame = CGRectMake(
0.0,
@@ -162,23 +154,20 @@ typedef NS_ENUM(NSUInteger, FLEXArgInputObjectType) {
[super layoutSubviews];
}
- (CGFloat)topInputFieldVerticalLayoutGuide
{
- (CGFloat)topInputFieldVerticalLayoutGuide {
// Our text view is offset from the segmented control
CGFloat segmentHeight = [self.objectTypeSegmentControl sizeThatFits:self.frame.size].height;
return segmentHeight + super.topInputFieldVerticalLayoutGuide + kSegmentInputMargin;
}
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
{
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
NSParameterAssert(type);
// Must be object type
return type[0] == '@';
return type[0] == FLEXTypeEncodingObjcObject || type[0] == FLEXTypeEncodingObjcClass;
}
+ (FLEXArgInputObjectType)preferredDefaultTypeForObjCType:(const char *)type withCurrentValue:(id)value
{
NSParameterAssert(type[0] == '@');
+ (FLEXArgInputObjectType)preferredDefaultTypeForObjCType:(const char *)type withCurrentValue:(id)value {
NSParameterAssert(type[0] == FLEXTypeEncodingObjcObject || type[0] == FLEXTypeEncodingObjcClass);
if (value) {
// If there's a current value, it must be serializable to JSON
@@ -11,36 +11,119 @@
@implementation FLEXArgumentInputStringView
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
{
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
self = [super initWithArgumentTypeEncoding:typeEncoding];
if (self) {
self.targetSize = FLEXArgumentInputViewSizeLarge;
FLEXTypeEncoding type = typeEncoding[0];
if (type == FLEXTypeEncodingConst) {
// A crash here would mean an invalid type encoding string
type = typeEncoding[1];
}
// Selectors don't need a multi-line text box
if (type == FLEXTypeEncodingSelector) {
self.targetSize = FLEXArgumentInputViewSizeSmall;
} else {
self.targetSize = FLEXArgumentInputViewSizeLarge;
}
}
return self;
}
- (void)setInputValue:(id)inputValue
{
self.inputTextView.text = inputValue;
- (void)setInputValue:(id)inputValue {
if ([inputValue isKindOfClass:[NSString class]]) {
self.inputTextView.text = inputValue;
} else if ([inputValue isKindOfClass:[NSValue class]]) {
NSValue *value = (id)inputValue;
NSParameterAssert(strlen(value.objCType) == 1);
// C-String or SEL from NSValue
FLEXTypeEncoding type = value.objCType[0];
if (type == FLEXTypeEncodingConst) {
// A crash here would mean an invalid type encoding string
type = value.objCType[1];
}
if (type == FLEXTypeEncodingCString) {
self.inputTextView.text = @((const char *)value.pointerValue);
} else if (type == FLEXTypeEncodingSelector) {
self.inputTextView.text = NSStringFromSelector((SEL)value.pointerValue);
}
}
}
- (id)inputValue
{
- (id)inputValue {
NSString *text = self.inputTextView.text;
// Interpret empty string as nil. We loose the ability to set empty string as a string value,
// but we accept that tradeoff in exchange for not having to type quotes for every string.
return self.inputTextView.text.length > 0 ? [self.inputTextView.text copy] : nil;
if (!text.length) {
return nil;
}
// Case: C-strings and SELs
if (self.typeEncoding.length <= 2) {
FLEXTypeEncoding type = [self.typeEncoding characterAtIndex:0];
if (type == FLEXTypeEncodingConst) {
// A crash here would mean an invalid type encoding string
type = [self.typeEncoding characterAtIndex:1];
}
if (type == FLEXTypeEncodingCString || type == FLEXTypeEncodingSelector) {
const char *encoding = self.typeEncoding.UTF8String;
SEL selector = NSSelectorFromString(text);
return [NSValue valueWithBytes:&selector objCType:encoding];
}
}
// Case: NSStrings
return self.inputTextView.text.copy;
}
// TODO: Support using object address for strings, as in the object arg view.
#pragma mark -
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
NSParameterAssert(type);
unsigned long len = strlen(type);
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
{
BOOL supported = type && strcmp(type, FLEXEncodeClass(NSString)) == 0;
supported = supported || (value && [value isKindOfClass:[NSString class]]);
return supported;
BOOL isConst = type[0] == FLEXTypeEncodingConst;
NSInteger i = isConst ? 1 : 0;
BOOL typeIsString = strcmp(type, FLEXEncodeClass(NSString)) == 0;
BOOL typeIsCString = len <= 2 && type[i] == FLEXTypeEncodingCString;
BOOL typeIsSEL = len <= 2 && type[i] == FLEXTypeEncodingSelector;
BOOL valueIsString = [value isKindOfClass:[NSString class]];
BOOL typeIsPrimitiveString = typeIsSEL || typeIsCString;
BOOL typeIsSupported = typeIsString || typeIsCString || typeIsSEL;
BOOL valueIsNSValueWithCorrectType = NO;
if ([value isKindOfClass:[NSValue class]]) {
NSValue *v = (id)value;
len = strlen(v.objCType);
if (len == 1) {
FLEXTypeEncoding type = v.objCType[i];
if (type == FLEXTypeEncodingCString && typeIsCString) {
valueIsNSValueWithCorrectType = YES;
} else if (type == FLEXTypeEncodingSelector && typeIsSEL) {
valueIsNSValueWithCorrectType = YES;
}
}
}
if (!value && typeIsSupported) {
return YES;
}
if (typeIsString && valueIsString) {
return YES;
}
// Primitive strings can be input as NSStrings or NSValues
if (typeIsPrimitiveString && (valueIsString || valueIsNSValueWithCorrectType)) {
return YES;
}
return NO;
}
@end
@@ -18,22 +18,26 @@
@implementation FLEXArgumentInputStructView
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
{
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
self = [super initWithArgumentTypeEncoding:typeEncoding];
if (self) {
NSMutableArray<FLEXArgumentInputView *> *inputViews = [NSMutableArray array];
NSArray<NSString *> *customTitles = [[self class] customFieldTitlesForTypeEncoding:typeEncoding];
[FLEXRuntimeUtility enumerateTypesInStructEncoding:typeEncoding usingBlock:^(NSString *structName, const char *fieldTypeEncoding, NSString *prettyTypeEncoding, NSUInteger fieldIndex, NSUInteger fieldOffset) {
[FLEXRuntimeUtility enumerateTypesInStructEncoding:typeEncoding usingBlock:^(NSString *structName,
const char *fieldTypeEncoding,
NSString *prettyTypeEncoding,
NSUInteger fieldIndex,
NSUInteger fieldOffset) {
FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:fieldTypeEncoding];
inputView.backgroundColor = self.backgroundColor;
inputView.targetSize = FLEXArgumentInputViewSizeSmall;
if (fieldIndex < customTitles.count) {
inputView.title = customTitles[fieldIndex];
} else {
inputView.title = [NSString stringWithFormat:@"%@ field %lu (%@)", structName, (unsigned long)fieldIndex, prettyTypeEncoding];
inputView.title = [NSString stringWithFormat:@"%@ field %lu (%@)",
structName, (unsigned long)fieldIndex, prettyTypeEncoding
];
}
[inputViews addObject:inputView];
@@ -47,16 +51,14 @@
#pragma mark - Superclass Overrides
- (void)setBackgroundColor:(UIColor *)backgroundColor
{
- (void)setBackgroundColor:(UIColor *)backgroundColor {
[super setBackgroundColor:backgroundColor];
for (FLEXArgumentInputView *inputView in self.argumentInputViews) {
inputView.backgroundColor = backgroundColor;
}
}
- (void)setInputValue:(id)inputValue
{
- (void)setInputValue:(id)inputValue {
if ([inputValue isKindOfClass:[NSValue class]]) {
const char *structTypeEncoding = [inputValue objCType];
if (strcmp(self.typeEncoding.UTF8String, structTypeEncoding) == 0) {
@@ -69,7 +71,11 @@
if (valueSize > 0) {
void *unboxedValue = malloc(valueSize);
[inputValue getValue:unboxedValue];
[FLEXRuntimeUtility enumerateTypesInStructEncoding:structTypeEncoding usingBlock:^(NSString *structName, const char *fieldTypeEncoding, NSString *prettyTypeEncoding, NSUInteger fieldIndex, NSUInteger fieldOffset) {
[FLEXRuntimeUtility enumerateTypesInStructEncoding:structTypeEncoding usingBlock:^(NSString *structName,
const char *fieldTypeEncoding,
NSString *prettyTypeEncoding,
NSUInteger fieldIndex,
NSUInteger fieldOffset) {
void *fieldPointer = unboxedValue + fieldOffset;
FLEXArgumentInputView *inputView = self.argumentInputViews[fieldIndex];
@@ -87,8 +93,7 @@
}
}
- (id)inputValue
{
- (id)inputValue {
NSValue *boxedStruct = nil;
const char *structTypeEncoding = self.typeEncoding.UTF8String;
NSUInteger structSize = 0;
@@ -99,7 +104,11 @@
if (structSize > 0) {
void *unboxedStruct = malloc(structSize);
[FLEXRuntimeUtility enumerateTypesInStructEncoding:structTypeEncoding usingBlock:^(NSString *structName, const char *fieldTypeEncoding, NSString *prettyTypeEncoding, NSUInteger fieldIndex, NSUInteger fieldOffset) {
[FLEXRuntimeUtility enumerateTypesInStructEncoding:structTypeEncoding usingBlock:^(NSString *structName,
const char *fieldTypeEncoding,
NSString *prettyTypeEncoding,
NSUInteger fieldIndex,
NSUInteger fieldOffset) {
void *fieldPointer = unboxedStruct + fieldOffset;
FLEXArgumentInputView *inputView = self.argumentInputViews[fieldIndex];
@@ -123,8 +132,7 @@
return boxedStruct;
}
- (BOOL)inputViewIsFirstResponder
{
- (BOOL)inputViewIsFirstResponder {
BOOL isFirstResponder = NO;
for (FLEXArgumentInputView *inputView in self.argumentInputViews) {
if ([inputView inputViewIsFirstResponder]) {
@@ -138,8 +146,7 @@
#pragma mark - Layout and Sizing
- (void)layoutSubviews
{
- (void)layoutSubviews {
[super layoutSubviews];
CGFloat runningOriginY = self.topInputFieldVerticalLayoutGuide;
@@ -151,13 +158,11 @@
}
}
+ (CGFloat)verticalPaddingBetweenFields
{
+ (CGFloat)verticalPaddingBetweenFields {
return 10.0;
}
- (CGSize)sizeThatFits:(CGSize)size
{
- (CGSize)sizeThatFits:(CGSize)size {
CGSize fitSize = [super sizeThatFits:size];
CGSize constrainSize = CGSizeMake(size.width, CGFLOAT_MAX);
@@ -174,13 +179,24 @@
#pragma mark - Class Helpers
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
{
return type && type[0] == FLEXTypeEncodingStructBegin;
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
NSParameterAssert(type);
if (type[0] == FLEXTypeEncodingStructBegin) {
// We cannot support anything with bitfields or structs,
// and this will throw an exception if it does
@try {
NSGetSizeAndAlignment(type, nil, nil);
} @catch (NSException *exception) {
return NO;
}
return YES;
}
return NO;
}
+ (NSArray<NSString *> *)customFieldTitlesForTypeEncoding:(const char *)typeEncoding
{
+ (NSArray<NSString *> *)customFieldTitlesForTypeEncoding:(const char *)typeEncoding {
NSArray<NSString *> *customTitles = nil;
if (strcmp(typeEncoding, @encode(CGRect)) == 0) {
customTitles = @[@"CGPoint origin", @"CGSize size"];
@@ -16,8 +16,7 @@
@implementation FLEXArgumentInputSwitchView
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
{
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
self = [super initWithArgumentTypeEncoding:typeEncoding];
if (self) {
self.inputSwitch = [UISwitch new];
@@ -31,8 +30,7 @@
#pragma mark Input/Output
- (void)setInputValue:(id)inputValue
{
- (void)setInputValue:(id)inputValue {
BOOL on = NO;
if ([inputValue isKindOfClass:[NSNumber class]]) {
NSNumber *number = (NSNumber *)inputValue;
@@ -46,30 +44,26 @@
self.inputSwitch.on = on;
}
- (id)inputValue
{
- (id)inputValue {
BOOL isOn = [self.inputSwitch isOn];
NSValue *boxedBool = [NSValue value:&isOn withObjCType:@encode(BOOL)];
return boxedBool;
}
- (void)switchValueDidChange:(id)sender
{
- (void)switchValueDidChange:(id)sender {
[self.delegate argumentInputViewValueDidChange:self];
}
#pragma mark - Layout and Sizing
- (void)layoutSubviews
{
- (void)layoutSubviews {
[super layoutSubviews];
self.inputSwitch.frame = CGRectMake(0, self.topInputFieldVerticalLayoutGuide, self.inputSwitch.frame.size.width, self.inputSwitch.frame.size.height);
}
- (CGSize)sizeThatFits:(CGSize)size
{
- (CGSize)sizeThatFits:(CGSize)size {
CGSize fitSize = [super sizeThatFits:size];
fitSize.height += self.inputSwitch.frame.size.height;
return fitSize;
@@ -78,10 +72,10 @@
#pragma mark - Class Helpers
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
{
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
NSParameterAssert(type);
// Only BOOLs. Current value is irrelevant.
return type && strcmp(type, @encode(BOOL)) == 0;
return strcmp(type, @encode(BOOL)) == 0;
}
@end
@@ -20,26 +20,31 @@
@implementation FLEXArgumentInputTextView
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
{
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
self = [super initWithArgumentTypeEncoding:typeEncoding];
if (self) {
self.inputTextView = [UITextView new];
self.inputTextView.font = [[self class] inputFont];
self.inputTextView.backgroundColor = [FLEXColor primaryBackgroundColor];
self.inputTextView.layer.borderColor = [FLEXColor borderColor].CGColor;
self.inputTextView.layer.borderWidth = 1.f;
self.inputTextView.layer.cornerRadius = 5.f;
self.inputTextView.backgroundColor = [FLEXColor secondaryGroupedBackgroundColor];
self.inputTextView.layer.cornerRadius = 10.f;
self.inputTextView.contentInset = UIEdgeInsetsMake(0, 5, 0, 0);
self.inputTextView.autocapitalizationType = UITextAutocapitalizationTypeNone;
self.inputTextView.autocorrectionType = UITextAutocorrectionTypeNo;
self.inputTextView.delegate = self;
self.inputTextView.inputAccessoryView = [self createToolBar];
[self addSubview:self.inputTextView];
if (@available(iOS 11, *)) {
[self.inputTextView.layer setValue:@YES forKey:@"continuousCorners"];
} else {
self.inputTextView.layer.borderWidth = 1.f;
self.inputTextView.layer.borderColor = [FLEXColor borderColor].CGColor;
}
self.placeholderLabel = [UILabel new];
self.placeholderLabel.font = self.inputTextView.font;
self.placeholderLabel.textColor = [FLEXColor deemphasizedTextColor];
self.placeholderLabel.numberOfLines = 0;
[self addSubview:self.inputTextView];
[self.inputTextView addSubview:self.placeholderLabel];
}
@@ -48,23 +53,26 @@
#pragma mark - Private
- (UIToolbar *)createToolBar
{
- (UIToolbar *)createToolBar {
UIToolbar *toolBar = [UIToolbar new];
[toolBar sizeToFit];
UIBarButtonItem *spaceItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil];
UIBarButtonItem *doneItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(textViewDone)];
toolBar.items = @[spaceItem, doneItem];
UIBarButtonItem *spaceItem = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace
target:nil action:nil
];
UIBarButtonItem *pasteItem = [[UIBarButtonItem alloc]
initWithTitle:@"Paste" style:UIBarButtonItemStyleDone
target:self.inputTextView action:@selector(paste:)
];
UIBarButtonItem *doneItem = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemDone
target:self.inputTextView action:@selector(resignFirstResponder)
];
toolBar.items = @[spaceItem, pasteItem, doneItem];
return toolBar;
}
- (void)textViewDone
{
[self.inputTextView resignFirstResponder];
}
- (void)setInputPlaceholderText:(NSString *)placeholder
{
- (void)setInputPlaceholderText:(NSString *)placeholder {
self.placeholderLabel.text = placeholder;
if (placeholder.length) {
if (!self.inputTextView.text.length) {
@@ -79,41 +87,35 @@
[self setNeedsLayout];
}
- (NSString *)inputPlaceholderText
{
- (NSString *)inputPlaceholderText {
return self.placeholderLabel.text;
}
#pragma mark - Superclass Overrides
- (BOOL)inputViewIsFirstResponder
{
- (BOOL)inputViewIsFirstResponder {
return self.inputTextView.isFirstResponder;
}
#pragma mark - Layout and Sizing
- (void)layoutSubviews
{
- (void)layoutSubviews {
[super layoutSubviews];
self.inputTextView.frame = CGRectMake(0, self.topInputFieldVerticalLayoutGuide, self.bounds.size.width, [self inputTextViewHeight]);
// Placeholder label is positioned by insetting origin,
// which is the line fragment padding for X and 0 for Y,
// Placeholder label is positioned by insetting then origin
// by the content inset then the text container inset
CGFloat leading = self.inputTextView.textContainer.lineFragmentPadding;
CGSize s = self.inputTextView.frame.size;
self.placeholderLabel.frame = CGRectMake(leading, 0, s.width, s.height);
self.placeholderLabel.frame = CGRectMake(0, 0, s.width, s.height);
self.placeholderLabel.frame = UIEdgeInsetsInsetRect(
UIEdgeInsetsInsetRect(self.placeholderLabel.frame, self.inputTextView.contentInset),
self.inputTextView.textContainerInset
);
}
- (NSUInteger)numberOfInputLines
{
- (NSUInteger)numberOfInputLines {
switch (self.targetSize) {
case FLEXArgumentInputViewSizeDefault:
return 2;
@@ -124,45 +126,27 @@
}
}
- (CGFloat)inputTextViewHeight
{
- (CGFloat)inputTextViewHeight {
return ceil([[self class] inputFont].lineHeight * self.numberOfInputLines) + 16.0;
}
- (CGSize)sizeThatFits:(CGSize)size
{
- (CGSize)sizeThatFits:(CGSize)size {
CGSize fitSize = [super sizeThatFits:size];
fitSize.height += [self inputTextViewHeight];
return fitSize;
}
#pragma mark - Trait collection changes
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
{
#if FLEX_AT_LEAST_IOS13_SDK
if (@available(iOS 13.0, *)) {
if (previousTraitCollection.userInterfaceStyle != self.traitCollection.userInterfaceStyle) {
self.inputTextView.layer.borderColor = [FLEXColor borderColor].CGColor;
}
}
#endif
}
#pragma mark - Class Helpers
+ (UIFont *)inputFont
{
return [FLEXUtility defaultFontOfSize:14.0];
+ (UIFont *)inputFont {
return [UIFont systemFontOfSize:14.0];
}
#pragma mark - UITextViewDelegate
- (void)textViewDidChange:(UITextView *)textView
{
- (void)textViewDidChange:(UITextView *)textView {
[self.delegate argumentInputViewValueDidChange:self];
self.placeholderLabel.hidden = !(self.inputPlaceholderText.length && !textView.text.length);
}
@@ -9,8 +9,11 @@
#import <UIKit/UIKit.h>
typedef NS_ENUM(NSUInteger, FLEXArgumentInputViewSize) {
/// 2 lines, medium-sized
FLEXArgumentInputViewSizeDefault = 0,
/// One line
FLEXArgumentInputViewSizeSmall,
/// Several lines
FLEXArgumentInputViewSizeLarge
};
@@ -8,6 +8,7 @@
#import "FLEXArgumentInputView.h"
#import "FLEXUtility.h"
#import "FLEXColor.h"
@interface FLEXArgumentInputView ()
@@ -18,8 +19,7 @@
@implementation FLEXArgumentInputView
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
{
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
self = [super initWithFrame:CGRectZero];
if (self) {
self.typeEncoding = typeEncoding != NULL ? @(typeEncoding) : nil;
@@ -27,8 +27,7 @@
return self;
}
- (void)layoutSubviews
{
- (void)layoutSubviews {
[super layoutSubviews];
if (self.showsTitle) {
@@ -38,14 +37,12 @@
}
}
- (void)setBackgroundColor:(UIColor *)backgroundColor
{
- (void)setBackgroundColor:(UIColor *)backgroundColor {
[super setBackgroundColor:backgroundColor];
self.titleLabel.backgroundColor = backgroundColor;
}
- (void)setTitle:(NSString *)title
{
- (void)setTitle:(NSString *)title {
if (![_title isEqual:title]) {
_title = title;
self.titleLabel.text = title;
@@ -53,26 +50,22 @@
}
}
- (UILabel *)titleLabel
{
- (UILabel *)titleLabel {
if (!_titleLabel) {
_titleLabel = [UILabel new];
_titleLabel.font = [[self class] titleFont];
_titleLabel.backgroundColor = self.backgroundColor;
_titleLabel.textColor = [UIColor colorWithWhite:0.3 alpha:1.0];
_titleLabel.textColor = [FLEXColor primaryTextColor];
_titleLabel.numberOfLines = 0;
[self addSubview:_titleLabel];
}
return _titleLabel;
}
- (BOOL)showsTitle
{
- (BOOL)showsTitle {
return self.title.length > 0;
}
- (CGFloat)topInputFieldVerticalLayoutGuide
{
- (CGFloat)topInputFieldVerticalLayoutGuide {
CGFloat verticalLayoutGuide = 0;
if (self.showsTitle) {
CGFloat titleHeight = [self.titleLabel sizeThatFits:self.bounds.size].height;
@@ -84,34 +77,29 @@
#pragma mark - Subclasses Can Override
- (BOOL)inputViewIsFirstResponder
{
- (BOOL)inputViewIsFirstResponder {
return NO;
}
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
{
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
return NO;
}
#pragma mark - Class Helpers
+ (UIFont *)titleFont
{
return [FLEXUtility defaultFontOfSize:12.0];
+ (UIFont *)titleFont {
return [UIFont systemFontOfSize:12.0];
}
+ (CGFloat)titleBottomPadding
{
+ (CGFloat)titleBottomPadding {
return 4.0;
}
#pragma mark - Sizing
- (CGSize)sizeThatFits:(CGSize)size
{
- (CGSize)sizeThatFits:(CGSize)size {
CGFloat height = 0;
if (self.title.length > 0) {
@@ -7,8 +7,7 @@
//
#import <Foundation/Foundation.h>
@class FLEXArgumentInputView;
#import "FLEXArgumentInputSwitchView.h"
@interface FLEXArgumentInputViewFactory : NSObject
@@ -21,13 +21,11 @@
@implementation FLEXArgumentInputViewFactory
+ (FLEXArgumentInputView *)argumentInputViewForTypeEncoding:(const char *)typeEncoding
{
+ (FLEXArgumentInputView *)argumentInputViewForTypeEncoding:(const char *)typeEncoding {
return [self argumentInputViewForTypeEncoding:typeEncoding currentValue:nil];
}
+ (FLEXArgumentInputView *)argumentInputViewForTypeEncoding:(const char *)typeEncoding currentValue:(id)currentValue
{
+ (FLEXArgumentInputView *)argumentInputViewForTypeEncoding:(const char *)typeEncoding currentValue:(id)currentValue {
Class subclass = [self argumentInputViewSubclassForTypeEncoding:typeEncoding currentValue:currentValue];
if (!subclass) {
// Fall back to a FLEXArgumentInputNotSupportedView if we can't find a subclass that fits the type encoding.
@@ -39,8 +37,7 @@
return [[subclass alloc] initWithArgumentTypeEncoding:typeEncoding + fieldNameOffset];
}
+ (Class)argumentInputViewSubclassForTypeEncoding:(const char *)typeEncoding currentValue:(id)currentValue
{
+ (Class)argumentInputViewSubclassForTypeEncoding:(const char *)typeEncoding currentValue:(id)currentValue {
// Remove the field name if there is any (e.g. \"width\"d -> d)
const NSUInteger fieldNameOffset = [FLEXRuntimeUtility fieldNameOffsetForTypeEncoding:typeEncoding];
Class argumentInputViewSubclass = nil;
@@ -66,8 +63,7 @@
return argumentInputViewSubclass;
}
+ (BOOL)canEditFieldWithTypeEncoding:(const char *)typeEncoding currentValue:(id)currentValue
{
+ (BOOL)canEditFieldWithTypeEncoding:(const char *)typeEncoding currentValue:(id)currentValue {
return [self argumentInputViewSubclassForTypeEncoding:typeEncoding currentValue:currentValue] != nil;
}
@@ -6,9 +6,9 @@
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import "FLEXMutableFieldEditorViewController.h"
#import "FLEXFieldEditorViewController.h"
@interface FLEXDefaultEditorViewController : FLEXMutableFieldEditorViewController
@interface FLEXDefaultEditorViewController : FLEXFieldEditorViewController
- (id)initWithDefaults:(NSUserDefaults *)defaults key:(NSString *)key;
@@ -21,8 +21,7 @@
@implementation FLEXDefaultEditorViewController
- (id)initWithDefaults:(NSUserDefaults *)defaults key:(NSString *)key
{
- (id)initWithDefaults:(NSUserDefaults *)defaults key:(NSString *)key {
self = [super initWithTarget:defaults];
if (self) {
self.key = key;
@@ -31,13 +30,11 @@
return self;
}
- (NSUserDefaults *)defaults
{
- (NSUserDefaults *)defaults {
return [self.target isKindOfClass:[NSUserDefaults class]] ? self.target : nil;
}
- (void)viewDidLoad
{
- (void)viewDidLoad {
[super viewDidLoad];
self.fieldEditorView.fieldDescription = self.key;
@@ -52,8 +49,7 @@
self.fieldEditorView.argumentInputViews = @[inputView];
}
- (void)actionButtonPressed:(id)sender
{
- (void)actionButtonPressed:(id)sender {
[super actionButtonPressed:sender];
id value = self.firstInputView.inputValue;
@@ -67,15 +63,13 @@
self.firstInputView.inputValue = [self.defaults objectForKey:self.key];
}
- (void)getterButtonPressed:(id)sender
{
- (void)getterButtonPressed:(id)sender {
[super getterButtonPressed:sender];
id returnedObject = [self.defaults objectForKey:self.key];
[self exploreObjectOrPopViewController:returnedObject];
}
+ (BOOL)canEditDefaultWithValue:(id)currentValue
{
+ (BOOL)canEditDefaultWithValue:(id)currentValue {
return [FLEXArgumentInputViewFactory
canEditFieldWithTypeEncoding:FLEXEncodeObject(currentValue)
currentValue:currentValue
+1 -1
View File
@@ -15,6 +15,6 @@
@property (nonatomic, copy) NSString *targetDescription;
@property (nonatomic, copy) NSString *fieldDescription;
@property (nonatomic) NSArray<FLEXArgumentInputView *> *argumentInputViews;
@property (nonatomic, copy) NSArray<FLEXArgumentInputView *> *argumentInputViews;
@end
+14 -27
View File
@@ -21,8 +21,7 @@
@implementation FLEXFieldEditorView
- (id)initWithFrame:(CGRect)frame
{
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.targetDescriptionLabel = [UILabel new];
@@ -44,8 +43,7 @@
return self;
}
- (void)layoutSubviews
{
- (void)layoutSubviews {
[super layoutSubviews];
CGFloat horizontalPadding = [[self class] horizontalPadding];
@@ -78,15 +76,13 @@
}
}
- (void)setBackgroundColor:(UIColor *)backgroundColor
{
- (void)setBackgroundColor:(UIColor *)backgroundColor {
[super setBackgroundColor:backgroundColor];
self.targetDescriptionLabel.backgroundColor = backgroundColor;
self.fieldDescriptionLabel.backgroundColor = backgroundColor;
}
- (void)setTargetDescription:(NSString *)targetDescription
{
- (void)setTargetDescription:(NSString *)targetDescription {
if (![_targetDescription isEqual:targetDescription]) {
_targetDescription = targetDescription;
self.targetDescriptionLabel.text = targetDescription;
@@ -94,8 +90,7 @@
}
}
- (void)setFieldDescription:(NSString *)fieldDescription
{
- (void)setFieldDescription:(NSString *)fieldDescription {
if (![_fieldDescription isEqual:fieldDescription]) {
_fieldDescription = fieldDescription;
self.fieldDescriptionLabel.text = fieldDescription;
@@ -103,8 +98,7 @@
}
}
- (void)setArgumentInputViews:(NSArray<FLEXArgumentInputView *> *)argumentInputViews
{
- (void)setArgumentInputViews:(NSArray<FLEXArgumentInputView *> *)argumentInputViews {
if (![_argumentInputViews isEqual:argumentInputViews]) {
for (FLEXArgumentInputView *inputView in _argumentInputViews) {
@@ -121,40 +115,33 @@
}
}
+ (UIView *)dividerView
{
+ (UIView *)dividerView {
UIView *dividerView = [UIView new];
dividerView.backgroundColor = [self dividerColor];
return dividerView;
}
+ (UIColor *)dividerColor
{
+ (UIColor *)dividerColor {
return UIColor.lightGrayColor;
}
+ (CGFloat)horizontalPadding
{
+ (CGFloat)horizontalPadding {
return 10.0;
}
+ (CGFloat)verticalPadding
{
+ (CGFloat)verticalPadding {
return 20.0;
}
+ (UIFont *)labelFont
{
return [FLEXUtility defaultFontOfSize:14.0];
+ (UIFont *)labelFont {
return [UIFont systemFontOfSize:14.0];
}
+ (CGFloat)dividerLineHeight
{
+ (CGFloat)dividerLineHeight {
return 1.0;
}
- (CGSize)sizeThatFits:(CGSize)size
{
- (CGSize)sizeThatFits:(CGSize)size {
CGFloat horizontalPadding = [[self class] horizontalPadding];
CGFloat verticalPadding = [[self class] verticalPadding];
CGFloat dividerLineHeight = [[self class] dividerLineHeight];
+17 -20
View File
@@ -1,32 +1,29 @@
//
// FLEXFieldEditorViewController.h
// Flipboard
// FLEX
//
// Created by Ryan Olson on 5/16/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
// Created by Tanner on 11/22/18.
// Copyright © 2018 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "FLEXVariableEditorViewController.h"
#import "FLEXProperty.h"
#import "FLEXIvar.h"
@class FLEXFieldEditorView;
@class FLEXArgumentInputView;
NS_ASSUME_NONNULL_BEGIN
@interface FLEXFieldEditorViewController : UIViewController
@interface FLEXFieldEditorViewController : FLEXVariableEditorViewController
- (id)initWithTarget:(id)target;
/// @return nil if the property is readonly or if the type is unsupported
+ (nullable instancetype)target:(id)target property:(FLEXProperty *)property;
/// @return nil if the ivar type is unsupported
+ (nullable instancetype)target:(id)target ivar:(FLEXIvar *)ivar;
// Convenience accessor since many subclasses only use one input view
@property (nonatomic, readonly) FLEXArgumentInputView *firstInputView;
/// Subclasses can change the button title via the \c title property
@property (nonatomic, readonly) UIBarButtonItem *getterButton;
// For subclass use only.
@property (nonatomic, readonly) id target;
@property (nonatomic, readonly) FLEXFieldEditorView *fieldEditorView;
@property (nonatomic, readonly) UIBarButtonItem *setterButton;
- (void)actionButtonPressed:(id)sender;
- (NSString *)titleForActionButton;
/// Pushes an explorer view controller for the given object
/// or pops the current view controller.
- (void)exploreObjectOrPopViewController:(id)objectOrNil;
- (void)getterButtonPressed:(id)sender;
@end
NS_ASSUME_NONNULL_END
+131 -101
View File
@@ -1,132 +1,162 @@
//
// FLEXFieldEditorViewController.m
// Flipboard
// FLEX
//
// Created by Ryan Olson on 5/16/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
// Created by Tanner on 11/22/18.
// Copyright © 2018 Flipboard. All rights reserved.
//
#import "FLEXColor.h"
#import "FLEXFieldEditorViewController.h"
#import "FLEXFieldEditorView.h"
#import "FLEXRuntimeUtility.h"
#import "FLEXUtility.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXArgumentInputView.h"
#import "FLEXArgumentInputViewFactory.h"
#import "FLEXObjectExplorerViewController.h"
#import "FLEXPropertyAttributes.h"
#import "FLEXUtility.h"
#import "FLEXColor.h"
#import "UIBarButtonItem+FLEX.h"
@interface FLEXFieldEditorViewController () <UIScrollViewDelegate>
@interface FLEXFieldEditorViewController () <FLEXArgumentInputViewDelegate>
@property (nonatomic) UIScrollView *scrollView;
@property (nonatomic) FLEXProperty *property;
@property (nonatomic) FLEXIvar *ivar;
@property (nonatomic, readwrite) id target;
@property (nonatomic, readwrite) FLEXFieldEditorView *fieldEditorView;
@property (nonatomic, readwrite) UIBarButtonItem *setterButton;
@property (nonatomic, readonly) id currentValue;
@property (nonatomic, readonly) const FLEXTypeEncoding *typeEncoding;
@property (nonatomic, readonly) NSString *fieldDescription;
@end
@implementation FLEXFieldEditorViewController
- (id)initWithTarget:(id)target
{
self = [super initWithNibName:nil bundle:nil];
if (self) {
self.target = target;
[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(keyboardDidShow:) name:UIKeyboardDidShowNotification object:nil];
[NSNotificationCenter.defaultCenter addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil];
#pragma mark - Initialization
+ (instancetype)target:(id)target property:(FLEXProperty *)property {
id value = [property getValue:target];
if (![self canEditProperty:property onObject:target currentValue:value]) {
return nil;
}
return self;
FLEXFieldEditorViewController *editor = [self target:target];
editor.title = @"Property";
editor.property = property;
return editor;
}
- (void)dealloc
{
[NSNotificationCenter.defaultCenter removeObserver:self];
+ (instancetype)target:(id)target ivar:(nonnull FLEXIvar *)ivar {
FLEXFieldEditorViewController *editor = [self target:target];
editor.title = @"Instance Variable";
editor.ivar = ivar;
return editor;
}
- (void)keyboardDidShow:(NSNotification *)notification
{
CGRect keyboardRectInWindow = [[[notification userInfo] objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
CGSize keyboardSize = [self.view convertRect:keyboardRectInWindow fromView:nil].size;
UIEdgeInsets scrollInsets = self.scrollView.contentInset;
scrollInsets.bottom = keyboardSize.height;
self.scrollView.contentInset = scrollInsets;
self.scrollView.scrollIndicatorInsets = scrollInsets;
// Find the active input view and scroll to make sure it's visible.
for (FLEXArgumentInputView *argumentInputView in self.fieldEditorView.argumentInputViews) {
if (argumentInputView.inputViewIsFirstResponder) {
CGRect scrollToVisibleRect = [self.scrollView convertRect:argumentInputView.bounds fromView:argumentInputView];
[self.scrollView scrollRectToVisible:scrollToVisibleRect animated:YES];
break;
}
}
}
#pragma mark - Overrides
- (void)keyboardWillHide:(NSNotification *)notification
{
UIEdgeInsets scrollInsets = self.scrollView.contentInset;
scrollInsets.bottom = 0.0;
self.scrollView.contentInset = scrollInsets;
self.scrollView.scrollIndicatorInsets = scrollInsets;
}
- (void)viewDidLoad
{
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [FLEXColor scrollViewBackgroundColor];
self.scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds];
self.scrollView.backgroundColor = self.view.backgroundColor;
self.scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
self.scrollView.delegate = self;
[self.view addSubview:self.scrollView];
self.fieldEditorView = [FLEXFieldEditorView new];
self.fieldEditorView.backgroundColor = self.view.backgroundColor;
self.fieldEditorView.targetDescription = [NSString stringWithFormat:@"%@ %p", [self.target class], self.target];
[self.scrollView addSubview:self.fieldEditorView];
self.setterButton = [[UIBarButtonItem alloc] initWithTitle:[self titleForActionButton] style:UIBarButtonItemStyleDone target:self action:@selector(actionButtonPressed:)];
self.navigationItem.rightBarButtonItem = self.setterButton;
}
- (void)viewWillLayoutSubviews
{
CGSize constrainSize = CGSizeMake(self.scrollView.bounds.size.width, CGFLOAT_MAX);
CGSize fieldEditorSize = [self.fieldEditorView sizeThatFits:constrainSize];
self.fieldEditorView.frame = CGRectMake(0, 0, fieldEditorSize.width, fieldEditorSize.height);
self.scrollView.contentSize = fieldEditorSize;
}
self.view.backgroundColor = [FLEXColor groupedBackgroundColor];
- (FLEXArgumentInputView *)firstInputView
{
return [self.fieldEditorView argumentInputViews].firstObject;
}
// Create getter button
_getterButton = [[UIBarButtonItem alloc]
initWithTitle:@"Get"
style:UIBarButtonItemStyleDone
target:self
action:@selector(getterButtonPressed:)
];
self.toolbarItems = @[
UIBarButtonItem.flex_flexibleSpace, self.getterButton, self.setterButton
];
- (void)actionButtonPressed:(id)sender
{
// Subclasses can override
[self.fieldEditorView endEditing:YES];
}
// Configure input view
self.fieldEditorView.fieldDescription = self.fieldDescription;
FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:self.typeEncoding];
inputView.inputValue = self.currentValue;
inputView.delegate = self;
self.fieldEditorView.argumentInputViews = @[inputView];
- (NSString *)titleForActionButton
{
// Subclasses can override.
return @"Set";
}
- (void)exploreObjectOrPopViewController:(id)objectOrNil {
if (objectOrNil) {
// For non-nil (or void) return types, push an explorer view controller to display the object
FLEXObjectExplorerViewController *explorerViewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:objectOrNil];
[self.navigationController pushViewController:explorerViewController animated:YES];
} else {
// If we didn't get a returned object but the method call succeeded,
// pop this view controller off the stack to indicate that the call went through.
[self.navigationController popViewControllerAnimated:YES];
// Don't show a "set" button for switches; we mutate when the switch is flipped
if ([inputView isKindOfClass:[FLEXArgumentInputSwitchView class]]) {
self.setterButton.enabled = NO;
self.setterButton.title = @"Flip the switch to call the setter";
// Put getter button before setter button
self.toolbarItems = @[
UIBarButtonItem.flex_flexibleSpace, self.setterButton, self.getterButton
];
}
}
- (void)actionButtonPressed:(id)sender {
[super actionButtonPressed:sender];
if (self.property) {
id userInputObject = self.firstInputView.inputValue;
NSArray *arguments = userInputObject ? @[userInputObject] : nil;
SEL setterSelector = self.property.likelySetter;
NSError *error = nil;
[FLEXRuntimeUtility performSelector:setterSelector onObject:self.target withArguments:arguments error:&error];
if (error) {
[FLEXAlert showAlert:@"Property Setter Failed" message:error.localizedDescription from:self];
sender = nil; // Don't pop back
}
} else {
// TODO: check mutability and use mutableCopy if necessary;
// this currently could and would assign NSArray to NSMutableArray
[self.ivar setValue:self.firstInputView.inputValue onObject:self.target];
}
// Go back after setting, but not for switches.
if (sender) {
[self.navigationController popViewControllerAnimated:YES];
} else {
self.firstInputView.inputValue = self.currentValue;
}
}
- (void)getterButtonPressed:(id)sender {
[self.fieldEditorView endEditing:YES];
[self exploreObjectOrPopViewController:self.currentValue];
}
- (void)argumentInputViewValueDidChange:(FLEXArgumentInputView *)argumentInputView {
if ([argumentInputView isKindOfClass:[FLEXArgumentInputSwitchView class]]) {
[self actionButtonPressed:nil];
}
}
#pragma mark - Private
- (id)currentValue {
if (self.property) {
return [self.property getValue:self.target];
} else {
return [self.ivar getValue:self.target];
}
}
- (const FLEXTypeEncoding *)typeEncoding {
if (self.property) {
return self.property.attributes.typeEncoding.UTF8String;
} else {
return self.ivar.typeEncoding.UTF8String;
}
}
- (NSString *)fieldDescription {
if (self.property) {
return self.property.fullDescription;
} else {
return self.ivar.description;
}
}
+ (BOOL)canEditProperty:(FLEXProperty *)property onObject:(id)object currentValue:(id)value {
const FLEXTypeEncoding *typeEncoding = property.attributes.typeEncoding.UTF8String;
BOOL canEditType = [FLEXArgumentInputViewFactory canEditFieldWithTypeEncoding:typeEncoding currentValue:value];
return canEditType && [object respondsToSelector:property.likelySetter];
}
+ (BOOL)canEditIvar:(Ivar)ivar currentValue:(id)value {
return [FLEXArgumentInputViewFactory canEditFieldWithTypeEncoding:ivar_getTypeEncoding(ivar) currentValue:value];
}
@end
@@ -1,18 +0,0 @@
//
// FLEXIvarEditorViewController.h
// Flipboard
//
// Created by Ryan Olson on 5/23/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import "FLEXMutableFieldEditorViewController.h"
#import <objc/runtime.h>
@interface FLEXIvarEditorViewController : FLEXMutableFieldEditorViewController
- (id)initWithTarget:(id)target ivar:(Ivar)ivar;
+ (BOOL)canEditIvar:(Ivar)ivar currentValue:(id)value;
@end
@@ -1,86 +0,0 @@
//
// FLEXIvarEditorViewController.m
// Flipboard
//
// Created by Ryan Olson on 5/23/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import "FLEXIvarEditorViewController.h"
#import "FLEXFieldEditorView.h"
#import "FLEXRuntimeUtility.h"
#import "FLEXArgumentInputView.h"
#import "FLEXArgumentInputViewFactory.h"
#import "FLEXArgumentInputSwitchView.h"
@interface FLEXIvarEditorViewController () <FLEXArgumentInputViewDelegate>
@property (nonatomic) Ivar ivar;
@end
@implementation FLEXIvarEditorViewController
- (id)initWithTarget:(id)target ivar:(Ivar)ivar
{
self = [super initWithTarget:target];
if (self) {
self.ivar = ivar;
self.title = @"Instance Variable";
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.fieldEditorView.fieldDescription = [FLEXRuntimeUtility prettyNameForIvar:self.ivar];
FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:ivar_getTypeEncoding(self.ivar)];
inputView.backgroundColor = self.view.backgroundColor;
inputView.inputValue = [FLEXRuntimeUtility valueForIvar:self.ivar onObject:self.target];
inputView.delegate = self;
self.fieldEditorView.argumentInputViews = @[inputView];
// Don't show a "set" button for switches. Set the ivar when the switch toggles.
if ([inputView isKindOfClass:[FLEXArgumentInputSwitchView class]]) {
self.navigationItem.rightBarButtonItem = nil;
}
}
- (void)actionButtonPressed:(id)sender
{
[super actionButtonPressed:sender];
// TODO: check mutability and use mutableCopy if necessary;
// this currently could and would assign NSArray to NSMutableArray
[FLEXRuntimeUtility setValue:self.firstInputView.inputValue forIvar:self.ivar onObject:self.target];
self.firstInputView.inputValue = [FLEXRuntimeUtility valueForIvar:self.ivar onObject:self.target];
// Pop view controller for consistency;
// property setters and method calls also pop on success.
[self.navigationController popViewControllerAnimated:YES];
}
- (void)getterButtonPressed:(id)sender
{
[super getterButtonPressed:sender];
id returnedObject = [FLEXRuntimeUtility valueForIvar:self.ivar onObject:self.target];
[self exploreObjectOrPopViewController:returnedObject];
}
- (void)argumentInputViewValueDidChange:(FLEXArgumentInputView *)argumentInputView
{
if ([argumentInputView isKindOfClass:[FLEXArgumentInputSwitchView class]]) {
[self actionButtonPressed:nil];
}
}
+ (BOOL)canEditIvar:(Ivar)ivar currentValue:(id)value
{
return [FLEXArgumentInputViewFactory canEditFieldWithTypeEncoding:ivar_getTypeEncoding(ivar) currentValue:value];
}
@end
@@ -6,11 +6,11 @@
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import "FLEXFieldEditorViewController.h"
#import <objc/runtime.h>
#import "FLEXVariableEditorViewController.h"
#import "FLEXMethod.h"
@interface FLEXMethodCallingViewController : FLEXFieldEditorViewController
@interface FLEXMethodCallingViewController : FLEXVariableEditorViewController
- (id)initWithTarget:(id)target method:(Method)method;
+ (instancetype)target:(id)target method:(FLEXMethod *)method;
@end
@@ -16,93 +16,89 @@
#import "FLEXUtility.h"
@interface FLEXMethodCallingViewController ()
@property (nonatomic) Method method;
@property (nonatomic) FLEXTypeEncoding *returnType;
@property (nonatomic) FLEXMethod *method;
@end
@implementation FLEXMethodCallingViewController
- (id)initWithTarget:(id)target method:(Method)method
{
+ (instancetype)target:(id)target method:(FLEXMethod *)method {
return [[self alloc] initWithTarget:target method:method];
}
- (id)initWithTarget:(id)target method:(FLEXMethod *)method {
NSParameterAssert(method.isInstanceMethod == !object_isClass(target));
self = [super initWithTarget:target];
if (self) {
self.method = method;
self.returnType = [FLEXRuntimeUtility returnTypeForMethod:method];
self.title = [self isClassMethod] ? @"Class Method" : @"Method";
self.title = method.isInstanceMethod ? @"Method" : @"Class Method";
}
return self;
}
- (void)viewDidLoad
{
- (void)viewDidLoad {
[super viewDidLoad];
NSString *returnType = @((const char *)self.returnType);
NSString *methodDescription = [FLEXRuntimeUtility prettyNameForMethod:self.method isClassMethod:[self isClassMethod]];
NSString *format = @"Signature:\n%@\n\nReturn Type:\n%@";
NSString *info = [NSString stringWithFormat:format, methodDescription, returnType];
self.fieldEditorView.fieldDescription = info;
NSArray<NSString *> *methodComponents = [FLEXRuntimeUtility prettyArgumentComponentsForMethod:self.method];
self.setterButton.title = @"Call";
// Configure field editor view
self.fieldEditorView.argumentInputViews = [self argumentInputViews];
self.fieldEditorView.fieldDescription = [NSString stringWithFormat:
@"Signature:\n%@\n\nReturn Type:\n%s",
self.method.description, (char *)self.method.returnType
];
}
- (NSArray<FLEXArgumentInputView *> *)argumentInputViews {
Method method = self.method.objc_method;
NSArray *methodComponents = [FLEXRuntimeUtility prettyArgumentComponentsForMethod:method];
NSMutableArray<FLEXArgumentInputView *> *argumentInputViews = [NSMutableArray array];
unsigned int argumentIndex = kFLEXNumberOfImplicitArgs;
for (NSString *methodComponent in methodComponents) {
char *argumentTypeEncoding = method_copyArgumentType(self.method, argumentIndex);
char *argumentTypeEncoding = method_copyArgumentType(method, argumentIndex);
FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:argumentTypeEncoding];
free(argumentTypeEncoding);
inputView.backgroundColor = self.view.backgroundColor;
inputView.title = methodComponent;
[argumentInputViews addObject:inputView];
argumentIndex++;
}
self.fieldEditorView.argumentInputViews = argumentInputViews;
return argumentInputViews;
}
- (void)dealloc
{
free(self.returnType);
self.returnType = NULL;
}
- (BOOL)isClassMethod
{
return self.target && self.target == [self.target class];
}
- (NSString *)titleForActionButton
{
return @"Call";
}
- (void)actionButtonPressed:(id)sender
{
- (void)actionButtonPressed:(id)sender {
[super actionButtonPressed:sender];
// Gather arguments
NSMutableArray *arguments = [NSMutableArray array];
for (FLEXArgumentInputView *inputView in self.fieldEditorView.argumentInputViews) {
id argumentValue = inputView.inputValue;
if (!argumentValue) {
// Use NSNulls as placeholders in the array. They will be interpreted as nil arguments.
argumentValue = [NSNull null];
}
[arguments addObject:argumentValue];
// Use NSNull as a nil placeholder; it will be interpreted as nil
[arguments addObject:inputView.inputValue ?: [NSNull null]];
}
// Call method
NSError *error = nil;
id returnedObject = [FLEXRuntimeUtility performSelector:method_getName(self.method) onObject:self.target withArguments:arguments error:&error];
id returnValue = [FLEXRuntimeUtility
performSelector:self.method.selector
onObject:self.target
withArguments:arguments
error:&error
];
// Display return value or error
if (error) {
[FLEXAlert showAlert:@"Method Call Failed" message:[error localizedDescription] from:self];
} else if (returnedObject) {
[FLEXAlert showAlert:@"Method Call Failed" message:error.localizedDescription from:self];
} else if (returnValue) {
// For non-nil (or void) return types, push an explorer view controller to display the returned object
returnedObject = [FLEXRuntimeUtility potentiallyUnwrapBoxedPointer:returnedObject type:self.returnType];
FLEXObjectExplorerViewController *explorerViewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:returnedObject];
[self.navigationController pushViewController:explorerViewController animated:YES];
returnValue = [FLEXRuntimeUtility potentiallyUnwrapBoxedPointer:returnValue type:self.method.returnType];
FLEXObjectExplorerViewController *explorer = [FLEXObjectExplorerFactory explorerViewControllerForObject:returnValue];
[self.navigationController pushViewController:explorer animated:YES];
} else {
[self exploreObjectOrPopViewController:returnedObject];
[self exploreObjectOrPopViewController:returnValue];
}
}
@@ -1,18 +0,0 @@
//
// FLEXMutableFieldEditorViewController.h
// FLEX
//
// Created by Tanner on 11/22/18.
// Copyright © 2018 Flipboard. All rights reserved.
//
#import "FLEXFieldEditorViewController.h"
@interface FLEXMutableFieldEditorViewController : FLEXFieldEditorViewController
@property (nonatomic, readonly) UIBarButtonItem *getterButton;
- (void)getterButtonPressed:(id)sender;
- (NSString *)titleForGetterButton;
@end
@@ -1,36 +0,0 @@
//
// FLEXMutableFieldEditorViewController.m
// FLEX
//
// Created by Tanner on 11/22/18.
// Copyright © 2018 Flipboard. All rights reserved.
//
#import "FLEXMutableFieldEditorViewController.h"
#import "FLEXFieldEditorView.h"
@interface FLEXMutableFieldEditorViewController ()
@property (nonatomic, readwrite) UIBarButtonItem *getterButton;
@end
@implementation FLEXMutableFieldEditorViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.getterButton = [[UIBarButtonItem alloc] initWithTitle:[self titleForGetterButton] style:UIBarButtonItemStyleDone target:self action:@selector(getterButtonPressed:)];
self.navigationItem.rightBarButtonItems = @[self.setterButton, self.getterButton];
}
- (void)getterButtonPressed:(id)sender {
// Subclasses can override
[self.fieldEditorView endEditing:YES];
}
- (NSString *)titleForGetterButton {
return @"Get";
}
@end
@@ -1,18 +0,0 @@
//
// FLEXPropertyEditorViewController.h
// Flipboard
//
// Created by Ryan Olson on 5/20/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import "FLEXMutableFieldEditorViewController.h"
#import <objc/runtime.h>
@interface FLEXPropertyEditorViewController : FLEXMutableFieldEditorViewController
- (id)initWithTarget:(id)target property:(objc_property_t)property;
+ (BOOL)canEditProperty:(objc_property_t)property onObject:(id)object currentValue:(id)value;
@end
@@ -1,100 +0,0 @@
//
// FLEXPropertyEditorViewController.m
// Flipboard
//
// Created by Ryan Olson on 5/20/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import "FLEXPropertyEditorViewController.h"
#import "FLEXRuntimeUtility.h"
#import "FLEXFieldEditorView.h"
#import "FLEXArgumentInputView.h"
#import "FLEXArgumentInputViewFactory.h"
#import "FLEXArgumentInputSwitchView.h"
#import "FLEXUtility.h"
@interface FLEXPropertyEditorViewController () <FLEXArgumentInputViewDelegate>
@property (nonatomic) objc_property_t property;
@end
@implementation FLEXPropertyEditorViewController
- (id)initWithTarget:(id)target property:(objc_property_t)property
{
self = [super initWithTarget:target];
if (self) {
self.property = property;
self.title = @"Property";
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.fieldEditorView.fieldDescription = [FLEXRuntimeUtility fullDescriptionForProperty:self.property];
id currentValue = [FLEXRuntimeUtility valueForProperty:self.property onObject:self.target];
self.setterButton.enabled = [[self class] canEditProperty:self.property onObject:self.target currentValue:currentValue];
const char *typeEncoding = [FLEXRuntimeUtility typeEncodingForProperty:self.property].UTF8String;
FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:typeEncoding];
inputView.backgroundColor = self.view.backgroundColor;
inputView.inputValue = [FLEXRuntimeUtility valueForProperty:self.property onObject:self.target];
inputView.delegate = self;
self.fieldEditorView.argumentInputViews = @[inputView];
// Don't show a "set" button for switches - just call the setter immediately after the switch toggles.
if ([inputView isKindOfClass:[FLEXArgumentInputSwitchView class]]) {
self.navigationItem.rightBarButtonItem = nil;
}
}
- (void)actionButtonPressed:(id)sender
{
[super actionButtonPressed:sender];
id userInputObject = self.firstInputView.inputValue;
NSArray *arguments = userInputObject ? @[userInputObject] : nil;
SEL setterSelector = [FLEXRuntimeUtility setterSelectorForProperty:self.property];
NSError *error = nil;
[FLEXRuntimeUtility performSelector:setterSelector onObject:self.target withArguments:arguments error:&error];
if (error) {
[FLEXAlert showAlert:@"Property Setter Failed" message:[error localizedDescription] from:self];
self.firstInputView.inputValue = [FLEXRuntimeUtility valueForProperty:self.property onObject:self.target];
} else {
// If the setter was called without error, pop the view controller to indicate that and make the user's life easier.
// Don't do this for simulated taps on the action button (i.e. from switch/BOOL editors). The experience is weird there.
if (sender) {
[self.navigationController popViewControllerAnimated:YES];
}
}
}
- (void)getterButtonPressed:(id)sender
{
[super getterButtonPressed:sender];
id returnedObject = [FLEXRuntimeUtility valueForProperty:self.property onObject:self.target];
[self exploreObjectOrPopViewController:returnedObject];
}
- (void)argumentInputViewValueDidChange:(FLEXArgumentInputView *)argumentInputView
{
if ([argumentInputView isKindOfClass:[FLEXArgumentInputSwitchView class]]) {
[self actionButtonPressed:nil];
}
}
+ (BOOL)canEditProperty:(objc_property_t)property onObject:(id)object currentValue:(id)value
{
const char *typeEncoding = [FLEXRuntimeUtility typeEncodingForProperty:property].UTF8String;
BOOL canEditType = [FLEXArgumentInputViewFactory canEditFieldWithTypeEncoding:typeEncoding currentValue:value];
SEL setterSelector = [FLEXRuntimeUtility setterSelectorForProperty:property];
BOOL isReadonly = [FLEXRuntimeUtility isReadonlyProperty:property] && (!setterSelector || ![object respondsToSelector:setterSelector]);
return canEditType && !isReadonly;
}
@end
@@ -0,0 +1,35 @@
//
// FLEXVariableEditorViewController.h
// Flipboard
//
// Created by Ryan Olson on 5/16/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
@class FLEXFieldEditorView;
@class FLEXArgumentInputView;
/// Provides a screen for editing or configuring one or more variables.
@interface FLEXVariableEditorViewController : UIViewController
+ (instancetype)target:(id)target;
- (id)initWithTarget:(id)target;
// Convenience accessor since many subclasses only use one input view
@property (nonatomic, readonly) FLEXArgumentInputView *firstInputView;
// For subclass use only.
@property (nonatomic, readonly) id target;
@property (nonatomic, readonly) FLEXFieldEditorView *fieldEditorView;
/// Subclasses can change the button title via the button's \c title property
@property (nonatomic, readonly) UIBarButtonItem *setterButton;
- (void)actionButtonPressed:(id)sender;
/// Pushes an explorer view controller for the given object
/// or pops the current view controller.
- (void)exploreObjectOrPopViewController:(id)objectOrNil;
@end
@@ -0,0 +1,137 @@
//
// FLEXVariableEditorViewController.m
// Flipboard
//
// Created by Ryan Olson on 5/16/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import "FLEXColor.h"
#import "FLEXVariableEditorViewController.h"
#import "FLEXFieldEditorView.h"
#import "FLEXRuntimeUtility.h"
#import "FLEXUtility.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXArgumentInputView.h"
#import "FLEXArgumentInputViewFactory.h"
#import "FLEXObjectExplorerViewController.h"
#import "UIBarButtonItem+FLEX.h"
@interface FLEXVariableEditorViewController () <UIScrollViewDelegate>
@property (nonatomic) UIScrollView *scrollView;
@property (nonatomic) id target;
@end
@implementation FLEXVariableEditorViewController
#pragma mark - Initialization
+ (instancetype)target:(id)target {
return [[self alloc] initWithTarget:target];
}
- (id)initWithTarget:(id)target {
self = [super init];
if (self) {
self.target = target;
[NSNotificationCenter.defaultCenter
addObserver:self selector:@selector(keyboardDidShow:)
name:UIKeyboardDidShowNotification object:nil
];
[NSNotificationCenter.defaultCenter
addObserver:self selector:@selector(keyboardWillHide:)
name:UIKeyboardWillHideNotification object:nil
];
}
return self;
}
- (void)dealloc {
[NSNotificationCenter.defaultCenter removeObserver:self];
}
#pragma mark - UIViewController methods
- (void)keyboardDidShow:(NSNotification *)notification {
CGRect keyboardRectInWindow = [[[notification userInfo] objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue];
CGSize keyboardSize = [self.view convertRect:keyboardRectInWindow fromView:nil].size;
UIEdgeInsets scrollInsets = self.scrollView.contentInset;
scrollInsets.bottom = keyboardSize.height;
self.scrollView.contentInset = scrollInsets;
self.scrollView.scrollIndicatorInsets = scrollInsets;
// Find the active input view and scroll to make sure it's visible.
for (FLEXArgumentInputView *argumentInputView in self.fieldEditorView.argumentInputViews) {
if (argumentInputView.inputViewIsFirstResponder) {
CGRect scrollToVisibleRect = [self.scrollView convertRect:argumentInputView.bounds fromView:argumentInputView];
[self.scrollView scrollRectToVisible:scrollToVisibleRect animated:YES];
break;
}
}
}
- (void)keyboardWillHide:(NSNotification *)notification {
UIEdgeInsets scrollInsets = self.scrollView.contentInset;
scrollInsets.bottom = 0.0;
self.scrollView.contentInset = scrollInsets;
self.scrollView.scrollIndicatorInsets = scrollInsets;
}
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = FLEXColor.scrollViewBackgroundColor;
self.scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds];
self.scrollView.backgroundColor = self.view.backgroundColor;
self.scrollView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
self.scrollView.delegate = self;
[self.view addSubview:self.scrollView];
_fieldEditorView = [FLEXFieldEditorView new];
self.fieldEditorView.targetDescription = [NSString stringWithFormat:@"%@ %p", [self.target class], self.target];
[self.scrollView addSubview:self.fieldEditorView];
_setterButton = [[UIBarButtonItem alloc]
initWithTitle:@"Set"
style:UIBarButtonItemStyleDone
target:self
action:@selector(actionButtonPressed:)
];
self.navigationController.toolbarHidden = NO;
self.toolbarItems = @[UIBarButtonItem.flex_flexibleSpace, self.setterButton];
}
- (void)viewWillLayoutSubviews {
CGSize constrainSize = CGSizeMake(self.scrollView.bounds.size.width, CGFLOAT_MAX);
CGSize fieldEditorSize = [self.fieldEditorView sizeThatFits:constrainSize];
self.fieldEditorView.frame = CGRectMake(0, 0, fieldEditorSize.width, fieldEditorSize.height);
self.scrollView.contentSize = fieldEditorSize;
}
#pragma mark - Public
- (FLEXArgumentInputView *)firstInputView {
return [self.fieldEditorView argumentInputViews].firstObject;
}
- (void)actionButtonPressed:(id)sender {
// Subclasses can override
[self.fieldEditorView endEditing:YES];
}
- (void)exploreObjectOrPopViewController:(id)objectOrNil {
if (objectOrNil) {
// For non-nil (or void) return types, push an explorer view controller to display the object
FLEXObjectExplorerViewController *explorerViewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:objectOrNil];
[self.navigationController pushViewController:explorerViewController animated:YES];
} else {
// If we didn't get a returned object but the method call succeeded,
// pop this view controller off the stack to indicate that the call went through.
[self.navigationController popViewControllerAnimated:YES];
}
}
@end
@@ -0,0 +1,19 @@
//
// FLEXBookmarkManager.h
// FLEX
//
// Created by Tanner on 2/6/20.
// Copyright © 2020 Flipboard. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface FLEXBookmarkManager : NSObject
@property (nonatomic, readonly, class) NSMutableArray *bookmarks;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,25 @@
//
// FLEXBookmarkManager.m
// FLEX
//
// Created by Tanner on 2/6/20.
// Copyright © 2020 Flipboard. All rights reserved.
//
#import "FLEXBookmarkManager.h"
static NSMutableArray *kFLEXBookmarkManagerBookmarks = nil;
@implementation FLEXBookmarkManager
+ (void)initialize {
if (self == [FLEXBookmarkManager class]) {
kFLEXBookmarkManagerBookmarks = [NSMutableArray new];
}
}
+ (NSMutableArray *)bookmarks {
return kFLEXBookmarkManagerBookmarks;
}
@end
@@ -0,0 +1,17 @@
//
// FLEXBookmarksViewController.h
// FLEX
//
// Created by Tanner on 2/6/20.
// Copyright © 2020 Flipboard. All rights reserved.
//
#import "FLEXTableViewController.h"
NS_ASSUME_NONNULL_BEGIN
@interface FLEXBookmarksViewController : FLEXTableViewController
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,235 @@
//
// FLEXBookmarksViewController.m
// FLEX
//
// Created by Tanner on 2/6/20.
// Copyright © 2020 Flipboard. All rights reserved.
//
#import "FLEXBookmarksViewController.h"
#import "FLEXExplorerViewController.h"
#import "FLEXNavigationController.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXBookmarkManager.h"
#import "UIBarButtonItem+FLEX.h"
#import "FLEXColor.h"
#import "FLEXUtility.h"
#import "FLEXRuntimeUtility.h"
#import "FLEXTableView.h"
@interface FLEXBookmarksViewController ()
@property (nonatomic, copy) NSArray *bookmarks;
@property (nonatomic, readonly) FLEXExplorerViewController *corePresenter;
@end
@implementation FLEXBookmarksViewController
#pragma mark - Initialization
- (id)init {
return [self initWithStyle:UITableViewStylePlain];
}
- (void)viewDidLoad {
[super viewDidLoad];
self.navigationController.hidesBarsOnSwipe = NO;
self.tableView.allowsMultipleSelectionDuringEditing = YES;
[self reloadData];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self setupDefaultBarItems];
}
#pragma mark - Private
- (void)reloadData {
// We assume the bookmarks aren't going to change out from under us, since
// presenting any other tool via keyboard shortcuts should dismiss us first
self.bookmarks = FLEXBookmarkManager.bookmarks;
self.title = [NSString stringWithFormat:@"Bookmarks (%@)", @(self.bookmarks.count)];
}
- (void)setupDefaultBarItems {
self.navigationItem.rightBarButtonItem = FLEXBarButtonItemSystem(Done, self, @selector(dismissAnimated));
self.toolbarItems = @[
UIBarButtonItem.flex_flexibleSpace,
FLEXBarButtonItemSystem(Edit, self, @selector(toggleEditing)),
];
// Disable editing if no bookmarks available
self.toolbarItems.lastObject.enabled = self.bookmarks.count > 0;
}
- (void)setupEditingBarItems {
self.navigationItem.rightBarButtonItem = nil;
self.toolbarItems = @[
[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)]
];
self.toolbarItems.firstObject.tintColor = FLEXColor.destructiveColor;
}
- (FLEXExplorerViewController *)corePresenter {
// We must be presented by a FLEXExplorerViewController, or presented
// by another view controller that was presented by FLEXExplorerViewController
FLEXExplorerViewController *presenter = (id)self.presentingViewController;
presenter = (id)presenter.presentingViewController ?: presenter;
presenter = (id)presenter.presentingViewController ?: presenter;
NSAssert(
[presenter isKindOfClass:[FLEXExplorerViewController class]],
@"The bookmarks view controller expects to be presented by the explorer controller"
);
return presenter;
}
#pragma mark Button Actions
- (void)dismissAnimated {
[self dismissAnimated:nil];
}
- (void)dismissAnimated:(id)selectedObject {
if (selectedObject) {
UIViewController *explorer = [FLEXObjectExplorerFactory
explorerViewControllerForObject:selectedObject
];
if ([self.presentingViewController isKindOfClass:[FLEXNavigationController class]]) {
// I am presented on an existing navigation stack, so
// dismiss myself and push the bookmark there
UINavigationController *presenter = (id)self.presentingViewController;
[presenter dismissViewControllerAnimated:YES completion:^{
[presenter pushViewController:explorer animated:YES];
}];
} else {
// Dismiss myself and present explorer
UIViewController *presenter = self.corePresenter;
[presenter dismissViewControllerAnimated:YES completion:^{
[presenter presentViewController:[FLEXNavigationController
withRootViewController:explorer
] animated:YES completion:nil];
}];
}
} else {
// Just dismiss myself
[self dismissViewControllerAnimated:YES completion:nil];
}
}
- (void)toggleEditing {
NSArray<NSIndexPath *> *selected = self.tableView.indexPathsForSelectedRows;
self.editing = !self.editing;
if (self.isEditing) {
[self setupEditingBarItems];
} else {
[self setupDefaultBarItems];
// Get index set of bookmarks to close
NSMutableIndexSet *indexes = [NSMutableIndexSet new];
for (NSIndexPath *ip in selected) {
[indexes addIndex:ip.row];
}
if (selected.count) {
// Close bookmarks and update data source
[FLEXBookmarkManager.bookmarks removeObjectsAtIndexes:indexes];
[self reloadData];
// Remove deleted rows
[self.tableView deleteRowsAtIndexPaths:selected withRowAnimation:UITableViewRowAnimationAutomatic];
}
}
}
- (void)closeAllButtonPressed {
[FLEXAlert makeSheet:^(FLEXAlert *make) {
NSInteger count = self.bookmarks.count;
NSString *title = FLEXPluralFormatString(count, @"Remove %@ bookmarks", @"Remove %@ bookmark");
make.button(title).destructiveStyle().handler(^(NSArray<NSString *> *strings) {
[self closeAll];
[self toggleEditing];
});
make.button(@"Cancel").cancelStyle();
} showFrom:self];
}
- (void)closeAll {
NSInteger rowCount = self.bookmarks.count;
// Close bookmarks and update data source
[FLEXBookmarkManager.bookmarks removeAllObjects];
[self reloadData];
// Delete rows from table view
NSArray<NSIndexPath *> *allRows = [NSArray flex_forEachUpTo:rowCount map:^id(NSUInteger row) {
return [NSIndexPath indexPathForRow:row inSection:0];
}];
[self.tableView deleteRowsAtIndexPaths:allRows withRowAnimation:UITableViewRowAnimationAutomatic];
}
#pragma mark - Table View Data Source
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.bookmarks.count;
}
- (UITableViewCell *)tableView:(FLEXTableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kFLEXDetailCell forIndexPath:indexPath];
id object = self.bookmarks[indexPath.row];
cell.textLabel.text = [FLEXRuntimeUtility safeDescriptionForObject:object];
cell.detailTextLabel.text = [NSString stringWithFormat:@"%@ — %p", [object class], object];
return cell;
}
#pragma mark - Table View Delegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if (self.editing) {
// Case: editing with multi-select
self.toolbarItems.lastObject.title = @"Remove Selected";
self.toolbarItems.lastObject.tintColor = FLEXColor.destructiveColor;
} else {
// Case: selected a bookmark
[self dismissAnimated:self.bookmarks[indexPath.row]];
}
}
- (void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath {
NSParameterAssert(self.editing);
if (tableView.indexPathsForSelectedRows.count == 0) {
self.toolbarItems.lastObject.title = @"Done";
self.toolbarItems.lastObject.tintColor = self.view.tintColor;
}
}
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
return YES;
}
- (void)tableView:(UITableView *)table
commitEditingStyle:(UITableViewCellEditingStyle)edit
forRowAtIndexPath:(NSIndexPath *)indexPath {
NSParameterAssert(edit == UITableViewCellEditingStyleDelete);
// Remove bookmark and update data source
[FLEXBookmarkManager.bookmarks removeObjectAtIndex:indexPath.row];
[self reloadData];
// Delete row from table view
[table deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
}
@end
@@ -8,21 +8,22 @@
#import <UIKit/UIKit.h>
@class FLEXWindow;
@protocol FLEXExplorerViewControllerDelegate;
/// A view controller that manages the FLEX toolbar.
@interface FLEXExplorerViewController : UIViewController
@property (nonatomic, weak) id <FLEXExplorerViewControllerDelegate> delegate;
@property (nonatomic, readonly) BOOL wantsWindowToBecomeKey;
- (BOOL)shouldReceiveTouchAtWindowPoint:(CGPoint)pointInWindowCoordinates;
- (BOOL)wantsWindowToBecomeKey;
/// @brief Used to present (or dismiss) a modal view controller ("tool"), typically triggered by pressing a button in the toolbar.
///
/// If a tool is already presented, this method simply dismisses it and calls the completion block.
/// If no tool is presented, @code future() @endcode is presented and the completion block is called.
- (void)toggleToolWithViewControllerProvider:(UIViewController *(^)(void))future completion:(void(^)(void))completion;
- (void)toggleToolWithViewControllerProvider:(UINavigationController *(^)(void))future completion:(void(^)(void))completion;
// Keyboard shortcut helpers
@@ -30,15 +31,19 @@
- (void)toggleMoveTool;
- (void)toggleViewsTool;
- (void)toggleMenuTool;
- (void)handleDownArrowKeyPressed;
- (void)handleUpArrowKeyPressed;
- (void)handleRightArrowKeyPressed;
- (void)handleLeftArrowKeyPressed;
/// @return YES if the explorer used the key press to perform an action, NO otherwise
- (BOOL)handleDownArrowKeyPressed;
/// @return YES if the explorer used the key press to perform an action, NO otherwise
- (BOOL)handleUpArrowKeyPressed;
/// @return YES if the explorer used the key press to perform an action, NO otherwise
- (BOOL)handleRightArrowKeyPressed;
/// @return YES if the explorer used the key press to perform an action, NO otherwise
- (BOOL)handleLeftArrowKeyPressed;
@end
#pragma mark -
@protocol FLEXExplorerViewControllerDelegate <NSObject>
- (void)explorerViewControllerDidFinish:(FLEXExplorerViewController *)explorerViewController;
@end
@@ -10,11 +10,16 @@
#import "FLEXExplorerToolbar.h"
#import "FLEXToolbarItem.h"
#import "FLEXUtility.h"
#import "FLEXHierarchyTableViewController.h"
#import "FLEXGlobalsTableViewController.h"
#import "FLEXWindow.h"
#import "FLEXTabList.h"
#import "FLEXNavigationController.h"
#import "FLEXHierarchyViewController.h"
#import "FLEXGlobalsViewController.h"
#import "FLEXObjectExplorerViewController.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXNetworkHistoryTableViewController.h"
#import "FLEXNetworkMITMViewController.h"
#import "FLEXTabsViewController.h"
#import "FLEXWindowManagerController.h"
static NSString *const kFLEXToolbarTopMarginDefaultsKey = @"com.flex.FLEXToolbar.topMargin";
@@ -24,7 +29,7 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
FLEXExplorerModeMove
};
@interface FLEXExplorerViewController () <FLEXHierarchyTableViewControllerDelegate, FLEXGlobalsTableViewControllerDelegate>
@interface FLEXExplorerViewController () <FLEXHierarchyDelegate, UIAdaptivePresentationControllerDelegate>
@property (nonatomic) FLEXExplorerToolbar *explorerToolbar;
@@ -56,20 +61,20 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
/// A colored transparent overlay to indicate that the view is selected.
@property (nonatomic) UIView *selectedViewOverlay;
/// Tracked so we can restore the key window after dismissing a modal.
/// We need to become key after modal presentation so we can correctly capture input.
/// If we're just showing the toolbar, we want the main app's window to remain key so that we don't interfere with input, status bar, etc.
@property (nonatomic) UIWindow *previousKeyWindow;
/// self.view.window as a \c FLEXWindow
@property (nonatomic, readonly) FLEXWindow *window;
/// All views that we're KVOing. Used to help us clean up properly.
@property (nonatomic) NSMutableSet<UIView *> *observedViews;
/// Used to preserve the target app's UIMenuController items.
@property (nonatomic) NSArray<UIMenuItem *> *appMenuItems;
@end
@implementation FLEXExplorerViewController
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
self.observedViews = [NSMutableSet set];
@@ -77,15 +82,13 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
return self;
}
-(void)dealloc
{
- (void)dealloc {
for (UIView *view in _observedViews) {
[self stopObservingView:view];
}
}
- (void)viewDidLoad
{
- (void)viewDidLoad {
[super viewDidLoad];
// Toolbar
@@ -96,15 +99,23 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
CGFloat toolbarOriginY = toolbarOriginYDefault ? [toolbarOriginYDefault doubleValue] : 100;
CGRect safeArea = [self viewSafeArea];
CGSize toolbarSize = [self.explorerToolbar sizeThatFits:CGSizeMake(CGRectGetWidth(self.view.bounds), CGRectGetHeight(safeArea))];
[self updateToolbarPositionWithUnconstrainedFrame:CGRectMake(CGRectGetMinX(safeArea), toolbarOriginY, toolbarSize.width, toolbarSize.height)];
self.explorerToolbar.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleTopMargin;
CGSize toolbarSize = [self.explorerToolbar sizeThatFits:CGSizeMake(
CGRectGetWidth(self.view.bounds), CGRectGetHeight(safeArea)
)];
[self updateToolbarPositionWithUnconstrainedFrame:CGRectMake(
CGRectGetMinX(safeArea), toolbarOriginY, toolbarSize.width, toolbarSize.height
)];
self.explorerToolbar.autoresizingMask = UIViewAutoresizingFlexibleWidth |
UIViewAutoresizingFlexibleBottomMargin |
UIViewAutoresizingFlexibleTopMargin;
[self.view addSubview:self.explorerToolbar];
[self setupToolbarActions];
[self setupToolbarGestures];
// View selection
UITapGestureRecognizer *selectionTapGR = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleSelectionTap:)];
UITapGestureRecognizer *selectionTapGR = [[UITapGestureRecognizer alloc]
initWithTarget:self action:@selector(handleSelectionTap:)
];
[self.view addGestureRecognizer:selectionTapGR];
// View moving
@@ -113,8 +124,7 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
[self.view addGestureRecognizer:self.movePanGR];
}
- (void)viewWillAppear:(BOOL)animated
{
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self updateButtonStates];
@@ -123,12 +133,12 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
#pragma mark - Rotation
- (UIViewController *)viewControllerForRotationAndOrientation
{
UIWindow *window = self.previousKeyWindow ?: [UIApplication.sharedApplication keyWindow];
UIViewController *viewController = window.rootViewController;
- (UIViewController *)viewControllerForRotationAndOrientation {
UIViewController *viewController = FLEXUtility.appKeyWindow.rootViewController;
// Obfuscating selector _viewControllerForSupportedInterfaceOrientations
NSString *viewControllerSelectorString = [@[@"_vie", @"wContro", @"llerFor", @"Supported", @"Interface", @"Orientations"] componentsJoinedByString:@""];
NSString *viewControllerSelectorString = [@[
@"_vie", @"wContro", @"llerFor", @"Supported", @"Interface", @"Orientations"
] componentsJoinedByString:@""];
SEL viewControllerSelector = NSSelectorFromString(viewControllerSelectorString);
if ([viewController respondsToSelector:viewControllerSelector]) {
viewController = [viewController valueForKey:viewControllerSelectorString];
@@ -137,11 +147,10 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
return viewController;
}
- (UIInterfaceOrientationMask)supportedInterfaceOrientations
{
- (UIInterfaceOrientationMask)supportedInterfaceOrientations {
UIViewController *viewControllerToAsk = [self viewControllerForRotationAndOrientation];
UIInterfaceOrientationMask supportedOrientations = [FLEXUtility infoPlistSupportedInterfaceOrientationsMask];
if (viewControllerToAsk && viewControllerToAsk != self) {
if (viewControllerToAsk && ![viewControllerToAsk isKindOfClass:[self class]]) {
supportedOrientations = [viewControllerToAsk supportedInterfaceOrientations];
}
@@ -155,8 +164,7 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
return supportedOrientations;
}
- (BOOL)shouldAutorotate
{
- (BOOL)shouldAutorotate {
UIViewController *viewControllerToAsk = [self viewControllerForRotationAndOrientation];
BOOL shouldAutorotate = YES;
if (viewControllerToAsk && viewControllerToAsk != self) {
@@ -165,8 +173,9 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
return shouldAutorotate;
}
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
- (void)viewWillTransitionToSize:(CGSize)size
withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator {
[super viewWillTransitionToSize:size withTransitionCoordinator:coordinator];
[coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
for (UIView *outlineView in self.outlineViewsForVisibleViews.allValues) {
outlineView.hidden = YES;
@@ -189,10 +198,10 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
}];
}
#pragma mark - Setter Overrides
- (void)setSelectedView:(UIView *)selectedView
{
- (void)setSelectedView:(UIView *)selectedView {
if (![_selectedView isEqual:selectedView]) {
if (![self.viewsAtTapPoint containsObject:_selectedView]) {
[self stopObservingView:_selectedView];
@@ -203,8 +212,12 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
[self beginObservingView:selectedView];
// Update the toolbar and selected overlay
self.explorerToolbar.selectedViewDescription = [FLEXUtility descriptionForView:selectedView includingFrame:YES];
self.explorerToolbar.selectedViewOverlayColor = [FLEXUtility consistentRandomColorForObject:selectedView];
self.explorerToolbar.selectedViewDescription = [FLEXUtility
descriptionForView:selectedView includingFrame:YES
];
self.explorerToolbar.selectedViewOverlayColor = [FLEXUtility
consistentRandomColorForObject:selectedView
];
if (selectedView) {
if (!self.selectedViewOverlay) {
@@ -217,7 +230,8 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
self.selectedViewOverlay.layer.borderColor = outlineColor.CGColor;
self.selectedViewOverlay.frame = [self.view convertRect:selectedView.bounds fromView:selectedView];
// Make sure the selected overlay is in front of all the other subviews except the toolbar, which should always stay on top.
// Make sure the selected overlay is in front of all the other subviews
// except the toolbar, which should always stay on top.
[self.view bringSubviewToFront:self.selectedViewOverlay];
[self.view bringSubviewToFront:self.explorerToolbar];
} else {
@@ -230,8 +244,7 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
}
}
- (void)setViewsAtTapPoint:(NSArray<UIView *> *)viewsAtTapPoint
{
- (void)setViewsAtTapPoint:(NSArray<UIView *> *)viewsAtTapPoint {
if (![_viewsAtTapPoint isEqual:viewsAtTapPoint]) {
for (UIView *view in _viewsAtTapPoint) {
if (view != self.selectedView) {
@@ -247,8 +260,7 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
}
}
- (void)setCurrentMode:(FLEXExplorerMode)currentMode
{
- (void)setCurrentMode:(FLEXExplorerMode)currentMode {
if (_currentMode != currentMode) {
_currentMode = currentMode;
switch (currentMode) {
@@ -267,7 +279,8 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
break;
case FLEXExplorerModeMove:
// Hide all the outline views to focus on the selected view, which is the only one that will move.
// Hide all the outline views to focus on the selected view,
// which is the only one that will move.
for (NSValue *key in self.outlineViewsForVisibleViews) {
UIView *outlineView = self.outlineViewsForVisibleViews[key];
outlineView.hidden = YES;
@@ -282,35 +295,32 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
#pragma mark - View Tracking
- (void)beginObservingView:(UIView *)view
{
- (void)beginObservingView:(UIView *)view {
// Bail if we're already observing this view or if there's nothing to observe.
if (!view || [self.observedViews containsObject:view]) {
return;
}
for (NSString *keyPath in [[self class] viewKeyPathsToTrack]) {
for (NSString *keyPath in self.viewKeyPathsToTrack) {
[view addObserver:self forKeyPath:keyPath options:0 context:NULL];
}
[self.observedViews addObject:view];
}
- (void)stopObservingView:(UIView *)view
{
- (void)stopObservingView:(UIView *)view {
if (!view) {
return;
}
for (NSString *keyPath in [[self class] viewKeyPathsToTrack]) {
for (NSString *keyPath in self.viewKeyPathsToTrack) {
[view removeObserver:self forKeyPath:keyPath];
}
[self.observedViews removeObject:view];
}
+ (NSArray<NSString *> *)viewKeyPathsToTrack
{
- (NSArray<NSString *> *)viewKeyPathsToTrack {
static NSArray<NSString *> *trackedViewKeyPaths = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
@@ -320,13 +330,13 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
return trackedViewKeyPaths;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *, id> *)change context:(void *)context
{
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object
change:(NSDictionary<NSString *, id> *)change
context:(void *)context {
[self updateOverlayAndDescriptionForObjectIfNeeded:object];
}
- (void)updateOverlayAndDescriptionForObjectIfNeeded:(id)object
{
- (void)updateOverlayAndDescriptionForObjectIfNeeded:(id)object {
NSUInteger indexOfView = [self.viewsAtTapPoint indexOfObject:object];
if (indexOfView != NSNotFound) {
UIView *view = self.viewsAtTapPoint[indexOfView];
@@ -338,79 +348,66 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
}
if (object == self.selectedView) {
// Update the selected view description since we show the frame value there.
self.explorerToolbar.selectedViewDescription = [FLEXUtility descriptionForView:self.selectedView includingFrame:YES];
self.explorerToolbar.selectedViewDescription = [FLEXUtility
descriptionForView:self.selectedView includingFrame:YES
];
CGRect selectedViewOutlineFrame = [self frameInLocalCoordinatesForView:self.selectedView];
self.selectedViewOverlay.frame = selectedViewOutlineFrame;
}
}
- (CGRect)frameInLocalCoordinatesForView:(UIView *)view
{
// First convert to window coordinates since the view may be in a different window than our view.
- (CGRect)frameInLocalCoordinatesForView:(UIView *)view {
// Convert to window coordinates since the view may be in a different window than our view
CGRect frameInWindow = [view convertRect:view.bounds toView:nil];
// Then convert from the window to our view's coordinate space.
// Convert from the window to our view's coordinate space
return [self.view convertRect:frameInWindow fromView:nil];
}
#pragma mark - Toolbar Buttons
- (void)setupToolbarActions
{
[self.explorerToolbar.selectItem addTarget:self action:@selector(selectButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
[self.explorerToolbar.hierarchyItem addTarget:self action:@selector(hierarchyButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
[self.explorerToolbar.moveItem addTarget:self action:@selector(moveButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
[self.explorerToolbar.globalsItem addTarget:self action:@selector(globalsButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
[self.explorerToolbar.closeItem addTarget:self action:@selector(closeButtonTapped:) forControlEvents:UIControlEventTouchUpInside];
- (void)setupToolbarActions {
FLEXExplorerToolbar *toolbar = self.explorerToolbar;
NSDictionary<NSString *, FLEXToolbarItem *> *actionsToItems = @{
NSStringFromSelector(@selector(selectButtonTapped:)): toolbar.selectItem,
NSStringFromSelector(@selector(hierarchyButtonTapped:)): toolbar.hierarchyItem,
NSStringFromSelector(@selector(moveButtonTapped:)): toolbar.moveItem,
NSStringFromSelector(@selector(globalsButtonTapped:)): toolbar.globalsItem,
NSStringFromSelector(@selector(closeButtonTapped:)): toolbar.closeItem,
};
[actionsToItems enumerateKeysAndObjectsUsingBlock:^(NSString *sel, FLEXToolbarItem *item, BOOL *stop) {
[item addTarget:self action:NSSelectorFromString(sel) forControlEvents:UIControlEventTouchUpInside];
}];
}
- (void)selectButtonTapped:(FLEXToolbarItem *)sender
{
- (void)selectButtonTapped:(FLEXToolbarItem *)sender {
[self toggleSelectTool];
}
- (void)hierarchyButtonTapped:(FLEXToolbarItem *)sender
{
- (void)hierarchyButtonTapped:(FLEXToolbarItem *)sender {
[self toggleViewsTool];
}
- (NSArray<UIView *> *)allViewsInHierarchy
{
NSMutableArray<UIView *> *allViews = [NSMutableArray array];
NSArray<UIWindow *> *windows = [FLEXUtility allWindows];
for (UIWindow *window in windows) {
if (window != self.view.window) {
[allViews addObject:window];
[allViews addObjectsFromArray:[self allRecursiveSubviewsInView:window]];
}
}
return allViews;
}
- (UIWindow *)statusWindow
{
- (UIWindow *)statusWindow {
NSString *statusBarString = [NSString stringWithFormat:@"%@arWindow", @"_statusB"];
return [UIApplication.sharedApplication valueForKey:statusBarString];
}
- (void)moveButtonTapped:(FLEXToolbarItem *)sender
{
- (void)moveButtonTapped:(FLEXToolbarItem *)sender {
[self toggleMoveTool];
}
- (void)globalsButtonTapped:(FLEXToolbarItem *)sender
{
- (void)globalsButtonTapped:(FLEXToolbarItem *)sender {
[self toggleMenuTool];
}
- (void)closeButtonTapped:(FLEXToolbarItem *)sender
{
- (void)closeButtonTapped:(FLEXToolbarItem *)sender {
self.currentMode = FLEXExplorerModeDefault;
[self.delegate explorerViewControllerDidFinish:self];
}
- (void)updateButtonStates
{
- (void)updateButtonStates {
// Move and details only active when an object is selected.
BOOL hasSelectedObject = self.selectedView != nil;
self.explorerToolbar.moveItem.enabled = hasSelectedObject;
@@ -421,23 +418,37 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
#pragma mark - Toolbar Dragging
- (void)setupToolbarGestures
{
- (void)setupToolbarGestures {
FLEXExplorerToolbar *toolbar = self.explorerToolbar;
// Pan gesture for dragging.
UIPanGestureRecognizer *panGR = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleToolbarPanGesture:)];
[self.explorerToolbar.dragHandle addGestureRecognizer:panGR];
[toolbar.dragHandle addGestureRecognizer:[[UIPanGestureRecognizer alloc]
initWithTarget:self action:@selector(handleToolbarPanGesture:)
]];
// Tap gesture for hinting.
UITapGestureRecognizer *hintTapGR = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleToolbarHintTapGesture:)];
[self.explorerToolbar.dragHandle addGestureRecognizer:hintTapGR];
[toolbar.dragHandle addGestureRecognizer:[[UITapGestureRecognizer alloc]
initWithTarget:self action:@selector(handleToolbarHintTapGesture:)
]];
// Tap gesture for showing additional details
self.detailsTapGR = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(handleToolbarDetailsTapGesture:)];
[self.explorerToolbar.selectedViewDescriptionContainer addGestureRecognizer:self.detailsTapGR];
self.detailsTapGR = [[UITapGestureRecognizer alloc]
initWithTarget:self action:@selector(handleToolbarDetailsTapGesture:)
];
[toolbar.selectedViewDescriptionContainer addGestureRecognizer:self.detailsTapGR];
// Long press gesture to present tabs manager
[toolbar.globalsItem addGestureRecognizer:[[UILongPressGestureRecognizer alloc]
initWithTarget:self action:@selector(handleToolbarShowTabsGesture:)
]];
// Long press gesture to present window manager
[toolbar.selectItem addGestureRecognizer:[[UILongPressGestureRecognizer alloc]
initWithTarget:self action:@selector(handleToolbarWindowManagerGesture:)
]];
}
- (void)handleToolbarPanGesture:(UIPanGestureRecognizer *)panGR
{
- (void)handleToolbarPanGesture:(UIPanGestureRecognizer *)panGR {
switch (panGR.state) {
case UIGestureRecognizerStateBegan:
self.toolbarFrameBeforeDragging = self.explorerToolbar.frame;
@@ -454,8 +465,7 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
}
}
- (void)updateToolbarPositionWithDragGesture:(UIPanGestureRecognizer *)panGR
{
- (void)updateToolbarPositionWithDragGesture:(UIPanGestureRecognizer *)panGR {
CGPoint translation = [panGR translationInView:self.view];
CGRect newToolbarFrame = self.toolbarFrameBeforeDragging;
newToolbarFrame.origin.y += translation.y;
@@ -463,10 +473,10 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
[self updateToolbarPositionWithUnconstrainedFrame:newToolbarFrame];
}
- (void)updateToolbarPositionWithUnconstrainedFrame:(CGRect)unconstrainedFrame
{
- (void)updateToolbarPositionWithUnconstrainedFrame:(CGRect)unconstrainedFrame {
CGRect safeArea = [self viewSafeArea];
// We only constrain the Y-axis because We want the toolbar to handle the X-axis safeArea layout by itself
// We only constrain the Y-axis because we want the toolbar
// to handle the X-axis safeArea layout by itself
CGFloat minY = CGRectGetMinY(safeArea);
CGFloat maxY = CGRectGetMaxY(safeArea) - unconstrainedFrame.size.height;
if (unconstrainedFrame.origin.y < minY) {
@@ -477,11 +487,12 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
self.explorerToolbar.frame = unconstrainedFrame;
[[NSUserDefaults standardUserDefaults] setDouble:unconstrainedFrame.origin.y forKey:kFLEXToolbarTopMarginDefaultsKey];
[NSUserDefaults.standardUserDefaults
setDouble:unconstrainedFrame.origin.y forKey:kFLEXToolbarTopMarginDefaultsKey
];
}
- (void)handleToolbarHintTapGesture:(UITapGestureRecognizer *)tapGR
{
- (void)handleToolbarHintTapGesture:(UITapGestureRecognizer *)tapGR {
// Bounce the toolbar to indicate that it is draggable.
// TODO: make it bouncier.
if (tapGR.state == UIGestureRecognizerStateRecognized) {
@@ -500,24 +511,41 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
}
}
- (void)handleToolbarDetailsTapGesture:(UITapGestureRecognizer *)tapGR
{
- (void)handleToolbarDetailsTapGesture:(UITapGestureRecognizer *)tapGR {
if (tapGR.state == UIGestureRecognizerStateRecognized && self.selectedView) {
FLEXObjectExplorerViewController *selectedViewExplorer = [FLEXObjectExplorerFactory explorerViewControllerForObject:self.selectedView];
selectedViewExplorer.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(selectedViewExplorerFinished:)];
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:selectedViewExplorer];
[self makeKeyAndPresentViewController:navigationController animated:YES completion:nil];
UIViewController *topStackVC = [FLEXObjectExplorerFactory explorerViewControllerForObject:self.selectedView];
[self presentViewController:
[FLEXNavigationController withRootViewController:topStackVC]
animated:YES completion:nil];
}
}
- (void)handleToolbarShowTabsGesture:(UILongPressGestureRecognizer *)sender {
// Back up the UIMenuController items since dismissViewController: will attempt to replace them
self.appMenuItems = UIMenuController.sharedMenuController.menuItems;
[super presentViewController:[[UINavigationController alloc]
initWithRootViewController:[FLEXTabsViewController new]
] animated:YES completion:nil];
}
- (void)handleToolbarWindowManagerGesture:(UILongPressGestureRecognizer *)sender {
// Back up the UIMenuController items since dismissViewController: will attempt to replace them
self.appMenuItems = UIMenuController.sharedMenuController.menuItems;
[super presentViewController:[[UINavigationController alloc]
initWithRootViewController:[FLEXWindowManagerController new]
] animated:YES completion:nil];
}
#pragma mark - View Selection
- (void)handleSelectionTap:(UITapGestureRecognizer *)tapGR
{
- (void)handleSelectionTap:(UITapGestureRecognizer *)tapGR {
// Only if we're in selection mode
if (self.currentMode == FLEXExplorerModeSelect && tapGR.state == UIGestureRecognizerStateRecognized) {
// Note that [tapGR locationInView:nil] is broken in iOS 8, so we have to do a two step conversion to window coordinates.
// Note that [tapGR locationInView:nil] is broken in iOS 8,
// so we have to do a two step conversion to window coordinates.
// Thanks to @lascorbe for finding this: https://github.com/Flipboard/FLEX/pull/31
CGPoint tapPointInView = [tapGR locationInView:self.view];
CGPoint tapPointInWindow = [self.view convertPoint:tapPointInView toView:nil];
@@ -525,8 +553,7 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
}
}
- (void)updateOutlineViewsForSelectionPoint:(CGPoint)selectionPointInWindow
{
- (void)updateOutlineViewsForSelectionPoint:(CGPoint)selectionPointInWindow {
[self removeAndClearOutlineViews];
// Include hidden views in the "viewsAtTapPoint" array so we can show them in the hierarchy list.
@@ -535,7 +562,7 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
// For outlined views and the selected view, only use visible views.
// Outlining hidden views adds clutter and makes the selection behavior confusing.
NSArray<UIView *> *visibleViewsAtTapPoint = [self viewsAtPoint:selectionPointInWindow skipHiddenViews:YES];
NSMutableDictionary<NSValue *, UIView *> *newOutlineViewsForVisibleViews = [NSMutableDictionary dictionary];
NSMutableDictionary<NSValue *, UIView *> *newOutlineViewsForVisibleViews = [NSMutableDictionary new];
for (UIView *view in visibleViewsAtTapPoint) {
UIView *outlineView = [self outlineViewForView:view];
[self.view addSubview:outlineView];
@@ -551,8 +578,7 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
[self updateButtonStates];
}
- (UIView *)outlineViewForView:(UIView *)view
{
- (UIView *)outlineViewForView:(UIView *)view {
CGRect outlineFrame = [self frameInLocalCoordinatesForView:view];
UIView *outlineView = [[UIView alloc] initWithFrame:outlineFrame];
outlineView.backgroundColor = UIColor.clearColor;
@@ -561,8 +587,7 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
return outlineView;
}
- (void)removeAndClearOutlineViews
{
- (void)removeAndClearOutlineViews {
for (NSValue *key in self.outlineViewsForVisibleViews) {
UIView *outlineView = self.outlineViewsForVisibleViews[key];
[outlineView removeFromSuperview];
@@ -570,22 +595,23 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
self.outlineViewsForVisibleViews = nil;
}
- (NSArray<UIView *> *)viewsAtPoint:(CGPoint)tapPointInWindow skipHiddenViews:(BOOL)skipHidden
{
- (NSArray<UIView *> *)viewsAtPoint:(CGPoint)tapPointInWindow skipHiddenViews:(BOOL)skipHidden {
NSMutableArray<UIView *> *views = [NSMutableArray array];
for (UIWindow *window in [FLEXUtility allWindows]) {
// Don't include the explorer's own window or subviews.
if (window != self.view.window && [window pointInside:tapPointInWindow withEvent:nil]) {
[views addObject:window];
[views addObjectsFromArray:[self recursiveSubviewsAtPoint:tapPointInWindow inView:window skipHiddenViews:skipHidden]];
[views addObjectsFromArray:[self
recursiveSubviewsAtPoint:tapPointInWindow inView:window skipHiddenViews:skipHidden
]];
}
}
return views;
}
- (UIView *)viewForSelectionAtPoint:(CGPoint)tapPointInWindow
{
// Select in the window that would handle the touch, but don't just use the result of hitTest:withEvent: so we can still select views with interaction disabled.
- (UIView *)viewForSelectionAtPoint:(CGPoint)tapPointInWindow {
// Select in the window that would handle the touch, but don't just use the result of
// hitTest:withEvent: so we can still select views with interaction disabled.
// Default to the the application's key window if none of the windows want the touch.
UIWindow *windowForSelection = [UIApplication.sharedApplication keyWindow];
for (UIWindow *window in [FLEXUtility allWindows].reverseObjectEnumerator) {
@@ -602,8 +628,9 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
return [self recursiveSubviewsAtPoint:tapPointInWindow inView:windowForSelection skipHiddenViews:YES].lastObject;
}
- (NSArray<UIView *> *)recursiveSubviewsAtPoint:(CGPoint)pointInView inView:(UIView *)view skipHiddenViews:(BOOL)skipHidden
{
- (NSArray<UIView *> *)recursiveSubviewsAtPoint:(CGPoint)pointInView
inView:(UIView *)view
skipHiddenViews:(BOOL)skipHidden {
NSMutableArray<UIView *> *subviewsAtPoint = [NSMutableArray array];
for (UIView *subview in view.subviews) {
BOOL isHidden = subview.hidden || subview.alpha < 0.01;
@@ -616,46 +643,22 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
[subviewsAtPoint addObject:subview];
}
// If this view doesn't clip to its bounds, we need to check its subviews even if it doesn't contain the selection point.
// They may be visible and contain the selection point.
// If this view doesn't clip to its bounds, we need to check its subviews even if it
// doesn't contain the selection point. They may be visible and contain the selection point.
if (subviewContainsPoint || !subview.clipsToBounds) {
CGPoint pointInSubview = [view convertPoint:pointInView toView:subview];
[subviewsAtPoint addObjectsFromArray:[self recursiveSubviewsAtPoint:pointInSubview inView:subview skipHiddenViews:skipHidden]];
[subviewsAtPoint addObjectsFromArray:[self
recursiveSubviewsAtPoint:pointInSubview inView:subview skipHiddenViews:skipHidden
]];
}
}
return subviewsAtPoint;
}
- (NSArray<UIView *> *)allRecursiveSubviewsInView:(UIView *)view
{
NSMutableArray<UIView *> *subviews = [NSMutableArray array];
for (UIView *subview in view.subviews) {
[subviews addObject:subview];
[subviews addObjectsFromArray:[self allRecursiveSubviewsInView:subview]];
}
return subviews;
}
- (NSDictionary<NSValue *, NSNumber *> *)hierarchyDepthsForViews:(NSArray<UIView *> *)views
{
NSMutableDictionary<NSValue *, NSNumber *> *hierarchyDepths = [NSMutableDictionary dictionary];
for (UIView *view in views) {
NSInteger depth = 0;
UIView *tryView = view;
while (tryView.superview) {
tryView = tryView.superview;
depth++;
}
[hierarchyDepths setObject:@(depth) forKey:[NSValue valueWithNonretainedObject:view]];
}
return hierarchyDepths;
}
#pragma mark - Selected View Moving
- (void)handleMovePan:(UIPanGestureRecognizer *)movePanGR
{
- (void)handleMovePan:(UIPanGestureRecognizer *)movePanGR {
switch (movePanGR.state) {
case UIGestureRecognizerStateBegan:
self.selectedViewFrameBeforeDragging = self.selectedView.frame;
@@ -672,8 +675,7 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
}
}
- (void)updateSelectedViewPositionWithDragGesture:(UIPanGestureRecognizer *)movePanGR
{
- (void)updateSelectedViewPositionWithDragGesture:(UIPanGestureRecognizer *)movePanGR {
CGPoint translation = [movePanGR translationInView:self.selectedView.superview];
CGRect newSelectedViewFrame = self.selectedViewFrameBeforeDragging;
newSelectedViewFrame.origin.x = FLEXFloor(newSelectedViewFrame.origin.x + translation.x);
@@ -684,8 +686,7 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
#pragma mark - Safe Area Handling
- (CGRect)viewSafeArea
{
- (CGRect)viewSafeArea {
CGRect safeArea = self.view.bounds;
if (@available(iOS 11.0, *)) {
safeArea = UIEdgeInsetsInsetRect(self.view.bounds, self.view.safeAreaInsets);
@@ -694,22 +695,27 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
return safeArea;
}
- (void)viewSafeAreaInsetsDidChange
{
- (void)viewSafeAreaInsetsDidChange {
if (@available(iOS 11.0, *)) {
[super viewSafeAreaInsetsDidChange];
CGRect safeArea = [self viewSafeArea];
CGSize toolbarSize = [self.explorerToolbar sizeThatFits:CGSizeMake(CGRectGetWidth(self.view.bounds), CGRectGetHeight(safeArea))];
[self updateToolbarPositionWithUnconstrainedFrame:CGRectMake(CGRectGetMinX(self.explorerToolbar.frame), CGRectGetMinY(self.explorerToolbar.frame), toolbarSize.width, toolbarSize.height)];
CGSize toolbarSize = [self.explorerToolbar sizeThatFits:CGSizeMake(
CGRectGetWidth(self.view.bounds), CGRectGetHeight(safeArea)
)];
[self updateToolbarPositionWithUnconstrainedFrame:CGRectMake(
CGRectGetMinX(self.explorerToolbar.frame),
CGRectGetMinY(self.explorerToolbar.frame),
toolbarSize.width,
toolbarSize.height)
];
}
}
#pragma mark - Touch Handling
- (BOOL)shouldReceiveTouchAtWindowPoint:(CGPoint)pointInWindowCoordinates
{
- (BOOL)shouldReceiveTouchAtWindowPoint:(CGPoint)pointInWindowCoordinates {
BOOL shouldReceiveTouch = NO;
CGPoint pointInLocalCoordinates = [self.view convertPoint:pointInWindowCoordinates fromView:nil];
@@ -738,12 +744,11 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
}
#pragma mark - FLEXHierarchyTableViewControllerDelegate
#pragma mark - FLEXHierarchyDelegate
- (void)hierarchyViewController:(FLEXHierarchyTableViewController *)hierarchyViewController didFinishWithSelectedView:(UIView *)selectedView
{
// Note that we need to wait until the view controller is dismissed to calculated the frame of the outline view.
// Otherwise the coordinate conversion doesn't give the correct result.
- (void)viewHierarchyDidDismiss:(UIView *)selectedView {
// Note that we need to wait until the view controller is dismissed to calculate the frame
// of the outline view, otherwise the coordinate conversion doesn't give the correct result.
[self toggleViewsToolWithCompletion:^{
// If the selected view is outside of the tap point array (selected from "Full Hierarchy"),
// then clear out the tap point array and remove all the outline views.
@@ -763,85 +768,74 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
}
#pragma mark - FLEXGlobalsViewControllerDelegate
- (void)globalsViewControllerDidFinish:(FLEXGlobalsTableViewController *)globalsViewController
{
[self resignKeyAndDismissViewControllerAnimated:YES completion:nil];
}
#pragma mark - FLEXObjectExplorerViewController Done Action
- (void)selectedViewExplorerFinished:(id)sender
{
[self resignKeyAndDismissViewControllerAnimated:YES completion:nil];
}
#pragma mark - Modal Presentation and Window Management
- (void)makeKeyAndPresentViewController:(UIViewController *)viewController animated:(BOOL)animated completion:(void (^)(void))completion
{
// Save the current key window so we can restore it following dismissal.
self.previousKeyWindow = UIApplication.sharedApplication.keyWindow;
- (void)presentViewController:(UIViewController *)toPresent
animated:(BOOL)animated
completion:(void (^)(void))completion {
// Make our window key to correctly handle input.
[self.view.window makeKeyWindow];
// Fix for iOS 13, regarding custom UIMenu callouts not appearing because
// the UITextEffectsWindow has a lower level than the FLEX window by default
// until a text field is activated, bringing it above the FLEX window.
if (@available(iOS 13, *)) {
for (UIWindow *window in UIApplication.sharedApplication.windows) {
if ([window isKindOfClass:NSClassFromString(@"UITextEffectsWindow")]) {
if (window.windowLevel <= self.view.window.windowLevel) {
window.windowLevel = self.view.window.windowLevel + 1;
break;
}
}
}
}
// Move the status bar on top of FLEX so we can get scroll to top behavior for taps.
[[self statusWindow] setWindowLevel:self.view.window.windowLevel + 1.0];
if (!@available(iOS 13, *)) {
[self statusWindow].windowLevel = self.view.window.windowLevel + 1.0;
}
// Back up and replace the UIMenuController items
self.appMenuItems = UIMenuController.sharedMenuController.menuItems;
// Initialize custom menu items for explorer screen
UIMenuItem *copyObjectAddress = [[UIMenuItem alloc]
initWithTitle:@"Copy Address"
action:NSSelectorFromString(@"copyObjectAddress:")
];
UIMenuController.sharedMenuController.menuItems = @[copyObjectAddress];
[UIMenuController.sharedMenuController update];
// Show the view controller.
[self presentViewController:viewController animated:animated completion:completion];
[super presentViewController:toPresent animated:animated completion:completion];
}
- (void)resignKeyAndDismissViewControllerAnimated:(BOOL)animated completion:(void (^)(void))completion
{
UIWindow *previousKeyWindow = self.previousKeyWindow;
self.previousKeyWindow = nil;
[previousKeyWindow makeKeyWindow];
[[previousKeyWindow rootViewController] setNeedsStatusBarAppearanceUpdate];
- (void)dismissViewControllerAnimated:(BOOL)animated completion:(void (^)(void))completion {
UIWindow *appWindow = self.window.previousKeyWindow;
[appWindow makeKeyWindow];
[appWindow.rootViewController setNeedsStatusBarAppearanceUpdate];
// Restore previous UIMenuController items
// Back up and replace the UIMenuController items
UIMenuController.sharedMenuController.menuItems = self.appMenuItems;
[UIMenuController.sharedMenuController update];
self.appMenuItems = nil;
// Restore the status bar window's normal window level.
// We want it above FLEX while a modal is presented for scroll to top, but below FLEX otherwise for exploration.
[[self statusWindow] setWindowLevel:UIWindowLevelStatusBar];
// We want it above FLEX while a modal is presented for
// scroll to top, but below FLEX otherwise for exploration.
[self statusWindow].windowLevel = UIWindowLevelStatusBar;
[self dismissViewControllerAnimated:animated completion:completion];
[super dismissViewControllerAnimated:animated completion:completion];
}
- (BOOL)wantsWindowToBecomeKey
{
return self.previousKeyWindow != nil;
return self.window.previousKeyWindow != nil;
}
- (void)toggleToolWithViewControllerProvider:(UIViewController *(^)(void))future completion:(void(^)(void))completion
{
- (void)toggleToolWithViewControllerProvider:(UINavigationController *(^)(void))future
completion:(void(^)(void))completion {
if (self.presentedViewController) {
[self resignKeyAndDismissViewControllerAnimated:YES completion:completion];
[self dismissViewControllerAnimated:YES completion:completion];
} else if (future) {
[self makeKeyAndPresentViewController:future() animated:YES completion:completion];
[self presentViewController:future() animated:YES completion:completion];
}
}
- (FLEXWindow *)window {
return (id)self.view.window;
}
#pragma mark - Keyboard Shortcut Helpers
- (void)toggleSelectTool
{
- (void)toggleSelectTool {
if (self.currentMode == FLEXExplorerModeSelect) {
self.currentMode = FLEXExplorerModeDefault;
} else {
@@ -849,8 +843,7 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
}
}
- (void)toggleMoveTool
{
- (void)toggleMoveTool {
if (self.currentMode == FLEXExplorerModeMove) {
self.currentMode = FLEXExplorerModeDefault;
} else {
@@ -858,19 +851,21 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
}
}
- (void)toggleViewsTool
{
- (void)toggleViewsTool {
[self toggleViewsToolWithCompletion:nil];
}
- (void)toggleViewsToolWithCompletion:(void(^)(void))completion
{
[self toggleToolWithViewControllerProvider:^UIViewController *{
NSArray<UIView *> *allViews = [self allViewsInHierarchy];
NSDictionary *depthsForViews = [self hierarchyDepthsForViews:allViews];
FLEXHierarchyTableViewController *hierarchyTVC = [[FLEXHierarchyTableViewController alloc] initWithViews:allViews viewsAtTap:self.viewsAtTapPoint selectedView:self.selectedView depths:depthsForViews];
hierarchyTVC.delegate = self;
return [[UINavigationController alloc] initWithRootViewController:hierarchyTVC];
- (void)toggleViewsToolWithCompletion:(void(^)(void))completion {
[self toggleToolWithViewControllerProvider:^UINavigationController *{
if (self.selectedView) {
return [FLEXHierarchyViewController
delegate:self
viewsAtTap:self.viewsAtTapPoint
selectedView:self.selectedView
];
} else {
return [FLEXHierarchyViewController delegate:self];
}
} completion:^{
if (completion) {
completion();
@@ -878,18 +873,13 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
}];
}
- (void)toggleMenuTool
{
[self toggleToolWithViewControllerProvider:^UIViewController *{
FLEXGlobalsTableViewController *globalsViewController = [FLEXGlobalsTableViewController new];
globalsViewController.delegate = self;
[FLEXGlobalsTableViewController setApplicationWindow:[UIApplication.sharedApplication keyWindow]];
return [[UINavigationController alloc] initWithRootViewController:globalsViewController];
- (void)toggleMenuTool {
[self toggleToolWithViewControllerProvider:^UINavigationController *{
return [FLEXNavigationController withRootViewController:[FLEXGlobalsViewController new]];
} completion:nil];
}
- (void)handleDownArrowKeyPressed
{
- (BOOL)handleDownArrowKeyPressed {
if (self.currentMode == FLEXExplorerModeMove) {
CGRect frame = self.selectedView.frame;
frame.origin.y += 1.0 / UIScreen.mainScreen.scale;
@@ -899,11 +889,14 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
if (selectedViewIndex > 0) {
self.selectedView = [self.viewsAtTapPoint objectAtIndex:selectedViewIndex - 1];
}
} else {
return NO;
}
return YES;
}
- (void)handleUpArrowKeyPressed
{
- (BOOL)handleUpArrowKeyPressed {
if (self.currentMode == FLEXExplorerModeMove) {
CGRect frame = self.selectedView.frame;
frame.origin.y -= 1.0 / UIScreen.mainScreen.scale;
@@ -913,25 +906,33 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
if (selectedViewIndex < self.viewsAtTapPoint.count - 1) {
self.selectedView = [self.viewsAtTapPoint objectAtIndex:selectedViewIndex + 1];
}
} else {
return NO;
}
return YES;
}
- (void)handleRightArrowKeyPressed
{
- (BOOL)handleRightArrowKeyPressed {
if (self.currentMode == FLEXExplorerModeMove) {
CGRect frame = self.selectedView.frame;
frame.origin.x += 1.0 / UIScreen.mainScreen.scale;
self.selectedView.frame = frame;
return YES;
}
return NO;
}
- (void)handleLeftArrowKeyPressed
{
- (BOOL)handleLeftArrowKeyPressed {
if (self.currentMode == FLEXExplorerModeMove) {
CGRect frame = self.selectedView.frame;
frame.origin.x -= 1.0 / UIScreen.mainScreen.scale;
self.selectedView.frame = frame;
return YES;
}
return NO;
}
@end
+13 -8
View File
@@ -8,17 +8,22 @@
#import <UIKit/UIKit.h>
@protocol FLEXWindowEventDelegate;
@interface FLEXWindow : UIWindow
@property (nonatomic, weak) id <FLEXWindowEventDelegate> eventDelegate;
@end
@protocol FLEXWindowEventDelegate <NSObject>
- (BOOL)shouldHandleTouchAtPoint:(CGPoint)pointInWindow;
- (BOOL)canBecomeKeyWindow;
@end
#pragma mark -
@interface FLEXWindow : UIWindow
@property (nonatomic, weak) id <FLEXWindowEventDelegate> eventDelegate;
/// Tracked so we can restore the key window after dismissing a modal.
/// We need to become key after modal presentation so we can correctly capture input.
/// If we're just showing the toolbar, we want the main app's window to remain key
/// so that we don't interfere with input, status bar, etc.
@property (nonatomic, readonly) UIWindow *previousKeyWindow;
@end
+16 -11
View File
@@ -7,15 +7,14 @@
//
#import "FLEXWindow.h"
#import "FLEXUtility.h"
#import <objc/runtime.h>
@implementation FLEXWindow
- (id)initWithFrame:(CGRect)frame
{
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = UIColor.clearColor;
// Some apps have windows at UIWindowLevelStatusBar + n.
// If we make the window level too high, we block out UIAlertViews.
// There's a balance between staying above the app's windows and staying below alerts.
@@ -25,8 +24,7 @@
return self;
}
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event {
BOOL pointInside = NO;
if ([self.eventDelegate shouldHandleTouchAtPoint:point]) {
pointInside = [super pointInside:point withEvent:event];
@@ -34,18 +32,25 @@
return pointInside;
}
- (BOOL)shouldAffectStatusBarAppearance
{
- (BOOL)shouldAffectStatusBarAppearance {
return [self isKeyWindow];
}
- (BOOL)canBecomeKeyWindow
{
- (BOOL)canBecomeKeyWindow {
return [self.eventDelegate canBecomeKeyWindow];
}
+ (void)initialize
{
- (void)makeKeyWindow {
_previousKeyWindow = FLEXUtility.appKeyWindow;
[super makeKeyWindow];
}
- (void)resignKeyWindow {
[super resignKeyWindow];
_previousKeyWindow = nil;
}
+ (void)initialize {
// This adds a method (superclass override) at runtime which gives us the status bar behavior we want.
// The FLEX window is intended to be an overlay that generally doesn't affect the app underneath.
// Most of the time, we want the app's main window(s) to be in control of status bar behavior.
@@ -0,0 +1,17 @@
//
// FLEXWindowManagerController.h
// FLEX
//
// Created by Tanner on 2/6/20.
// Copyright © 2020 Flipboard. All rights reserved.
//
#import "FLEXTableViewController.h"
NS_ASSUME_NONNULL_BEGIN
@interface FLEXWindowManagerController : FLEXTableViewController
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,303 @@
//
// FLEXWindowManagerController.m
// FLEX
//
// Created by Tanner on 2/6/20.
// Copyright © 2020 Flipboard. All rights reserved.
//
#import "FLEXWindowManagerController.h"
#import "FLEXManager+Private.h"
#import "FLEXUtility.h"
@interface FLEXWindowManagerController ()
@property (nonatomic) UIWindow *keyWindow;
@property (nonatomic, copy) NSString *keyWindowSubtitle;
@property (nonatomic, copy) NSArray<UIWindow *> *windows;
@property (nonatomic, copy) NSArray<NSString *> *windowSubtitles;
@property (nonatomic, copy) NSArray<UIScene *> *scenes API_AVAILABLE(ios(13));
@property (nonatomic, copy) NSArray<NSString *> *sceneSubtitles;
@end
@implementation FLEXWindowManagerController
#pragma mark - Initialization
- (id)init {
return [self initWithStyle:UITableViewStylePlain];
}
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"Windows";
if (@available(iOS 13, *)) {
self.title = @"Windows and Scenes";
}
[self disableToolbar];
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(dismissAnimated)
];
[self reloadData];
}
#pragma mark - Private
- (void)reloadData {
self.keyWindow = UIApplication.sharedApplication.keyWindow;
self.windows = UIApplication.sharedApplication.windows;
self.windowSubtitles = [self.windows flex_mapped:^id(UIWindow *window, NSUInteger idx) {
return [NSString stringWithFormat:@"Level: %@ — Root: %@",
@(window.windowLevel), window.rootViewController
];
}];
self.keyWindowSubtitle = self.windowSubtitles[[self.windows indexOfObject:self.keyWindow]];
if (@available(iOS 13, *)) {
self.scenes = UIApplication.sharedApplication.connectedScenes.allObjects;
self.sceneSubtitles = [self.scenes flex_mapped:^id(UIScene *scene, NSUInteger idx) {
return [self sceneDescription:scene];
}];
}
}
- (void)dismissAnimated {
[self dismissViewControllerAnimated:YES completion:nil];
}
- (void)showRevertOrDismissAlert:(void(^)())revertBlock {
[self.tableView deselectRowAtIndexPath:self.tableView.indexPathForSelectedRow animated:YES];
[self reloadData];
[self.tableView reloadData];
UIWindow *highestWindow = UIApplication.sharedApplication.keyWindow;
UIWindowLevel maxLevel = 0;
for (UIWindow *window in UIApplication.sharedApplication.windows) {
if (window.windowLevel > maxLevel) {
maxLevel = window.windowLevel;
highestWindow = window;
}
}
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(@"Keep Changes?");
make.message(@"If you do not wish to keep these settings, choose 'Revert Changes' below.");
make.button(@"Keep Changes").destructiveStyle();
make.button(@"Keep Changes and Dismiss").destructiveStyle().handler(^(NSArray<NSString *> *strings) {
[self dismissAnimated];
});
make.button(@"Revert Changes").cancelStyle().handler(^(NSArray<NSString *> *strings) {
revertBlock();
[self reloadData];
[self.tableView reloadData];
});
} showFrom:[FLEXUtility topViewControllerInWindow:highestWindow]];
}
- (NSString *)sceneDescription:(UIScene *)scene API_AVAILABLE(ios(13)) {
NSString *state = [self stringFromSceneState:scene.activationState];
NSString *title = scene.title.length ? scene.title : nil;
NSString *suffix = nil;
if ([scene isKindOfClass:[UIWindowScene class]]) {
UIWindowScene *windowScene = (id)scene;
suffix = FLEXPluralString(windowScene.windows.count, @"windows", @"window");
}
NSMutableString *description = state.mutableCopy;
if (title) {
[description appendFormat:@" — %@", title];
}
if (suffix) {
[description appendFormat:@" — %@", suffix];
}
return description.copy;
}
- (NSString *)stringFromSceneState:(UISceneActivationState)state API_AVAILABLE(ios(13)) {
switch (state) {
case UISceneActivationStateUnattached:
return @"Unattached";
case UISceneActivationStateForegroundActive:
return @"Active";
case UISceneActivationStateForegroundInactive:
return @"Inactive";
case UISceneActivationStateBackground:
return @"Backgrounded";
}
return [NSString stringWithFormat:@"Unknown state: %@", @(state)];
}
#pragma mark - Table View Data Source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
if (@available(iOS 13, *)) {
return 3;
}
return 2;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
switch (section) {
case 0:
return 1;
case 1:
return self.windows.count;
case 2:
if (@available(iOS 13, *)) {
return self.scenes.count;
}
}
@throw NSInternalInconsistencyException;
return 0;
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
switch (section) {
case 0: return @"Key Window";
case 1: return @"Windows";
case 2: return @"Connected Scenes";
}
return nil;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kFLEXDetailCell forIndexPath:indexPath];
UIWindow *window = nil;
NSString *subtitle = nil;
switch (indexPath.section) {
case 0:
window = self.keyWindow;
subtitle = self.keyWindowSubtitle;
break;
case 1:
window = self.windows[indexPath.row];
subtitle = self.windowSubtitles[indexPath.row];
break;
case 2:
if (@available(iOS 13, *)) {
UIScene *scene = self.scenes[indexPath.row];
cell.textLabel.text = scene.description;
cell.detailTextLabel.text = self.sceneSubtitles[indexPath.row];
return cell;
}
}
cell.textLabel.text = window.description;
cell.detailTextLabel.text = [NSString
stringWithFormat:@"Level: %@ — Root: %@", @(window.windowLevel), window.rootViewController
];
return cell;
}
#pragma mark - Table View Delegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
UIWindow *window = nil;
NSString *subtitle = nil;
FLEXWindow *flex = FLEXManager.sharedManager.explorerWindow;
id cancelHandler = ^{
[self.tableView deselectRowAtIndexPath:self.tableView.indexPathForSelectedRow animated:YES];
};
switch (indexPath.section) {
case 0:
window = self.keyWindow;
subtitle = self.keyWindowSubtitle;
break;
case 1:
window = self.windows[indexPath.row];
subtitle = self.windowSubtitles[indexPath.row];
break;
case 2:
if (@available(iOS 13, *)) {
UIScene *scene = self.scenes[indexPath.row];
UIWindowScene *oldScene = flex.windowScene;
BOOL isWindowScene = [scene isKindOfClass:[UIWindowScene class]];
BOOL isFLEXScene = isWindowScene ? flex.windowScene == scene : NO;
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(NSStringFromClass(scene.class));
if (isWindowScene) {
if (isFLEXScene) {
make.message(@"Already the FLEX window scene");
}
make.button(@"Set as FLEX Window Scene")
.handler(^(NSArray<NSString *> *strings) {
flex.windowScene = (id)scene;
[self showRevertOrDismissAlert:^{
flex.windowScene = oldScene;
}];
}).enabled(!isFLEXScene);
make.button(@"Cancel").cancelStyle();
} else {
make.message(@"Not a UIWindowScene");
make.button(@"Dismiss").cancelStyle().handler(cancelHandler);
}
} showFrom:self];
}
}
__block UIWindow *targetWindow = nil, *oldKeyWindow = nil;
__block UIWindowLevel oldLevel;
__block BOOL wasVisible;
subtitle = [subtitle stringByAppendingString:
@"\n\n1) Adjust the FLEX window level relative to this window,\n"
"2) adjust this window's level relative to the FLEX window,\n"
"3) set this window's level to a specific value, or\n"
"4) make this window the key window if it isn't already."
];
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(NSStringFromClass(window.class)).message(subtitle);
make.button(@"Adjust FLEX Window Level").handler(^(NSArray<NSString *> *strings) {
targetWindow = flex; oldLevel = flex.windowLevel;
flex.windowLevel = window.windowLevel + strings.firstObject.integerValue;
[self showRevertOrDismissAlert:^{ targetWindow.windowLevel = oldLevel; }];
});
make.button(@"Adjust This Window's Level").handler(^(NSArray<NSString *> *strings) {
targetWindow = window; oldLevel = window.windowLevel;
window.windowLevel = flex.windowLevel + strings.firstObject.integerValue;
[self showRevertOrDismissAlert:^{ targetWindow.windowLevel = oldLevel; }];
});
make.button(@"Set This Window's Level").handler(^(NSArray<NSString *> *strings) {
targetWindow = window; oldLevel = window.windowLevel;
window.windowLevel = strings.firstObject.integerValue;
[self showRevertOrDismissAlert:^{ targetWindow.windowLevel = oldLevel; }];
});
make.button(@"Make Key And Visible").handler(^(NSArray<NSString *> *strings) {
oldKeyWindow = UIApplication.sharedApplication.keyWindow;
wasVisible = window.hidden;
[window makeKeyAndVisible];
[self showRevertOrDismissAlert:^{
window.hidden = wasVisible;
[oldKeyWindow makeKeyWindow];
}];
}).enabled(!window.isKeyWindow && !window.hidden);
make.button(@"Cancel").cancelStyle().handler(cancelHandler);
make.textField(@"+/- window level, i.e. 5 or -10");
} showFrom:self];
}
@end
@@ -0,0 +1,45 @@
//
// FLEXTabList.h
// FLEX
//
// Created by Tanner on 2/1/20.
// Copyright © 2020 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface FLEXTabList : NSObject
@property (nonatomic, readonly, class) FLEXTabList *sharedList;
@property (nonatomic, readonly, nullable) UINavigationController *activeTab;
@property (nonatomic, readonly) NSArray<UINavigationController *> *openTabs;
/// Snapshots of each tab when they were last active.
@property (nonatomic, readonly) NSArray<UIImage *> *openTabSnapshots;
/// \c NSNotFound if no tabs are present.
/// Setting this property changes the active tab to one of the already open tabs.
@property (nonatomic) NSInteger activeTabIndex;
/// Adds a new tab and sets the new tab as the active tab.
- (void)addTab:(UINavigationController *)newTab;
/// Closes the given tab. If this tab was the active tab,
/// the most recent tab before that becomes the active tab.
- (void)closeTab:(UINavigationController *)tab;
/// Closes a tab at the given index. If this tab was the active tab,
/// the most recent tab before that becomes the active tab.
- (void)closeTabAtIndex:(NSInteger)idx;
/// Closes all of the tabs at the given indexes. If the active tab
/// is included, the most recent still-open tab becomes the active tab.
- (void)closeTabsAtIndexes:(NSIndexSet *)indexes;
/// A shortcut to close the active tab.
- (void)closeActiveTab;
/// A shortcut to close \e every tab.
- (void)closeAllTabs;
- (void)updateSnapshotForActiveTab;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,133 @@
//
// FLEXTabList.m
// FLEX
//
// Created by Tanner on 2/1/20.
// Copyright © 2020 Flipboard. All rights reserved.
//
#import "FLEXTabList.h"
#import "FLEXUtility.h"
@interface FLEXTabList () {
NSMutableArray *_openTabs;
NSMutableArray *_openTabSnapshots;
}
@end
#pragma mark -
@implementation FLEXTabList
#pragma mark Initialization
+ (FLEXTabList *)sharedList {
static FLEXTabList *sharedList = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedList = [self new];
});
return sharedList;
}
- (id)init {
self = [super init];
if (self) {
_openTabs = [NSMutableArray new];
_openTabSnapshots = [NSMutableArray new];
_activeTabIndex = NSNotFound;
}
return self;
}
#pragma mark Private
- (void)chooseNewActiveTab {
if (self.openTabs.count) {
self.activeTabIndex = self.openTabs.count - 1;
} else {
self.activeTabIndex = NSNotFound;
}
}
#pragma mark Public
- (void)setActiveTabIndex:(NSInteger)idx {
NSParameterAssert(idx < self.openTabs.count || idx == NSNotFound);
if (_activeTabIndex == idx) return;
_activeTabIndex = idx;
_activeTab = (idx == NSNotFound) ? nil : self.openTabs[idx];
}
- (void)addTab:(UINavigationController *)newTab {
NSParameterAssert(newTab);
// Update snapshot of the last active tab
if (self.activeTab) {
[self updateSnapshotForActiveTab];
}
// Add new tab and snapshot,
// update active tab and index
[_openTabs addObject:newTab];
[_openTabSnapshots addObject:[FLEXUtility previewImageForView:newTab.view]];
_activeTab = newTab;
_activeTabIndex = self.openTabs.count - 1;
}
- (void)closeTab:(UINavigationController *)tab {
NSParameterAssert(tab);
NSParameterAssert([self.openTabs containsObject:tab]);
NSInteger idx = [self.openTabs indexOfObject:tab];
[self closeTabAtIndex:idx];
}
- (void)closeTabAtIndex:(NSInteger)idx {
NSParameterAssert(idx < self.openTabs.count);
// Remove old tab and snapshot
[_openTabs removeObjectAtIndex:idx];
[_openTabSnapshots removeObjectAtIndex:idx];
// Update active tab and index if needed
if (self.activeTabIndex == idx) {
[self chooseNewActiveTab];
}
}
- (void)closeTabsAtIndexes:(NSIndexSet *)indexes {
// Remove old tabs and snapshot
[_openTabs removeObjectsAtIndexes:indexes];
[_openTabSnapshots removeObjectsAtIndexes:indexes];
// Update active tab and index if needed
if ([indexes containsIndex:self.activeTabIndex]) {
[self chooseNewActiveTab];
}
}
- (void)closeActiveTab {
[self closeTab:self.activeTab];
}
- (void)closeAllTabs {
// Remove tabs and snapshots
[_openTabs removeAllObjects];
[_openTabSnapshots removeAllObjects];
// Update active tab index
self.activeTabIndex = NSNotFound;
}
- (void)updateSnapshotForActiveTab {
if (self.activeTabIndex != NSNotFound) {
UIImage *newSnapshot = [FLEXUtility previewImageForView:self.activeTab.view];
[_openTabSnapshots replaceObjectAtIndex:self.activeTabIndex withObject:newSnapshot];
}
}
@end
@@ -0,0 +1,13 @@
//
// FLEXTabsViewController.h
// FLEX
//
// Created by Tanner on 2/4/20.
// Copyright © 2020 Flipboard. All rights reserved.
//
#import "FLEXTableViewController.h"
@interface FLEXTabsViewController : FLEXTableViewController
@end
@@ -0,0 +1,315 @@
//
// FLEXTabsViewController.m
// FLEX
//
// Created by Tanner on 2/4/20.
// Copyright © 2020 Flipboard. All rights reserved.
//
#import "FLEXTabsViewController.h"
#import "FLEXNavigationController.h"
#import "FLEXTabList.h"
#import "FLEXBookmarkManager.h"
#import "FLEXTableView.h"
#import "FLEXUtility.h"
#import "FLEXColor.h"
#import "UIBarButtonItem+FLEX.h"
#import "FLEXExplorerViewController.h"
#import "FLEXGlobalsViewController.h"
#import "FLEXBookmarksViewController.h"
@interface FLEXTabsViewController ()
@property (nonatomic, copy) NSArray<UINavigationController *> *openTabs;
@property (nonatomic, copy) NSArray<UIImage *> *tabSnapshots;
@property (nonatomic) NSInteger activeIndex;
@property (nonatomic) BOOL presentNewActiveTabOnDismiss;
@property (nonatomic, readonly) FLEXExplorerViewController *corePresenter;
@end
@implementation FLEXTabsViewController
#pragma mark - Initialization
- (id)init {
return [self initWithStyle:UITableViewStylePlain];
}
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"Open Tabs";
self.navigationController.hidesBarsOnSwipe = NO;
self.tableView.allowsMultipleSelectionDuringEditing = YES;
[FLEXTabList.sharedList updateSnapshotForActiveTab];
[self reloadData:NO];
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self setupDefaultBarItems];
}
#pragma mark - Private
/// @param trackActiveTabDelta whether to check if the active
/// tab changed and needs to be presented upon "Done" dismissal.
/// @return whether the active tab changed or not (if there are any tabs left)
- (BOOL)reloadData:(BOOL)trackActiveTabDelta {
BOOL activeTabDidChange = NO;
FLEXTabList *list = FLEXTabList.sharedList;
// Flag to enable check to determine whether
if (trackActiveTabDelta) {
NSInteger oldActiveIndex = self.activeIndex;
if (oldActiveIndex != list.activeTabIndex && list.activeTabIndex != NSNotFound) {
self.presentNewActiveTabOnDismiss = YES;
activeTabDidChange = YES;
}
}
// We assume the tabs aren't going to change out from under us, since
// presenting any other tool via keyboard shortcuts should dismiss us first
self.openTabs = list.openTabs;
self.tabSnapshots = list.openTabSnapshots;
self.activeIndex = list.activeTabIndex;
return activeTabDidChange;
}
- (void)reloadActiveTabRowIfChanged:(BOOL)activeTabChanged {
// Refresh the newly active tab row if needed
if (activeTabChanged) {
NSIndexPath *active = [NSIndexPath
indexPathForRow:self.activeIndex inSection:0
];
[self.tableView reloadRowsAtIndexPaths:@[active] withRowAnimation:UITableViewRowAnimationNone];
}
}
- (void)setupDefaultBarItems {
self.navigationItem.rightBarButtonItem = FLEXBarButtonItemSystem(Done, self, @selector(dismissAnimated));
self.toolbarItems = @[
UIBarButtonItem.flex_fixedSpace,
UIBarButtonItem.flex_flexibleSpace,
FLEXBarButtonItemSystem(Add, self, @selector(addTabButtonPressed)),
UIBarButtonItem.flex_flexibleSpace,
FLEXBarButtonItemSystem(Edit, self, @selector(toggleEditing)),
];
// Disable editing if no tabs available
self.toolbarItems.lastObject.enabled = self.openTabs.count > 0;
}
- (void)setupEditingBarItems {
self.navigationItem.rightBarButtonItem = nil;
self.toolbarItems = @[
[UIBarButtonItem itemWithTitle:@"Close All" target:self action:@selector(closeAllButtonPressed)],
UIBarButtonItem.flex_flexibleSpace,
[UIBarButtonItem disabledSystemItem:UIBarButtonSystemItemAdd],
UIBarButtonItem.flex_flexibleSpace,
// We use a non-system done item because we change its title dynamically
[UIBarButtonItem doneStyleitemWithTitle:@"Done" target:self action:@selector(toggleEditing)]
];
self.toolbarItems.firstObject.tintColor = FLEXColor.destructiveColor;
}
- (FLEXExplorerViewController *)corePresenter {
// We must be presented by a FLEXExplorerViewController, or presented
// by another view controller that was presented by FLEXExplorerViewController
FLEXExplorerViewController *presenter = (id)self.presentingViewController;
presenter = (id)presenter.presentingViewController ?: presenter;
NSAssert(
[presenter isKindOfClass:[FLEXExplorerViewController class]],
@"The tabs view controller expects to be presented by the explorer controller"
);
return presenter;
}
#pragma mark Button Actions
- (void)dismissAnimated {
if (self.presentNewActiveTabOnDismiss) {
// The active tab was closed so we need to present the new one
UIViewController *activeTab = FLEXTabList.sharedList.activeTab;
FLEXExplorerViewController *presenter = self.corePresenter;
[presenter dismissViewControllerAnimated:YES completion:^{
[presenter presentViewController:activeTab animated:YES completion:nil];
}];
} else if (self.activeIndex == NSNotFound) {
// The only tab was closed, so dismiss everything
[self.corePresenter dismissViewControllerAnimated:YES completion:nil];
} else {
// Simple dismiss with the same active tab, only dismiss myself
[self dismissViewControllerAnimated:YES completion:nil];
}
}
- (void)toggleEditing {
NSArray<NSIndexPath *> *selected = self.tableView.indexPathsForSelectedRows;
self.editing = !self.editing;
if (self.isEditing) {
[self setupEditingBarItems];
} else {
[self setupDefaultBarItems];
// Get index set of tabs to close
NSMutableIndexSet *indexes = [NSMutableIndexSet new];
for (NSIndexPath *ip in selected) {
[indexes addIndex:ip.row];
}
if (selected.count) {
// Close tabs and update data source
[FLEXTabList.sharedList closeTabsAtIndexes:indexes];
BOOL activeTabChanged = [self reloadData:YES];
// Remove deleted rows
[self.tableView deleteRowsAtIndexPaths:selected withRowAnimation:UITableViewRowAnimationAutomatic];
// Refresh the newly active tab row if needed
[self reloadActiveTabRowIfChanged:activeTabChanged];
}
}
}
- (void)addTabButtonPressed {
if (FLEXBookmarkManager.bookmarks.count) {
[FLEXAlert makeSheet:^(FLEXAlert *make) {
make.title(@"New Tab");
make.button(@"Main Menu").handler(^(NSArray<NSString *> *strings) {
[self addTabAndDismiss:[FLEXNavigationController
withRootViewController:[FLEXGlobalsViewController new]
]];
});
make.button(@"Choose from Bookmarks").handler(^(NSArray<NSString *> *strings) {
[self presentViewController:[FLEXNavigationController
withRootViewController:[FLEXBookmarksViewController new]
] animated:YES completion:nil];
});
make.button(@"Cancel").cancelStyle();
} showFrom:self];
} else {
// No bookmarks, just open the main menu
[self addTabAndDismiss:[FLEXNavigationController
withRootViewController:[FLEXGlobalsViewController new]
]];
}
}
- (void)addTabAndDismiss:(UINavigationController *)newTab {
FLEXExplorerViewController *presenter = self.corePresenter;
[presenter dismissViewControllerAnimated:YES completion:^{
[presenter presentViewController:newTab animated:YES completion:nil];
}];
}
- (void)closeAllButtonPressed {
[FLEXAlert makeSheet:^(FLEXAlert *make) {
NSInteger count = self.openTabs.count;
NSString *title = FLEXPluralFormatString(count, @"Close %@ tabs", @"Close %@ tab");
make.button(title).destructiveStyle().handler(^(NSArray<NSString *> *strings) {
[self closeAll];
[self toggleEditing];
});
make.button(@"Cancel").cancelStyle();
} showFrom:self];
}
- (void)closeAll {
NSInteger rowCount = self.openTabs.count;
// Close tabs and update data source
[FLEXTabList.sharedList closeAllTabs];
[self reloadData:YES];
// Delete rows from table view
NSArray<NSIndexPath *> *allRows = [NSArray flex_forEachUpTo:rowCount map:^id(NSUInteger row) {
return [NSIndexPath indexPathForRow:row inSection:0];
}];
[self.tableView deleteRowsAtIndexPaths:allRows withRowAnimation:UITableViewRowAnimationAutomatic];
}
#pragma mark - Table View Data Source
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.openTabs.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:kFLEXDetailCell forIndexPath:indexPath];
UINavigationController *tab = self.openTabs[indexPath.row];
cell.imageView.image = self.tabSnapshots[indexPath.row];
cell.textLabel.text = tab.topViewController.title;
cell.detailTextLabel.text = FLEXPluralString(tab.viewControllers.count, @"pages", @"page");
if (!cell.tag) {
cell.textLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleHeadline];
cell.detailTextLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleSubheadline];
cell.tag = 1;
}
if (indexPath.row == self.activeIndex) {
cell.backgroundColor = FLEXColor.secondaryBackgroundColor;
} else {
cell.backgroundColor = FLEXColor.primaryBackgroundColor;
}
return cell;
}
#pragma mark - Table View Delegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if (self.editing) {
// Case: editing with multi-select
self.toolbarItems.lastObject.title = @"Close Selected";
self.toolbarItems.lastObject.tintColor = FLEXColor.destructiveColor;
} else {
if (self.activeIndex == indexPath.row && self.corePresenter != self.presentingViewController) {
// Case: selected the already active tab
[self dismissAnimated];
} else {
// Case: selected a different tab,
// or selected a tab when presented from the FLEX toolbar
FLEXTabList.sharedList.activeTabIndex = indexPath.row;
self.presentNewActiveTabOnDismiss = YES;
[self dismissAnimated];
}
}
}
- (void)tableView:(UITableView *)tableView didDeselectRowAtIndexPath:(NSIndexPath *)indexPath {
NSParameterAssert(self.editing);
if (tableView.indexPathsForSelectedRows.count == 0) {
self.toolbarItems.lastObject.title = @"Done";
self.toolbarItems.lastObject.tintColor = self.view.tintColor;
}
}
- (BOOL)tableView:(UITableView *)tableView canEditRowAtIndexPath:(NSIndexPath *)indexPath {
return YES;
}
- (void)tableView:(UITableView *)table
commitEditingStyle:(UITableViewCellEditingStyle)edit
forRowAtIndexPath:(NSIndexPath *)indexPath {
NSParameterAssert(edit == UITableViewCellEditingStyleDelete);
// Close tab and update data source
[FLEXTabList.sharedList closeTab:self.openTabs[indexPath.row]];
BOOL activeTabChanged = [self reloadData:YES];
// Delete row from table view
[table deleteRowsAtIndexPaths:@[indexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
// Refresh the newly active tab row if needed
[self reloadActiveTabRowIfChanged:activeTabChanged];
}
@end
+2
View File
@@ -7,3 +7,5 @@
//
#import <FLEX/FLEXManager.h>
#import <FLEX/FLEXManager+Extensibility.h>
#import <FLEX/FLEXManager+Networking.h>
+5 -66
View File
@@ -6,18 +6,15 @@
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#if !FLEX_AT_LEAST_IOS13_SDK
@class UIWindowScene;
#endif
typedef UIViewController *(^FLEXCustomContentViewerFuture)(NSData *data);
@interface FLEXManager : NSObject
+ (instancetype)sharedManager;
@property (nonatomic, readonly, class) FLEXManager *sharedManager;
@property (nonatomic, readonly) BOOL isHidden;
@@ -29,71 +26,13 @@ typedef UIViewController *(^FLEXCustomContentViewerFuture)(NSData *data);
/// it chooses by default is not the one you wish to display it in.
- (void)showExplorerFromScene:(UIWindowScene *)scene API_AVAILABLE(ios(13.0));
#pragma mark - Network Debugging
/// If this property is set to YES, FLEX will swizzle NSURLConnection*Delegate and NSURLSession*Delegate methods
/// on classes that conform to the protocols. This allows you to view network activity history from the main FLEX menu.
/// Full responses are kept temporarily in a size-limited cache and may be pruned under memory pressure.
@property (nonatomic, getter=isNetworkDebuggingEnabled) BOOL networkDebuggingEnabled;
/// Defaults to 25 MB if never set. Values set here are persisted across launches of the app.
/// The response cache uses an NSCache, so it may purge prior to hitting the limit when the app is under memory pressure.
@property (nonatomic) NSUInteger networkResponseCacheByteLimit;
/// Requests whose host ends with one of the blacklisted entries in this array will be not be recorded (eg. google.com).
/// Wildcard or subdomain entries are not required (eg. google.com will match any subdomain under google.com).
/// Useful to remove requests that are typically noisy, such as analytics requests that you aren't interested in tracking.
@property (nonatomic, copy) NSArray<NSString *> *networkRequestHostBlacklist;
#pragma mark - Keyboard Shortcuts
/// Simulator keyboard shortcuts are enabled by default.
/// The shortcuts will not fire when there is an active text field, text view, or other responder accepting key input.
/// You can disable keyboard shortcuts if you have existing keyboard shortcuts that conflict with FLEX, or if you like doing things the hard way ;)
/// Keyboard shortcuts are always disabled (and support is compiled out) in non-simulator builds
@property (nonatomic) BOOL simulatorShortcutsEnabled;
/// Adds an action to run when the specified key & modifier combination is pressed
/// @param key A single character string matching a key on the keyboard
/// @param modifiers Modifier keys such as shift, command, or alt/option
/// @param action The block to run on the main thread when the key & modifier combination is recognized.
/// @param description Shown the the keyboard shortcut help menu, which is accessed via the '?' key.
/// @note The action block will be retained for the duration of the application. You may want to use weak references.
/// @note FLEX registers several default keyboard shortcuts. Use the '?' key to see a list of shortcuts.
- (void)registerSimulatorShortcutWithKey:(NSString *)key modifiers:(UIKeyModifierFlags)modifiers action:(dispatch_block_t)action description:(NSString *)description;
#pragma mark - Extensions
#pragma mark - Misc
/// Default database password is @c nil by default.
/// Set this to the password you want the databases to open with.
@property (copy, nonatomic) NSString *defaultSqliteDatabasePassword;
/// Adds an entry at the bottom of the list of Global State items. Call this method before this view controller is displayed.
/// @param entryName The string to be displayed in the cell.
/// @param objectFutureBlock When you tap on the row, information about the object returned by this block will be displayed.
/// Passing a block that returns an object allows you to display information about an object whose actual pointer may change at runtime (e.g. +currentUser)
/// @note This method must be called from the main thread.
/// The objectFutureBlock will be invoked from the main thread and may return nil.
/// @note The passed block will be copied and retain for the duration of the application, you may want to use __weak references.
- (void)registerGlobalEntryWithName:(NSString *)entryName objectFutureBlock:(id (^)(void))objectFutureBlock;
/// Adds an entry at the bottom of the list of Global State items. Call this method before this view controller is displayed.
/// @param entryName The string to be displayed in the cell.
/// @param viewControllerFutureBlock When you tap on the row, view controller returned by this block will be pushed on the navigation controller stack.
/// @note This method must be called from the main thread.
/// The viewControllerFutureBlock will be invoked from the main thread and may not return nil.
/// @note The passed block will be copied and retain for the duration of the application, you may want to use __weak references.
- (void)registerGlobalEntryWithName:(NSString *)entryName
viewControllerFutureBlock:(UIViewController * (^)(void))viewControllerFutureBlock;
/// Sets custom viewer for specific content type.
/// @param contentType Mime type like application/json
/// @param viewControllerFutureBlock Viewer (view controller) creation block
/// @note This method must be called from the main thread.
/// The viewControllerFutureBlock will be invoked from the main thread and may not return nil.
/// @note The passed block will be copied and retain for the duration of the application, you may want to use __weak references.
- (void)setCustomViewerForContentType:(NSString *)contentType
viewControllerFutureBlock:(FLEXCustomContentViewerFuture)viewControllerFutureBlock;
@end
typedef UIViewController *(^FLEXCustomContentViewerFuture)(NSData *data);
@@ -28,8 +28,7 @@ static const CGFloat kColumnMargin = 1;
@implementation FLEXMultiColumnTableView
- (instancetype)initWithFrame:(CGRect)frame
{
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
[self loadUI];
@@ -37,14 +36,12 @@ static const CGFloat kColumnMargin = 1;
return self;
}
- (void)didMoveToSuperview
{
- (void)didMoveToSuperview {
[super didMoveToSuperview];
[self reloadData];
}
- (void)layoutSubviews
{
- (void)layoutSubviews {
[super layoutSubviews];
CGFloat width = self.frame.size.width;
@@ -73,15 +70,13 @@ static const CGFloat kColumnMargin = 1;
}
- (void)loadUI
{
- (void)loadUI {
[self loadHeaderScrollView];
[self loadContentScrollView];
[self loadLeftView];
}
- (void)reloadData
{
- (void)reloadData {
[self loadLeftViewData];
[self loadContentData];
[self loadHeaderData];
@@ -89,8 +84,7 @@ static const CGFloat kColumnMargin = 1;
#pragma mark - UI
- (void)loadHeaderScrollView
{
- (void)loadHeaderScrollView {
UIScrollView *headerScrollView = [UIScrollView new];
headerScrollView.delegate = self;
self.headerScrollView = headerScrollView;
@@ -99,8 +93,7 @@ static const CGFloat kColumnMargin = 1;
[self addSubview:headerScrollView];
}
- (void)loadContentScrollView
{
- (void)loadContentScrollView {
UIScrollView *scrollView = [UIScrollView new];
scrollView.bounces = NO;
@@ -119,8 +112,7 @@ static const CGFloat kColumnMargin = 1;
}
- (void)loadLeftView
{
- (void)loadLeftView {
UITableView *leftTableView = [UITableView new];
leftTableView.delegate = self;
leftTableView.dataSource = self;
@@ -138,8 +130,7 @@ static const CGFloat kColumnMargin = 1;
#pragma mark - Data
- (void)loadHeaderData
{
- (void)loadHeaderData {
NSArray<UIView *> *subviews = self.headerScrollView.subviews;
for (UIView *subview in subviews) {
@@ -166,8 +157,7 @@ static const CGFloat kColumnMargin = 1;
}
}
- (void)contentHeaderTap:(UIGestureRecognizer *)gesture
{
- (void)contentHeaderTap:(UIGestureRecognizer *)gesture {
FLEXTableColumnHeader *header = (FLEXTableColumnHeader *)gesture.view;
NSString *string = header.label.text;
FLEXTableColumnHeaderSortType currentType = [self.sortStatusDict[string] integerValue];
@@ -191,19 +181,16 @@ static const CGFloat kColumnMargin = 1;
}
- (void)loadContentData
{
- (void)loadContentData {
[self.contentTableView reloadData];
}
- (void)loadLeftViewData
{
- (void)loadLeftViewData {
[self.leftTableView reloadData];
}
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UIColor *backgroundColor = UIColor.whiteColor;
if (indexPath.row % 2 != 0) {
backgroundColor = [UIColor colorWithWhite:0.950 alpha:0.750];
@@ -239,20 +226,17 @@ static const CGFloat kColumnMargin = 1;
}
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return [self.dataSource numberOfRowsInTableView:self];
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return [self.dataSource multiColumnTableView:self heightForContentCellInRow:indexPath.row];
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
if (scrollView == self.contentScrollView) {
self.headerScrollView.contentOffset = scrollView.contentOffset;
}
@@ -270,8 +254,7 @@ static const CGFloat kColumnMargin = 1;
#pragma mark -
#pragma mark UITableView Delegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
if (tableView == self.leftTableView) {
[self.contentTableView selectRowAtIndexPath:indexPath
animated:NO
@@ -287,59 +270,48 @@ static const CGFloat kColumnMargin = 1;
#pragma mark -
#pragma mark DataSource Accessor
- (NSInteger)numberOfRows
{
- (NSInteger)numberOfRows {
return [self.dataSource numberOfRowsInTableView:self];
}
- (NSInteger)numberOfColumns
{
- (NSInteger)numberOfColumns {
return [self.dataSource numberOfColumnsInTableView:self];
}
- (NSString *)columnTitleForColumn:(NSInteger)column
{
- (NSString *)columnTitleForColumn:(NSInteger)column {
return [self.dataSource columnNameInColumn:column];
}
- (NSString *)rowTitleForRow:(NSInteger)row
{
- (NSString *)rowTitleForRow:(NSInteger)row {
return [self.dataSource rowNameInRow:row];
}
- (NSString *)contentAtColumn:(NSInteger)column row:(NSInteger)row;
{
- (NSString *)contentAtColumn:(NSInteger)column row:(NSInteger)row; {
return [self.dataSource contentAtColumn:column row:row];
}
- (CGFloat)contentWidthForColumn:(NSInteger)column
{
- (CGFloat)contentWidthForColumn:(NSInteger)column {
return [self.dataSource multiColumnTableView:self widthForContentCellInColumn:column];
}
- (CGFloat)contentHeightForRow:(NSInteger)row
{
- (CGFloat)contentHeightForRow:(NSInteger)row {
return [self.dataSource multiColumnTableView:self heightForContentCellInRow:row];
}
- (CGFloat)topHeaderHeight
{
- (CGFloat)topHeaderHeight {
return [self.dataSource heightForTopHeaderInTableView:self];
}
- (CGFloat)leftHeaderWidth
{
- (CGFloat)leftHeaderWidth {
return [self.dataSource widthForLeftHeaderInTableView:self];
}
- (CGFloat)columnMargin
{
- (CGFloat)columnMargin {
return kColumnMargin;
}
- (void)tableContentCell:(FLEXTableContentCell *)tableView labelDidTapWithText:(NSString *)text
{
- (void)tableContentCell:(FLEXTableContentCell *)tableView labelDidTapWithText:(NSString *)text {
[self.delegate multiColumnTableView:self didTapLabelWithText:text];
}
@@ -26,8 +26,7 @@
@implementation FLEXRealmDatabaseManager
- (instancetype)initWithPath:(NSString*)aPath
{
- (instancetype)initWithPath:(NSString*)aPath {
Class realmClass = NSClassFromString(@"RLMRealm");
if (realmClass == nil) {
return nil;
@@ -41,8 +40,7 @@
return self;
}
- (BOOL)open
{
- (BOOL)open {
Class realmClass = NSClassFromString(@"RLMRealm");
Class configurationClass = NSClassFromString(@"RLMRealmConfiguration");
@@ -57,8 +55,7 @@
return (error == nil);
}
- (NSArray<NSDictionary<NSString *, id> *> *)queryAllTables
{
- (NSArray<NSDictionary<NSString *, id> *> *)queryAllTables {
NSMutableArray<NSDictionary<NSString *, id> *> *allTables = [NSMutableArray array];
RLMSchema *schema = [self.realm schema];
@@ -74,8 +71,7 @@
return allTables;
}
- (NSArray<NSString *> *)queryAllColumnsWithTableName:(NSString *)tableName
{
- (NSArray<NSString *> *)queryAllColumnsWithTableName:(NSString *)tableName {
RLMObjectSchema *objectSchema = [[self.realm schema] schemaForClassName:tableName];
if (objectSchema == nil) {
return nil;
@@ -89,8 +85,7 @@
return columnNames;
}
- (NSArray<NSDictionary<NSString *, id> *> *)queryAllDataWithTableName:(NSString *)tableName
{
- (NSArray<NSDictionary<NSString *, id> *> *)queryAllDataWithTableName:(NSString *)tableName {
RLMObjectSchema *objectSchema = [[self.realm schema] schemaForClassName:tableName];
RLMResults *results = [self.realm allObjects:tableName];
if (results.count == 0 || objectSchema == nil) {
@@ -13,14 +13,12 @@
static NSString *const QUERY_TABLENAMES_SQL = @"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name";
@implementation FLEXSQLiteDatabaseManager
{
@implementation FLEXSQLiteDatabaseManager {
sqlite3* _db;
NSString* _databasePath;
}
- (instancetype)initWithPath:(NSString*)aPath
{
- (instancetype)initWithPath:(NSString*)aPath {
self = [super init];
if (self) {
@@ -86,13 +84,11 @@ static NSString *const QUERY_TABLENAMES_SQL = @"SELECT name FROM sqlite_master W
}
- (NSArray<NSDictionary<NSString *, id> *> *)queryAllTables
{
- (NSArray<NSDictionary<NSString *, id> *> *)queryAllTables {
return [self executeQuery:QUERY_TABLENAMES_SQL];
}
- (NSArray<NSString *> *)queryAllColumnsWithTableName:(NSString *)tableName
{
- (NSArray<NSString *> *)queryAllColumnsWithTableName:(NSString *)tableName {
NSString *sql = [NSString stringWithFormat:@"PRAGMA table_info('%@')",tableName];
NSArray<NSDictionary<NSString *, id> *> *resultArray = [self executeQuery:sql];
NSMutableArray<NSString *> *array = [NSMutableArray array];
@@ -103,8 +99,7 @@ static NSString *const QUERY_TABLENAMES_SQL = @"SELECT name FROM sqlite_master W
return array;
}
- (NSArray<NSDictionary<NSString *, id> *> *)queryAllDataWithTableName:(NSString *)tableName
{
- (NSArray<NSDictionary<NSString *, id> *> *)queryAllDataWithTableName:(NSString *)tableName {
NSString *sql = [NSString stringWithFormat:@"SELECT * FROM %@",tableName];
return [self executeQuery:sql];
}
@@ -112,8 +107,7 @@ static NSString *const QUERY_TABLENAMES_SQL = @"SELECT name FROM sqlite_master W
#pragma mark -
#pragma mark - Private
- (NSArray<NSDictionary<NSString *, id> *> *)executeQuery:(NSString *)sql
{
- (NSArray<NSDictionary<NSString *, id> *> *)executeQuery:(NSString *)sql {
[self open];
NSMutableArray<NSDictionary<NSString *, id> *> *resultArray = [NSMutableArray array];
sqlite3_stmt *pstmt;
@@ -8,14 +8,12 @@
#import "FLEXTableColumnHeader.h"
@implementation FLEXTableColumnHeader
{
@implementation FLEXTableColumnHeader {
UILabel *_arrowLabel;
}
- (instancetype)initWithFrame:(CGRect)frame
{
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = UIColor.whiteColor;
@@ -38,8 +36,7 @@
return self;
}
- (void)changeSortStatusWithType:(FLEXTableColumnHeaderSortType)type
{
- (void)changeSortStatusWithType:(FLEXTableColumnHeaderSortType)type {
switch (type) {
case FLEXTableColumnHeaderSortTypeNone:
_arrowLabel.text = @"";
@@ -15,8 +15,7 @@
@implementation FLEXTableContentCell
+ (instancetype)cellWithTableView:(UITableView *)tableView columnNumber:(NSInteger)number;
{
+ (instancetype)cellWithTableView:(UITableView *)tableView columnNumber:(NSInteger)number; {
static NSString *identifier = @"FLEXTableContentCell";
FLEXTableContentCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
if (!cell) {
@@ -43,8 +42,7 @@
return cell;
}
- (void)layoutSubviews
{
- (void)layoutSubviews {
[super layoutSubviews];
CGFloat labelWidth = self.contentView.frame.size.width / self.labels.count;
CGFloat labelHeight = self.contentView.frame.size.height;
@@ -55,8 +53,7 @@
}
- (void)labelDidTap:(UIGestureRecognizer *)gesture
{
- (void)labelDidTap:(UIGestureRecognizer *)gesture {
UILabel *label = (UILabel *)gesture.view;
if ([self.delegate respondsToSelector:@selector(tableContentCell:labelDidTapWithText:)]) {
[self.delegate tableContentCell:self labelDidTapWithText:label.text];
@@ -25,8 +25,7 @@
[self.view addSubview:self.multiColumnView];
}
- (void)viewWillAppear:(BOOL)animated
{
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self.multiColumnView reloadData];
}
@@ -48,29 +47,24 @@
}
#pragma mark MultiColumnTableView DataSource
- (NSInteger)numberOfColumnsInTableView:(FLEXMultiColumnTableView *)tableView
{
- (NSInteger)numberOfColumnsInTableView:(FLEXMultiColumnTableView *)tableView {
return self.columnsArray.count;
}
- (NSInteger)numberOfRowsInTableView:(FLEXMultiColumnTableView *)tableView
{
- (NSInteger)numberOfRowsInTableView:(FLEXMultiColumnTableView *)tableView {
return self.contentsArray.count;
}
- (NSString *)columnNameInColumn:(NSInteger)column
{
- (NSString *)columnNameInColumn:(NSInteger)column {
return self.columnsArray[column];
}
- (NSString *)rowNameInRow:(NSInteger)row
{
- (NSString *)rowNameInRow:(NSInteger)row {
return [NSString stringWithFormat:@"%ld",(long)row];
}
- (NSString *)contentAtColumn:(NSInteger)column row:(NSInteger)row
{
- (NSString *)contentAtColumn:(NSInteger)column row:(NSInteger)row {
if (self.contentsArray.count > row) {
NSDictionary<NSString *, id> *dic = self.contentsArray[row];
if (self.contentsArray.count > column) {
@@ -80,8 +74,7 @@
return @"";
}
- (NSArray *)contentAtRow:(NSInteger)row
{
- (NSArray *)contentAtRow:(NSInteger)row {
NSMutableArray *result = [NSMutableArray array];
if (self.contentsArray.count > row) {
NSDictionary<NSString *, id> *dic = self.contentsArray[row];
@@ -94,24 +87,20 @@
}
- (CGFloat)multiColumnTableView:(FLEXMultiColumnTableView *)tableView
heightForContentCellInRow:(NSInteger)row
{
heightForContentCellInRow:(NSInteger)row {
return 40;
}
- (CGFloat)multiColumnTableView:(FLEXMultiColumnTableView *)tableView
widthForContentCellInColumn:(NSInteger)column
{
widthForContentCellInColumn:(NSInteger)column {
return 120;
}
- (CGFloat)heightForTopHeaderInTableView:(FLEXMultiColumnTableView *)tableView
{
- (CGFloat)heightForTopHeaderInTableView:(FLEXMultiColumnTableView *)tableView {
return 40;
}
- (CGFloat)widthForLeftHeaderInTableView:(FLEXMultiColumnTableView *)tableView
{
- (CGFloat)widthForLeftHeaderInTableView:(FLEXMultiColumnTableView *)tableView {
NSString *str = [NSString stringWithFormat:@"%lu",(unsigned long)self.contentsArray.count];
NSDictionary<NSString *, id> *attrs = @{@"NSFontAttributeName":[UIFont systemFontOfSize:17.0]};
CGSize size = [str boundingRectWithSize:CGSizeMake(CGFLOAT_MAX, 14)
@@ -124,14 +113,12 @@
#pragma mark MultiColumnTableView Delegate
- (void)multiColumnTableView:(FLEXMultiColumnTableView *)tableView didTapLabelWithText:(NSString *)text
{
- (void)multiColumnTableView:(FLEXMultiColumnTableView *)tableView didTapLabelWithText:(NSString *)text {
FLEXWebViewController * detailViewController = [[FLEXWebViewController alloc] initWithText:text];
[self.navigationController pushViewController:detailViewController animated:YES];
}
- (void)multiColumnTableView:(FLEXMultiColumnTableView *)tableView didTapHeaderWithText:(NSString *)text sortType:(FLEXTableColumnHeaderSortType)sortType
{
- (void)multiColumnTableView:(FLEXMultiColumnTableView *)tableView didTapHeaderWithText:(NSString *)text sortType:(FLEXTableColumnHeaderSortType)sortType {
NSArray<NSDictionary<NSString *, id> *> *sortContentData = [self.contentsArray sortedArrayUsingComparator:^NSComparisonResult(NSDictionary<NSString *, id> * obj1, NSDictionary<NSString *, id> * obj2) {
@@ -163,8 +150,7 @@
#pragma mark About Transition
- (void)willTransitionToTraitCollection:(UITraitCollection *)newCollection
withTransitionCoordinator:(id <UIViewControllerTransitionCoordinator>)coordinator
{
withTransitionCoordinator:(id <UIViewControllerTransitionCoordinator>)coordinator {
[super willTransitionToTraitCollection:newCollection
withTransitionCoordinator:coordinator];
[coordinator animateAlongsideTransition:^(id <UIViewControllerTransitionCoordinatorContext> context) {
@@ -10,8 +10,7 @@
@implementation FLEXTableLeftCell
+ (instancetype)cellWithTableView:(UITableView *)tableView
{
+ (instancetype)cellWithTableView:(UITableView *)tableView {
static NSString *identifier = @"FLEXTableLeftCell";
FLEXTableLeftCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
@@ -27,8 +26,7 @@
return cell;
}
- (void)layoutSubviews
{
- (void)layoutSubviews {
[super layoutSubviews];
self.titlelabel.frame = self.contentView.frame;
}
@@ -14,8 +14,7 @@
#import "FLEXTableContentViewController.h"
@interface FLEXTableListViewController ()
{
@interface FLEXTableListViewController () {
id<FLEXDatabaseManager> _dbm;
NSString *_databasePath;
}
@@ -30,8 +29,7 @@
@implementation FLEXTableListViewController
- (instancetype)initWithPath:(NSString *)path
{
- (instancetype)initWithPath:(NSString *)path {
self = [super initWithStyle:UITableViewStyleGrouped];
if (self) {
_databasePath = [path copy];
@@ -42,8 +40,7 @@
return self;
}
- (id<FLEXDatabaseManager>)databaseManagerForFileAtPath:(NSString *)path
{
- (id<FLEXDatabaseManager>)databaseManagerForFileAtPath:(NSString *)path {
NSString *pathExtension = path.pathExtension.lowercaseString;
NSArray<NSString *> *sqliteExtensions = [FLEXTableListViewController supportedSQLiteExtensions];
@@ -59,8 +56,7 @@
return nil;
}
- (void)getAllTables
{
- (void)getAllTables {
NSArray<NSDictionary<NSString *, id> *> *resultArray = [_dbm queryAllTables];
NSMutableArray<NSString *> *array = [NSMutableArray array];
for (NSDictionary<NSString *, id> *dict in resultArray) {
@@ -71,8 +67,7 @@
self.filteredTables = array;
}
- (void)viewDidLoad
{
- (void)viewDidLoad {
[super viewDidLoad];
self.showsSearchBar = YES;
@@ -80,8 +75,7 @@
#pragma mark - Search bar
- (void)updateSearchResults:(NSString *)searchText
{
- (void)updateSearchResults:(NSString *)searchText {
if (searchText.length > 0) {
NSPredicate *searchPredicate = [NSPredicate predicateWithFormat:@"SELF CONTAINS[cd] %@", searchText];
self.filteredTables = [self.tables filteredArrayUsingPredicate:searchPredicate];
@@ -94,13 +88,11 @@
#pragma mark - Table View Data Source
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.filteredTables.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"FLEXTableListViewControllerCell"];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
@@ -111,8 +103,7 @@
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
FLEXTableContentViewController *contentViewController = [FLEXTableContentViewController new];
contentViewController.contentsArray = [_dbm queryAllDataWithTableName:self.filteredTables[indexPath.row]];
@@ -122,15 +113,13 @@
[self.navigationController pushViewController:contentViewController animated:YES];
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
return [NSString stringWithFormat:@"Tables (%lu)", (unsigned long)self.filteredTables.count];
}
#pragma mark - FLEXTableListViewController
+ (BOOL)supportsExtension:(NSString *)extension
{
+ (BOOL)supportsExtension:(NSString *)extension {
extension = extension.lowercaseString;
NSArray<NSString *> *sqliteExtensions = [FLEXTableListViewController supportedSQLiteExtensions];
@@ -146,13 +135,11 @@
return NO;
}
+ (NSArray<NSString *> *)supportedSQLiteExtensions
{
+ (NSArray<NSString *> *)supportedSQLiteExtensions {
return @[@"db", @"sqlite", @"sqlite3"];
}
+ (NSArray<NSString *> *)supportedRealmExtensions
{
+ (NSArray<NSString *> *)supportedRealmExtensions {
if (NSClassFromString(@"RLMRealm") == nil) {
return nil;
}
@@ -7,13 +7,13 @@
//
#import "FLEXAddressExplorerCoordinator.h"
#import "FLEXGlobalsTableViewController.h"
#import "FLEXGlobalsViewController.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXObjectExplorerViewController.h"
#import "FLEXRuntimeUtility.h"
#import "FLEXUtility.h"
@interface FLEXGlobalsTableViewController (FLEXAddressExploration)
@interface FLEXGlobalsViewController (FLEXAddressExploration)
- (void)deselectSelectedRow;
- (void)tryExploreAddress:(NSString *)addressString safely:(BOOL)safely;
@end
@@ -23,11 +23,11 @@
#pragma mark - FLEXGlobalsEntry
+ (NSString *)globalsEntryTitle:(FLEXGlobalsRow)row {
return @"🔎 Address Explorer";
return @"🔎 Address Explorer";
}
+ (FLEXGlobalsTableViewControllerRowAction)globalsEntryRowAction:(FLEXGlobalsRow)row {
return ^(FLEXGlobalsTableViewController *host) {
return ^(FLEXGlobalsViewController *host) {
NSString *title = @"Explore Object at Address";
NSString *message = @"Paste a hexadecimal address below, starting with '0x'. "
@@ -59,7 +59,7 @@
@end
@implementation FLEXGlobalsTableViewController (FLEXAddressExploration)
@implementation FLEXGlobalsViewController (FLEXAddressExploration)
- (void)deselectSelectedRow {
NSIndexPath *selected = self.tableView.indexPathForSelectedRow;
@@ -1,16 +0,0 @@
//
// FLEXClassesTableViewController.h
// Flipboard
//
// Created by Ryan Olson on 2014-05-03.
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import "FLEXTableViewController.h"
#import "FLEXGlobalsEntry.h"
@interface FLEXClassesTableViewController : FLEXTableViewController <FLEXGlobalsEntry>
+ (instancetype)binaryImageName:(NSString *)binaryImageName;
@end
@@ -1,152 +0,0 @@
//
// FLEXClassesTableViewController.m
// Flipboard
//
// Created by Ryan Olson on 2014-05-03.
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import "FLEXClassesTableViewController.h"
#import "FLEXObjectExplorerViewController.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXUtility.h"
#import <objc/runtime.h>
@interface FLEXClassesTableViewController ()
@property (nonatomic, copy) NSArray<NSString *> *classNames;
@property (nonatomic, copy) NSArray<NSString *> *filteredClassNames;
@property (nonatomic, copy) NSString *binaryImageName;
@end
@implementation FLEXClassesTableViewController
#pragma mark - Initialization
+ (instancetype)binaryImageName:(NSString *)binaryImageName
{
return [[self alloc] initWithBinaryImageName:binaryImageName];
}
- (id)initWithBinaryImageName:(NSString *)binaryImageName
{
NSParameterAssert(binaryImageName);
self = [super init];
if (self) {
self.binaryImageName = binaryImageName;
[self loadClassNames];
}
return self;
}
#pragma mark - Internal
- (void)viewDidLoad
{
[super viewDidLoad];
self.showsSearchBar = YES;
[self updateTitle];
}
- (void)updateTitle
{
NSString *shortImageName = self.binaryImageName.lastPathComponent;
self.title = [NSString stringWithFormat:@"%@ Classes (%lu)",
shortImageName, (unsigned long)self.filteredClassNames.count
];
}
- (void)setClassNames:(NSArray<NSString *> *)classNames
{
_classNames = self.filteredClassNames = classNames.copy;
}
- (void)loadClassNames
{
unsigned int classNamesCount = 0;
const char **classNames = objc_copyClassNamesForImage(self.binaryImageName.UTF8String, &classNamesCount);
if (classNames) {
NSMutableArray<NSString *> *classNameStrings = [NSMutableArray array];
for (unsigned int i = 0; i < classNamesCount; i++) {
const char *className = classNames[i];
NSString *classNameString = [NSString stringWithUTF8String:className];
[classNameStrings addObject:classNameString];
}
self.classNames = [classNameStrings sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)];
free(classNames);
}
}
#pragma mark - FLEXGlobalsEntry
+ (NSString *)globalsEntryTitle:(FLEXGlobalsRow)row {
return [NSString stringWithFormat:@"📕 %@ Classes", [FLEXUtility applicationName]];
}
+ (UIViewController *)globalsEntryViewController:(FLEXGlobalsRow)row {
return [self binaryImageName:[FLEXUtility applicationImageName]];
}
#pragma mark - Search bar
- (void)updateSearchResults:(NSString *)searchText
{
if (searchText.length > 0) {
NSPredicate *searchPredicate = [NSPredicate predicateWithFormat:@"SELF CONTAINS[cd] %@", searchText];
self.filteredClassNames = [self.classNames filteredArrayUsingPredicate:searchPredicate].reverseObjectEnumerator.allObjects;
} else {
self.filteredClassNames = self.classNames;
}
[self updateTitle];
[self.tableView reloadData];
}
#pragma mark - Table View Data Source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.filteredClassNames.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = @"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
cell.textLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
}
cell.textLabel.text = self.filteredClassNames[indexPath.row];
return cell;
}
#pragma mark - Table View Delegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *className = self.filteredClassNames[indexPath.row];
Class selectedClass = objc_getClass(className.UTF8String);
FLEXObjectExplorerViewController *objectExplorer = [FLEXObjectExplorerFactory explorerViewControllerForObject:selectedClass];
[self.navigationController pushViewController:objectExplorer animated:YES];
}
@end
@@ -52,8 +52,8 @@
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];
cell.textLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
cell.detailTextLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
cell.textLabel.font = UIFont.flex_defaultTableCellFont;
cell.detailTextLabel.font = UIFont.flex_defaultTableCellFont;
cell.detailTextLabel.textColor = UIColor.grayColor;
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
}
@@ -1,5 +1,5 @@
//
// FLEXInstancesTableViewController.h
// FLEXInstancesViewController.h
// Flipboard
//
// Created by Ryan Olson on 5/28/14.
@@ -8,7 +8,7 @@
#import <UIKit/UIKit.h>
@interface FLEXInstancesTableViewController : UITableViewController
@interface FLEXInstancesViewController : UITableViewController
+ (instancetype)instancesTableViewControllerForClassName:(NSString *)className;
+ (instancetype)instancesTableViewControllerForInstancesReferencingObject:(id)object;
@@ -1,22 +1,23 @@
//
// FLEXInstancesTableViewController.m
// FLEXInstancesViewController.m
// Flipboard
//
// Created by Ryan Olson on 5/28/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import "FLEXInstancesTableViewController.h"
#import "FLEXInstancesViewController.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXObjectExplorerViewController.h"
#import "FLEXRuntimeUtility.h"
#import "FLEXUtility.h"
#import "FLEXHeapEnumerator.h"
#import "FLEXObjectRef.h"
#import "NSString+FLEX.h"
#import <malloc/malloc.h>
@interface FLEXInstancesTableViewController ()
@interface FLEXInstancesViewController ()
/// Array of [[section], [section], ...]
/// where [section] is [["row title", instance], ["row title", instance], ...]
@@ -28,7 +29,7 @@
@end
@implementation FLEXInstancesTableViewController
@implementation FLEXInstancesViewController
- (id)initWithReferences:(NSArray<FLEXObjectRef *> *)references {
return [self initWithReferences:references predicates:nil sectionTitles:nil];
@@ -55,8 +56,7 @@
return self;
}
+ (instancetype)instancesTableViewControllerForClassName:(NSString *)className
{
+ (instancetype)instancesTableViewControllerForClassName:(NSString *)className {
const char *classNameCString = className.UTF8String;
NSMutableArray *instances = [NSMutableArray array];
[FLEXHeapEnumerator enumerateLiveObjectsUsingBlock:^(__unsafe_unretained id object, __unsafe_unretained Class actualClass) {
@@ -71,17 +71,25 @@
}
}];
NSArray<FLEXObjectRef *> *references = [FLEXObjectRef referencingAll:instances];
FLEXInstancesTableViewController *viewController = [[self alloc] initWithReferences:references];
FLEXInstancesViewController *viewController = [[self alloc] initWithReferences:references];
viewController.title = [NSString stringWithFormat:@"%@ (%lu)", className, (unsigned long)instances.count];
return viewController;
}
+ (instancetype)instancesTableViewControllerForInstancesReferencingObject:(id)object
{
+ (instancetype)instancesTableViewControllerForInstancesReferencingObject:(id)object {
static Class SwiftObjectClass = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SwiftObjectClass = NSClassFromString(@"SwiftObject");
if (!SwiftObjectClass) {
SwiftObjectClass = NSClassFromString(@"Swift._SwiftObject");
}
});
NSMutableArray<FLEXObjectRef *> *instances = [NSMutableArray array];
[FLEXHeapEnumerator enumerateLiveObjectsUsingBlock:^(__unsafe_unretained id tryObject, __unsafe_unretained Class actualClass) {
// Skip Swift objects
if ([actualClass isKindOfClass:NSClassFromString(@"SwiftObject")]) {
if ([actualClass isKindOfClass:SwiftObjectClass]) {
return;
}
@@ -91,33 +99,37 @@
while (tryClass) {
unsigned int ivarCount = 0;
Ivar *ivars = class_copyIvarList(tryClass, &ivarCount);
for (unsigned int ivarIndex = 0; ivarIndex < ivarCount; ivarIndex++) {
Ivar ivar = ivars[ivarIndex];
const char *typeEncoding = ivar_getTypeEncoding(ivar);
if (typeEncoding[0] == FLEXTypeEncodingObjcObject || typeEncoding[0] == FLEXTypeEncodingObjcClass) {
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) {
[instances addObject:[FLEXObjectRef referencing:tryObject ivar:@(ivar_getName(ivar))]];
NSString *ivarName = @(ivar_getName(ivar) ?: "???");
[instances addObject:[FLEXObjectRef referencing:tryObject ivar:ivarName]];
return;
}
}
}
tryClass = class_getSuperclass(tryClass);
}
}];
NSArray<NSPredicate *> *predicates = [self defaultPredicates];
NSArray<NSString *> *sectionTitles = [self defaultSectionTitles];
FLEXInstancesTableViewController *viewController = [[self alloc] initWithReferences:instances
FLEXInstancesViewController *viewController = [[self alloc] initWithReferences:instances
predicates:predicates
sectionTitles:sectionTitles];
viewController.title = [NSString stringWithFormat:@"Referencing %@ %p", NSStringFromClass(object_getClass(object)), object];
return viewController;
}
+ (NSPredicate *)defaultPredicateForSection:(NSInteger)section
{
+ (NSPredicate *)defaultPredicateForSection:(NSInteger)section {
// These are the types of references that we typically don't care about.
// We want this list of "object-ivar pairs" split into two sections.
BOOL(^isObserver)(FLEXObjectRef *, NSDictionary *) = ^BOOL(FLEXObjectRef *ref, NSDictionary *bindings) {
@@ -171,8 +183,7 @@
return @[@"", @"AutoLayout", @"Trivial"];
}
- (void)buildSections
{
- (void)buildSections {
NSInteger maxSections = self.maxSections;
NSMutableArray *sections = [NSMutableArray array];
for (NSInteger i = 0; i < maxSections; i++) {
@@ -190,24 +201,21 @@
#pragma mark - Table View Data Source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return self.maxSections;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.sections[section].count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = @"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
UIFont *cellFont = [FLEXUtility defaultTableViewCellLabelFont];
UIFont *cellFont = UIFont.flex_defaultTableCellFont;
cell.textLabel.font = cellFont;
cell.detailTextLabel.font = cellFont;
cell.detailTextLabel.textColor = UIColor.grayColor;
@@ -215,13 +223,12 @@
FLEXObjectRef *row = self.sections[indexPath.section][indexPath.row];
cell.textLabel.text = row.reference;
cell.detailTextLabel.text = [FLEXRuntimeUtility descriptionForIvarOrPropertyValue:row.object];
cell.detailTextLabel.text = [FLEXRuntimeUtility summaryForObject:row.object];
return cell;
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
if (self.sectionTitles.count) {
// Return nil instead of empty strings
NSString *title = self.sectionTitles[section];
@@ -236,8 +243,7 @@
#pragma mark - Table View Delegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
id instance = self.instances[indexPath.row].object;
FLEXObjectExplorerViewController *drillInViewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:instance];
[self.navigationController pushViewController:drillInViewController animated:YES];
@@ -1,14 +0,0 @@
//
// FLEXLibrariesTableViewController.h
// Flipboard
//
// Created by Ryan Olson on 2014-05-02.
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import "FLEXTableViewController.h"
#import "FLEXGlobalsEntry.h"
@interface FLEXLibrariesTableViewController : FLEXTableViewController <FLEXGlobalsEntry>
@end
@@ -1,199 +0,0 @@
//
// FLEXLibrariesTableViewController.m
// Flipboard
//
// Created by Ryan Olson on 2014-05-02.
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import "FLEXLibrariesTableViewController.h"
#import "FLEXUtility.h"
#import "FLEXClassesTableViewController.h"
#import "FLEXObjectExplorerFactory.h"
#import <objc/runtime.h>
@interface FLEXLibrariesTableViewController ()
@property (nonatomic) NSArray<NSString *> *imageNames;
@property (nonatomic) NSArray<NSString *> *filteredImageNames;
@property (nonatomic) NSString *headerTitle;
@property (nonatomic) Class foundClass;
@end
@implementation FLEXLibrariesTableViewController
- (id)initWithStyle:(UITableViewStyle)style
{
self = [super initWithStyle:style];
if (self) {
[self loadImageNames];
}
return self;
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.showsSearchBar = YES;
[self updateHeaderTitle];
}
- (void)updateHeaderTitle
{
if (self.foundClass) {
self.headerTitle = @"Looking for this?";
} else if (self.imageNames.count == self.filteredImageNames.count) {
// Unfiltered
self.headerTitle = [NSString stringWithFormat:@"%@ libraries", @(self.imageNames.count)];
} else {
self.headerTitle = [NSString
stringWithFormat:@"%@ of %@ libraries",
@(self.filteredImageNames.count), @(self.imageNames.count)
];
}
}
#pragma mark - FLEXGlobalsEntry
+ (NSString *)globalsEntryTitle:(FLEXGlobalsRow)row {
return @"📚 System Libraries";
}
+ (UIViewController *)globalsEntryViewController:(FLEXGlobalsRow)row {
FLEXLibrariesTableViewController *librariesViewController = [self new];
librariesViewController.title = [self globalsEntryTitle:row];
return librariesViewController;
}
#pragma mark - Binary Images
- (void)loadImageNames
{
unsigned int imageNamesCount = 0;
const char **imageNames = objc_copyImageNames(&imageNamesCount);
if (imageNames) {
NSMutableArray<NSString *> *imageNameStrings = [NSMutableArray array];
NSString *appImageName = [FLEXUtility applicationImageName];
for (unsigned int i = 0; i < imageNamesCount; i++) {
const char *imageName = imageNames[i];
NSString *imageNameString = [NSString stringWithUTF8String:imageName];
// Skip the app's image. We're just showing system libraries and frameworks.
if (![imageNameString isEqual:appImageName]) {
[imageNameStrings addObject:imageNameString];
}
}
// Sort alphabetically
self.imageNames = [imageNameStrings sortedArrayWithOptions:0 usingComparator:^NSComparisonResult(NSString *name1, NSString *name2) {
NSString *shortName1 = [self shortNameForImageName:name1];
NSString *shortName2 = [self shortNameForImageName:name2];
return [shortName1 caseInsensitiveCompare:shortName2];
}];
free(imageNames);
}
}
- (NSString *)shortNameForImageName:(NSString *)imageName
{
NSArray<NSString *> *components = [imageName componentsSeparatedByString:@"/"];
if (components.count >= 2) {
return [NSString stringWithFormat:@"%@/%@", components[components.count - 2], components[components.count - 1]];
}
return imageName.lastPathComponent;
}
- (void)setImageNames:(NSArray<NSString *> *)imageNames
{
if (![_imageNames isEqual:imageNames]) {
_imageNames = imageNames;
self.filteredImageNames = imageNames;
}
}
#pragma mark - Filtering
- (void)updateSearchResults:(NSString *)searchText
{
if (searchText.length) {
NSPredicate *searchPredicate = [NSPredicate predicateWithBlock:^BOOL(NSString *evaluatedObject, NSDictionary<NSString *, id> *bindings) {
BOOL matches = NO;
NSString *shortName = [self shortNameForImageName:evaluatedObject];
if ([shortName rangeOfString:searchText options:NSCaseInsensitiveSearch].length > 0) {
matches = YES;
}
return matches;
}];
self.filteredImageNames = [self.imageNames filteredArrayUsingPredicate:searchPredicate];
} else {
self.filteredImageNames = self.imageNames;
}
self.foundClass = NSClassFromString(searchText);
[self updateHeaderTitle];
[self.tableView reloadData];
}
#pragma mark - Table View Data Source
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.filteredImageNames.count + (self.foundClass ? 1 : 0);
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *cellIdentifier = @"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
cell.textLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
}
NSString *executablePath;
if (self.foundClass) {
if (indexPath.row == 0) {
cell.textLabel.text = [NSString stringWithFormat:@"Class \"%@\"", self.searchText];
return cell;
} else {
executablePath = self.filteredImageNames[indexPath.row-1];
}
} else {
executablePath = self.filteredImageNames[indexPath.row];
}
cell.textLabel.text = [self shortNameForImageName:executablePath];
return cell;
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
return self.headerTitle;
}
#pragma mark - Table View Delegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
if (indexPath.row == 0 && self.foundClass) {
[self.navigationController pushViewController:[FLEXObjectExplorerFactory
explorerViewControllerForObject:self.foundClass
] animated:YES];
} else {
[self.navigationController pushViewController:[FLEXClassesTableViewController
binaryImageName:self.filteredImageNames[self.foundClass ? 0 : indexPath.row]
] animated:YES];
}
}
@end
@@ -8,7 +8,7 @@
#import "FLEXLiveObjectsTableViewController.h"
#import "FLEXHeapEnumerator.h"
#import "FLEXInstancesTableViewController.h"
#import "FLEXInstancesViewController.h"
#import "FLEXUtility.h"
#import "FLEXScopeCarousel.h"
#import "FLEXTableView.h"
@@ -30,13 +30,11 @@ static const NSInteger kFLEXLiveObjectsSortBySizeIndex = 2;
@implementation FLEXLiveObjectsTableViewController
- (void)loadView
{
- (void)loadView {
self.tableView = [FLEXTableView flexDefaultTableView];
}
- (void)viewDidLoad
{
- (void)viewDidLoad {
[super viewDidLoad];
// self.title = @"Live Objects";
@@ -51,13 +49,11 @@ static const NSInteger kFLEXLiveObjectsSortBySizeIndex = 2;
[self reloadTableData];
}
- (NSArray<NSString *> *)allClassNames
{
- (NSArray<NSString *> *)allClassNames {
return self.instanceCountsForClassNames.allKeys;
}
- (void)reloadTableData
{
- (void)reloadTableData {
// Set up a CFMutableDictionary with class pointer keys and NSUInteger values.
// We abuse CFMutableDictionary a little to have primitive keys through judicious casting, but it gets the job done.
// The dictionary is intialized with a 0 count for each class so that it doesn't have to expand during enumeration.
@@ -98,14 +94,12 @@ static const NSInteger kFLEXLiveObjectsSortBySizeIndex = 2;
[self updateSearchResults:nil];
}
- (void)refreshControlDidRefresh:(id)sender
{
- (void)refreshControlDidRefresh:(id)sender {
[self reloadTableData];
[self.refreshControl endRefreshing];
}
- (void)updateHeaderTitle
{
- (void)updateHeaderTitle {
NSUInteger totalCount = 0;
NSUInteger totalSize = 0;
for (NSString *className in self.allClassNames) {
@@ -159,8 +153,7 @@ static const NSInteger kFLEXLiveObjectsSortBySizeIndex = 2;
#pragma mark - Search bar
- (void)updateSearchResults:(NSString *)filter
{
- (void)updateSearchResults:(NSString *)filter {
NSInteger selectedScope = self.selectedScope;
if (filter.length) {
@@ -197,18 +190,15 @@ static const NSInteger kFLEXLiveObjectsSortBySizeIndex = 2;
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.filteredClassNames.count;
}
- (UITableViewCell *)tableView:(__kindof UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
- (UITableViewCell *)tableView:(__kindof UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView
dequeueReusableCellWithIdentifier:kFLEXDefaultCell
forIndexPath:indexPath
@@ -230,18 +220,16 @@ static const NSInteger kFLEXLiveObjectsSortBySizeIndex = 2;
return cell;
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
return self.headerTitle;
}
#pragma mark - Table view delegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NSString *className = self.filteredClassNames[indexPath.row];
FLEXInstancesTableViewController *instancesViewController = [FLEXInstancesTableViewController instancesTableViewControllerForClassName:className];
FLEXInstancesViewController *instancesViewController = [FLEXInstancesViewController instancesTableViewControllerForClassName:className];
[self.navigationController pushViewController:instancesViewController animated:YES];
}
@@ -19,8 +19,7 @@
@implementation FLEXWebViewController
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil {
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
WKWebViewConfiguration *configuration = [WKWebViewConfiguration new];
@@ -35,8 +34,7 @@
return self;
}
- (id)initWithText:(NSString *)text
{
- (id)initWithText:(NSString *)text {
self = [self initWithNibName:nil bundle:nil];
if (self) {
self.originalText = text;
@@ -46,8 +44,7 @@
return self;
}
- (id)initWithURL:(NSURL *)url
{
- (id)initWithURL:(NSURL *)url {
self = [self initWithNibName:nil bundle:nil];
if (self) {
NSURLRequest *request = [NSURLRequest requestWithURL:url];
@@ -56,16 +53,14 @@
return self;
}
- (void)dealloc
{
- (void)dealloc {
// WKWebView's delegate is assigned so we need to clear it manually.
if (_webView.navigationDelegate == self) {
_webView.navigationDelegate = nil;
}
}
- (void)viewDidLoad
{
- (void)viewDidLoad {
[super viewDidLoad];
[self.view addSubview:self.webView];
@@ -77,16 +72,14 @@
}
}
- (void)copyButtonTapped:(id)sender
{
- (void)copyButtonTapped:(id)sender {
[UIPasteboard.generalPasteboard setString:self.originalText];
}
#pragma mark - WKWebView Delegate
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
{
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
WKNavigationActionPolicy policy = WKNavigationActionPolicyCancel;
if (navigationAction.navigationType == WKNavigationTypeOther) {
// Allow the initial load
@@ -105,8 +98,7 @@
#pragma mark - Class Helpers
+ (BOOL)supportsPathExtension:(NSString *)extension
{
+ (BOOL)supportsPathExtension:(NSString *)extension {
BOOL supported = NO;
NSSet<NSString *> *supportedExtensions = [self webViewSupportedPathExtensions];
if ([supportedExtensions containsObject:[extension lowercaseString]]) {
@@ -115,8 +107,7 @@
return supported;
}
+ (NSSet<NSString *> *)webViewSupportedPathExtensions
{
+ (NSSet<NSString *> *)webViewSupportedPathExtensions {
static NSSet<NSString *> *pathExtensions = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
@@ -10,13 +10,11 @@
@implementation NSMutableArray (FLEXStack)
- (void)flex_push:(id)anObject
{
- (void)flex_push:(id)anObject {
[self addObject:anObject];
}
- (id)flex_pop
{
- (id)flex_pop {
id anObject = self.lastObject;
[self removeLastObject];
return anObject;
@@ -35,8 +33,7 @@
#pragma mark - private
- (uint64_t)totalSizeAtPath:(NSString *)path
{
- (uint64_t)totalSizeAtPath:(NSString *)path {
NSFileManager *fileManager = NSFileManager.defaultManager;
NSDictionary<NSString *, id> *attributes = [fileManager attributesOfItemAtPath:path error:NULL];
uint64_t totalSize = [attributes fileSize];
@@ -50,8 +47,7 @@
#pragma mark - instance method
- (id)initWithPath:(NSString *)currentPath searchString:(NSString *)searchString
{
- (id)initWithPath:(NSString *)currentPath searchString:(NSString *)searchString {
self = [super init];
if (self) {
self.path = currentPath;
@@ -62,8 +58,7 @@
#pragma mark - methods to override
- (void)main
{
- (void)main {
NSFileManager *fileManager = NSFileManager.defaultManager;
NSMutableArray<NSString *> *searchPaths = [NSMutableArray array];
NSMutableDictionary<NSString *, NSNumber *> *sizeMapping = [NSMutableDictionary dictionary];
@@ -31,18 +31,15 @@
@implementation FLEXFileBrowserTableViewController
+ (instancetype)path:(NSString *)path
{
+ (instancetype)path:(NSString *)path {
return [[self alloc] initWithPath:path];
}
- (id)init
{
- (id)init {
return [self initWithPath:NSHomeDirectory()];
}
- (id)initWithPath:(NSString *)path
{
- (id)initWithPath:(NSString *)path {
self = [super init];
if (self) {
self.path = path;
@@ -81,8 +78,7 @@
#pragma mark - UIViewController
- (void)viewDidLoad
{
- (void)viewDidLoad {
[super viewDidLoad];
self.showsSearchBar = YES;
@@ -109,8 +105,7 @@
#pragma mark - FLEXFileBrowserSearchOperationDelegate
- (void)fileBrowserSearchOperationResult:(NSArray<NSString *> *)searchResult size:(uint64_t)size
{
- (void)fileBrowserSearchOperationResult:(NSArray<NSString *> *)searchResult size:(uint64_t)size {
self.searchPaths = searchResult;
self.searchPathsSize = @(size);
[self.tableView reloadData];
@@ -118,15 +113,13 @@
#pragma mark - Search bar
- (void)updateSearchResults:(NSString *)newText
{
- (void)updateSearchResults:(NSString *)newText {
[self reloadDisplayedPaths];
}
#pragma mark UISearchControllerDelegate
- (void)willDismissSearchController:(UISearchController *)searchController
{
- (void)willDismissSearchController:(UISearchController *)searchController {
[self.operationQueue cancelAllOperations];
[self reloadCurrentPath];
[self.tableView reloadData];
@@ -134,18 +127,15 @@
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.searchController.isActive ? self.searchPaths.count : self.childPaths.count;
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
BOOL isSearchActive = self.searchController.isActive;
NSNumber *currentSize = isSearchActive ? self.searchPathsSize : self.recursiveSize;
NSArray<NSString *> *currentPaths = isSearchActive ? self.searchPaths : self.childPaths;
@@ -160,8 +150,7 @@
return [NSString stringWithFormat:@"%lu files (%@)", (unsigned long)currentPaths.count, sizeString];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSString *fullPath = [self filePathAtIndexPath:indexPath];
NSDictionary<NSString *, id> *attributes = [NSFileManager.defaultManager attributesOfItemAtPath:fullPath error:NULL];
BOOL isDirectory = [attributes.fileType isEqual:NSFileTypeDirectory];
@@ -184,8 +173,8 @@
if (!cell) {
cell = [[FLEXFileBrowserTableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:cellIdentifier];
cell.textLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
cell.detailTextLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
cell.textLabel.font = UIFont.flex_defaultTableCellFont;
cell.detailTextLabel.font = UIFont.flex_defaultTableCellFont;
cell.detailTextLabel.textColor = UIColor.grayColor;
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
}
@@ -201,8 +190,7 @@
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
[self.tableView deselectRowAtIndexPath:indexPath animated:YES];
NSString *fullPath = [self filePathAtIndexPath:indexPath];
@@ -224,7 +212,7 @@
if (isDirectory) {
drillInViewController = [[[self class] alloc] initWithPath:fullPath];
} else if (image) {
drillInViewController = [[FLEXImagePreviewViewController alloc] initWithImage:image];
drillInViewController = [FLEXImagePreviewViewController forImage:image];
} else {
NSData *fileData = [NSData dataWithContentsOfFile:fullPath];
if (!fileData.length) {
@@ -283,8 +271,7 @@
}
}
- (BOOL)tableView:(UITableView *)tableView shouldShowMenuForRowAtIndexPath:(NSIndexPath *)indexPath
{
- (BOOL)tableView:(UITableView *)tableView shouldShowMenuForRowAtIndexPath:(NSIndexPath *)indexPath {
UIMenuItem *rename = [[UIMenuItem alloc] initWithTitle:@"Rename" action:@selector(fileBrowserRename:)];
UIMenuItem *delete = [[UIMenuItem alloc] initWithTitle:@"Delete" action:@selector(fileBrowserDelete:)];
UIMenuItem *copyPath = [[UIMenuItem alloc] initWithTitle:@"Copy Path" action:@selector(fileBrowserCopyPath:)];
@@ -295,16 +282,14 @@
return YES;
}
- (BOOL)tableView:(UITableView *)tableView canPerformAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender
{
- (BOOL)tableView:(UITableView *)tableView canPerformAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender {
return action == @selector(fileBrowserDelete:)
|| action == @selector(fileBrowserRename:)
|| action == @selector(fileBrowserCopyPath:)
|| action == @selector(fileBrowserShare:);
}
- (void)tableView:(UITableView *)tableView performAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender
{
- (void)tableView:(UITableView *)tableView performAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender {
// Empty, but has to exist for the menu to show
// The table view only calls this method for actions in the UIResponderStandardEditActions informal protocol.
// Since our actions are outside of that protocol, we need to manually handle the action forwarding from the cells.
@@ -312,8 +297,7 @@
#if FLEX_AT_LEAST_IOS13_SDK
- (UIContextMenuConfiguration *)tableView:(UITableView *)tableView contextMenuConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath point:(CGPoint)point __IOS_AVAILABLE(13.0)
{
- (UIContextMenuConfiguration *)tableView:(UITableView *)tableView contextMenuConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath point:(CGPoint)point __IOS_AVAILABLE(13.0) {
__weak typeof(self) weakSelf = self;
return [UIContextMenuConfiguration configurationWithIdentifier:nil
previewProvider:nil
@@ -349,8 +333,7 @@
#endif
- (void)openFileController:(NSString *)fullPath
{
- (void)openFileController:(NSString *)fullPath {
UIDocumentInteractionController *controller = [UIDocumentInteractionController new];
controller.URL = [NSURL fileURLWithPath:fullPath];
@@ -358,8 +341,7 @@
self.documentController = controller;
}
- (void)fileBrowserRename:(UITableViewCell *)sender
{
- (void)fileBrowserRename:(UITableViewCell *)sender {
NSIndexPath *indexPath = [self.tableView indexPathForCell:sender];
NSString *fullPath = [self filePathAtIndexPath:indexPath];
@@ -384,8 +366,7 @@
}
}
- (void)fileBrowserDelete:(UITableViewCell *)sender
{
- (void)fileBrowserDelete:(UITableViewCell *)sender {
NSIndexPath *indexPath = [self.tableView indexPathForCell:sender];
NSString *fullPath = [self filePathAtIndexPath:indexPath];
@@ -409,15 +390,13 @@
}
}
- (void)fileBrowserCopyPath:(UITableViewCell *)sender
{
- (void)fileBrowserCopyPath:(UITableViewCell *)sender {
NSIndexPath *indexPath = [self.tableView indexPathForCell:sender];
NSString *fullPath = [self filePathAtIndexPath:indexPath];
UIPasteboard.generalPasteboard.string = fullPath;
}
- (void)fileBrowserShare:(UITableViewCell *)sender
{
- (void)fileBrowserShare:(UITableViewCell *)sender {
NSIndexPath *indexPath = [self.tableView indexPathForCell:sender];
NSString *pathString = [self filePathAtIndexPath:indexPath];
NSURL *filePath = [NSURL fileURLWithPath:pathString];
@@ -435,8 +414,7 @@
}
}
- (void)reloadDisplayedPaths
{
- (void)reloadDisplayedPaths {
if (self.searchController.isActive) {
[self updateSearchPaths];
} else {
@@ -445,8 +423,7 @@
}
}
- (void)reloadCurrentPath
{
- (void)reloadCurrentPath {
NSMutableArray<NSString *> *childPaths = [NSMutableArray array];
NSArray<NSString *> *subpaths = [NSFileManager.defaultManager contentsOfDirectoryAtPath:self.path error:NULL];
for (NSString *subpath in subpaths) {
@@ -455,8 +432,7 @@
self.childPaths = childPaths;
}
- (void)updateSearchPaths
{
- (void)updateSearchPaths {
self.searchPaths = nil;
self.searchPathsSize = nil;
@@ -467,8 +443,7 @@
[self.operationQueue addOperation:newOperation];
}
- (NSString *)filePathAtIndexPath:(NSIndexPath *)indexPath
{
- (NSString *)filePathAtIndexPath:(NSIndexPath *)indexPath {
return self.searchController.isActive ? self.searchPaths[indexPath.row] : self.childPaths[indexPath.row];
}
@@ -477,29 +452,24 @@
@implementation FLEXFileBrowserTableViewCell
- (void)forwardAction:(SEL)action withSender:(id)sender
{
- (void)forwardAction:(SEL)action withSender:(id)sender {
id target = [self.nextResponder targetForAction:action withSender:sender];
[UIApplication.sharedApplication sendAction:action to:target from:self forEvent:nil];
}
- (void)fileBrowserRename:(UIMenuController *)sender
{
- (void)fileBrowserRename:(UIMenuController *)sender {
[self forwardAction:_cmd withSender:sender];
}
- (void)fileBrowserDelete:(UIMenuController *)sender
{
- (void)fileBrowserDelete:(UIMenuController *)sender {
[self forwardAction:_cmd withSender:sender];
}
- (void)fileBrowserCopyPath:(UIMenuController *)sender
{
- (void)fileBrowserCopyPath:(UIMenuController *)sender {
[self forwardAction:_cmd withSender:sender];
}
- (void)fileBrowserShare:(UIMenuController *)sender
{
- (void)fileBrowserShare:(UIMenuController *)sender {
[self forwardAction:_cmd withSender:sender];
}
@@ -7,8 +7,9 @@
//
#import <UIKit/UIKit.h>
#import "FLEXTableViewSection.h"
@class FLEXGlobalsTableViewController;
@class FLEXGlobalsViewController;
NS_ASSUME_NONNULL_BEGIN
typedef NS_ENUM(NSUInteger, FLEXGlobalsRow) {
FLEXGlobalsRowProcessInfo,
@@ -17,8 +18,7 @@ typedef NS_ENUM(NSUInteger, FLEXGlobalsRow) {
FLEXGlobalsRowLiveObjects,
FLEXGlobalsRowAddressInspector,
FLEXGlobalsRowCookies,
FLEXGlobalsRowSystemLibraries,
FLEXGlobalsRowAppClasses,
FLEXGlobalsRowBrowseRuntime,
FLEXGlobalsRowAppKeychainItems,
FLEXGlobalsRowAppDelegate,
FLEXGlobalsRowRootViewController,
@@ -34,12 +34,12 @@ typedef NS_ENUM(NSUInteger, FLEXGlobalsRow) {
FLEXGlobalsRowCount
};
typedef NSString *(^FLEXGlobalsEntryNameFuture)(void);
typedef NSString * _Nonnull (^FLEXGlobalsEntryNameFuture)(void);
/// Simply return a view controller to be pushed on the navigation stack
typedef UIViewController *(^FLEXGlobalsTableViewControllerViewControllerFuture)(void);
typedef UIViewController * _Nullable (^FLEXGlobalsTableViewControllerViewControllerFuture)(void);
/// Do something like present an alert, then use the host
/// view controller to present or push another view controller.
typedef void (^FLEXGlobalsTableViewControllerRowAction)(FLEXGlobalsTableViewController *host);
typedef void (^FLEXGlobalsTableViewControllerRowAction)(FLEXGlobalsViewController * _Nonnull host);
/// For view controllers to conform to to indicate they support being used
/// in the globals table view controller. These methods help create concrete entries.
@@ -47,27 +47,39 @@ typedef void (^FLEXGlobalsTableViewControllerRowAction)(FLEXGlobalsTableViewCont
/// Previously, the concrete entries relied on "futures" for the view controller and title.
/// With this protocol, the conforming class itself can act as a future, since the methods
/// will not be invoked until the title and view controller / row action are needed.
///
/// Entries can implement \c globalsEntryViewController: to unconditionally provide a
/// view controller, or \c globalsEntryRowAction: to conditionally provide one and
/// perform some action (such as present an alert) if no view controller is available,
/// or both if there is a mix of rows where some are guaranteed to work and some are not.
/// Where both are implemented, \c globalsEntryRowAction: takes precedence; if it returns
/// an action for the requested row, that will be used instead of \c globalsEntryViewController:
@protocol FLEXGlobalsEntry <NSObject>
+ (NSString *)globalsEntryTitle:(FLEXGlobalsRow)row;
// Must respond to at least one of the below
// Must respond to at least one of the below.
// globalsEntryRowAction: takes precedence if both are implemented.
@optional
+ (UIViewController *)globalsEntryViewController:(FLEXGlobalsRow)row;
+ (FLEXGlobalsTableViewControllerRowAction)globalsEntryRowAction:(FLEXGlobalsRow)row;
+ (nullable UIViewController *)globalsEntryViewController:(FLEXGlobalsRow)row;
+ (nullable FLEXGlobalsTableViewControllerRowAction)globalsEntryRowAction:(FLEXGlobalsRow)row;
@end
@interface FLEXGlobalsEntry : NSObject <FLEXPatternMatching>
@interface FLEXGlobalsEntry : NSObject
@property (nonatomic, readonly) FLEXGlobalsEntryNameFuture entryNameFuture;
@property (nonatomic, readonly) FLEXGlobalsTableViewControllerViewControllerFuture viewControllerFuture;
@property (nonatomic, readonly) FLEXGlobalsTableViewControllerRowAction rowAction;
@property (nonatomic, readonly, nonnull) FLEXGlobalsEntryNameFuture entryNameFuture;
@property (nonatomic, readonly, nullable) FLEXGlobalsTableViewControllerViewControllerFuture viewControllerFuture;
@property (nonatomic, readonly, nullable) FLEXGlobalsTableViewControllerRowAction rowAction;
+ (instancetype)entryWithEntry:(Class<FLEXGlobalsEntry>)entry row:(FLEXGlobalsRow)row;
+ (instancetype)entryWithNameFuture:(FLEXGlobalsEntryNameFuture)nameFuture viewControllerFuture:(FLEXGlobalsTableViewControllerViewControllerFuture)viewControllerFuture;
+ (instancetype)entryWithNameFuture:(FLEXGlobalsEntryNameFuture)nameFuture action:(FLEXGlobalsTableViewControllerRowAction)rowSelectedAction;
+ (instancetype)entryWithNameFuture:(FLEXGlobalsEntryNameFuture)nameFuture
viewControllerFuture:(FLEXGlobalsTableViewControllerViewControllerFuture)viewControllerFuture;
+ (instancetype)entryWithNameFuture:(FLEXGlobalsEntryNameFuture)nameFuture
action:(FLEXGlobalsTableViewControllerRowAction)rowSelectedAction;
@end
@@ -76,6 +88,8 @@ typedef void (^FLEXGlobalsTableViewControllerRowAction)(FLEXGlobalsTableViewCont
/// @return The result of passing self to +[FLEXGlobalsEntry entryWithEntry:]
/// if the class conforms to FLEXGlobalsEntry, else, nil.
+ (FLEXGlobalsEntry *)flex_concreteGlobalsEntry:(FLEXGlobalsRow)row;
+ (nullable FLEXGlobalsEntry *)flex_concreteGlobalsEntry:(FLEXGlobalsRow)row;
@end
NS_ASSUME_NONNULL_END
@@ -10,19 +10,22 @@
@implementation FLEXGlobalsEntry
+ (instancetype)entryWithEntry:(Class<FLEXGlobalsEntry>)cls row:(FLEXGlobalsRow)row
{
+ (instancetype)entryWithEntry:(Class<FLEXGlobalsEntry>)cls row:(FLEXGlobalsRow)row {
BOOL providesVCs = [cls respondsToSelector:@selector(globalsEntryViewController:)];
BOOL providesActions = [cls respondsToSelector:@selector(globalsEntryRowAction:)];
NSParameterAssert(cls);
NSParameterAssert(
[cls respondsToSelector:@selector(globalsEntryViewController:)] ||
[cls respondsToSelector:@selector(globalsEntryRowAction:)]
);
NSParameterAssert(providesVCs || providesActions);
FLEXGlobalsEntry *entry = [self new];
entry->_entryNameFuture = ^{ return [cls globalsEntryTitle:row]; };
if ([cls respondsToSelector:@selector(globalsEntryViewController:)]) {
entry->_viewControllerFuture = ^{ return [cls globalsEntryViewController:row]; };
if (providesVCs) {
id action = providesActions ? [cls globalsEntryRowAction:row] : nil;
if (action) {
entry->_rowAction = action;
} else {
entry->_viewControllerFuture = ^{ return [cls globalsEntryViewController:row]; };
}
} else {
entry->_rowAction = [cls globalsEntryRowAction:row];
}
@@ -31,8 +34,7 @@
}
+ (instancetype)entryWithNameFuture:(FLEXGlobalsEntryNameFuture)nameFuture
viewControllerFuture:(FLEXGlobalsTableViewControllerViewControllerFuture)viewControllerFuture
{
viewControllerFuture:(FLEXGlobalsTableViewControllerViewControllerFuture)viewControllerFuture {
NSParameterAssert(nameFuture);
NSParameterAssert(viewControllerFuture);
@@ -44,8 +46,7 @@
}
+ (instancetype)entryWithNameFuture:(FLEXGlobalsEntryNameFuture)nameFuture
action:(FLEXGlobalsTableViewControllerRowAction)rowSelectedAction
{
action:(FLEXGlobalsTableViewControllerRowAction)rowSelectedAction {
NSParameterAssert(nameFuture);
NSParameterAssert(rowSelectedAction);
@@ -56,13 +57,6 @@
return entry;
}
#pragma mark FLEXPatternMatching
- (BOOL)matches:(NSString *)query
{
return [self.entryNameFuture() localizedCaseInsensitiveContainsString:query];
}
@end
#pragma mark - flex_concreteGlobalsEntry
@@ -0,0 +1,20 @@
//
// FLEXGlobalsSection.h
// FLEX
//
// Created by Tanner Bennett on 7/11/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import "FLEXTableViewSection.h"
#import "FLEXGlobalsEntry.h"
NS_ASSUME_NONNULL_BEGIN
@interface FLEXGlobalsSection : FLEXTableViewSection
+ (instancetype)title:(NSString *)title rows:(NSArray<FLEXGlobalsEntry *> *)rows;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,83 @@
//
// FLEXGlobalsSection.m
// FLEX
//
// Created by Tanner Bennett on 7/11/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import "FLEXGlobalsSection.h"
#import "NSArray+Functional.h"
@interface FLEXGlobalsSection ()
/// Filtered rows
@property (nonatomic) NSArray<FLEXGlobalsEntry *> *rows;
/// Unfiltered rows
@property (nonatomic) NSArray<FLEXGlobalsEntry *> *allRows;
@end
@implementation FLEXGlobalsSection
#pragma mark - Initialization
+ (instancetype)title:(NSString *)title rows:(NSArray<FLEXGlobalsEntry *> *)rows {
FLEXGlobalsSection *s = [self new];
s->_title = title;
s.allRows = rows;
return s;
}
- (void)setAllRows:(NSArray<FLEXGlobalsEntry *> *)allRows {
_allRows = allRows.copy;
[self reloadData];
}
#pragma mark - Overrides
- (NSInteger)numberOfRows {
return self.rows.count;
}
- (void)setFilterText:(NSString *)filterText {
super.filterText = filterText;
[self reloadData];
}
- (void)reloadData {
NSString *filterText = self.filterText;
if (filterText.length) {
self.rows = [self.allRows flex_filtered:^BOOL(FLEXGlobalsEntry *entry, NSUInteger idx) {
return [entry.entryNameFuture() localizedCaseInsensitiveContainsString:filterText];
}];
} else {
self.rows = self.allRows;
}
}
- (BOOL)canSelectRow:(NSInteger)row {
return YES;
}
- (void (^)(__kindof UIViewController *))didSelectRowAction:(NSInteger)row {
return (id)self.rows[row].rowAction;
}
- (UIViewController *)viewControllerToPushForRow:(NSInteger)row {
return self.rows[row].viewControllerFuture ? self.rows[row].viewControllerFuture() : nil;
}
- (void)configureCell:(__kindof UITableViewCell *)cell forRow:(NSInteger)row {
cell.textLabel.text = self.rows[row].entryNameFuture();
}
@end
@implementation FLEXGlobalsSection (Subscripting)
- (id)objectAtIndexedSubscript:(NSUInteger)idx {
return self.rows[idx];
}
@end
@@ -1,5 +1,5 @@
//
// FLEXGlobalsTableViewController.h
// FLEXGlobalsViewController.h
// Flipboard
//
// Created by Ryan Olson on 2014-05-03.
@@ -9,7 +9,7 @@
#import "FLEXTableViewController.h"
@protocol FLEXGlobalsTableViewControllerDelegate;
typedef NS_ENUM(NSUInteger, FLEXGlobalsSection) {
typedef NS_ENUM(NSUInteger, FLEXGlobalsSectionKind) {
/// NSProcessInfo, Network history, system log,
/// heap, address explorer, libraries, app classes
FLEXGlobalsSectionProcessAndEvents,
@@ -23,18 +23,6 @@ typedef NS_ENUM(NSUInteger, FLEXGlobalsSection) {
FLEXGlobalsSectionCount
};
@interface FLEXGlobalsTableViewController : FLEXTableViewController
@property (nonatomic, weak) id <FLEXGlobalsTableViewControllerDelegate> delegate;
/// We pretend that one of the app's windows is still the key window, even though the explorer window may have become key.
/// We want to display debug state about the application, not about this tool.
+ (void)setApplicationWindow:(UIWindow *)applicationWindow;
@end
@protocol FLEXGlobalsTableViewControllerDelegate <NSObject>
- (void)globalsViewControllerDidFinish:(FLEXGlobalsTableViewController *)globalsViewController;
@interface FLEXGlobalsViewController : FLEXTableViewController
@end
@@ -1,16 +1,15 @@
//
// FLEXGlobalsTableViewController.m
// FLEXGlobalsViewController.m
// Flipboard
//
// Created by Ryan Olson on 2014-05-03.
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import "FLEXGlobalsTableViewController.h"
#import "FLEXGlobalsViewController.h"
#import "FLEXUtility.h"
#import "FLEXRuntimeUtility.h"
#import "FLEXLibrariesTableViewController.h"
#import "FLEXClassesTableViewController.h"
#import "FLEXObjcRuntimeViewController.h"
#import "FLEXKeychainTableViewController.h"
#import "FLEXObjectExplorerViewController.h"
#import "FLEXObjectExplorerFactory.h"
@@ -20,23 +19,22 @@
#import "FLEXGlobalsEntry.h"
#import "FLEXManager+Private.h"
#import "FLEXSystemLogTableViewController.h"
#import "FLEXNetworkHistoryTableViewController.h"
#import "FLEXNetworkMITMViewController.h"
#import "FLEXAddressExplorerCoordinator.h"
#import "FLEXTableViewSection.h"
static __weak UIWindow *s_applicationWindow = nil;
@interface FLEXGlobalsTableViewController ()
@property (nonatomic, readonly) NSArray<FLEXTableViewSection<FLEXGlobalsEntry *> *> *sections;
@property (nonatomic, copy) NSArray<FLEXTableViewSection<FLEXGlobalsEntry *> *> *filteredSections;
#import "FLEXGlobalsSection.h"
@interface FLEXGlobalsViewController ()
/// Only displayed sections of the table view; empty sections are purged from this array.
@property (nonatomic, copy) NSArray<FLEXGlobalsSection *> *sections;
/// Every section in the table view, regardless of whether or not a section is empty.
@property (nonatomic, readonly) NSArray<FLEXGlobalsSection *> *allSections;
@end
@implementation FLEXGlobalsTableViewController
@implementation FLEXGlobalsViewController
+ (NSString *)globalsTitleForSection:(FLEXGlobalsSection)section
{
#pragma mark - Initialization
+ (NSString *)globalsTitleForSection:(FLEXGlobalsSectionKind)section {
switch (section) {
case FLEXGlobalsSectionProcessAndEvents:
return @"Process and Events";
@@ -52,17 +50,14 @@ static __weak UIWindow *s_applicationWindow = nil;
}
}
+ (FLEXGlobalsEntry *)globalsEntryForRow:(FLEXGlobalsRow)row
{
+ (FLEXGlobalsEntry *)globalsEntryForRow:(FLEXGlobalsRow)row {
switch (row) {
case FLEXGlobalsRowAppClasses:
return [FLEXClassesTableViewController flex_concreteGlobalsEntry:row];
case FLEXGlobalsRowAppKeychainItems:
return [FLEXKeychainTableViewController flex_concreteGlobalsEntry:row];
case FLEXGlobalsRowAddressInspector:
return [FLEXAddressExplorerCoordinator flex_concreteGlobalsEntry:row];
case FLEXGlobalsRowSystemLibraries:
return [FLEXLibrariesTableViewController flex_concreteGlobalsEntry:row];
case FLEXGlobalsRowBrowseRuntime:
return [FLEXObjcRuntimeViewController flex_concreteGlobalsEntry:row];
case FLEXGlobalsRowLiveObjects:
return [FLEXLiveObjectsTableViewController flex_concreteGlobalsEntry:row];
case FLEXGlobalsRowCookies:
@@ -73,15 +68,8 @@ static __weak UIWindow *s_applicationWindow = nil;
case FLEXGlobalsRowSystemLog:
return [FLEXSystemLogTableViewController flex_concreteGlobalsEntry:row];
case FLEXGlobalsRowNetworkHistory:
return [FLEXNetworkHistoryTableViewController flex_concreteGlobalsEntry:row];
return [FLEXNetworkMITMViewController flex_concreteGlobalsEntry:row];
case FLEXGlobalsRowKeyWindow:
return [FLEXGlobalsEntry
entryWithNameFuture:^NSString *{
return @"🔑 -[UIApplication keyWindow]";
} viewControllerFuture:^UIViewController *{
return [FLEXObjectExplorerFactory explorerViewControllerForObject:s_applicationWindow];
}
];
case FLEXGlobalsRowRootViewController:
case FLEXGlobalsRowProcessInfo:
case FLEXGlobalsRowAppDelegate:
@@ -101,20 +89,18 @@ static __weak UIWindow *s_applicationWindow = nil;
}
}
+ (NSArray<FLEXTableViewSection<FLEXGlobalsEntry *> *> *)defaultGlobalSections
{
static NSArray<FLEXTableViewSection<FLEXGlobalsEntry *> *> *sections = nil;
+ (NSArray<FLEXGlobalsSection *> *)defaultGlobalSections {
static NSArray<FLEXGlobalsSection *> *sections = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSArray *rows = @[
NSArray *rowsBySection = @[
@[
[self globalsEntryForRow:FLEXGlobalsRowNetworkHistory],
[self globalsEntryForRow:FLEXGlobalsRowSystemLog],
[self globalsEntryForRow:FLEXGlobalsRowProcessInfo],
[self globalsEntryForRow:FLEXGlobalsRowLiveObjects],
[self globalsEntryForRow:FLEXGlobalsRowAddressInspector],
[self globalsEntryForRow:FLEXGlobalsRowSystemLibraries],
[self globalsEntryForRow:FLEXGlobalsRowAppClasses],
[self globalsEntryForRow:FLEXGlobalsRowBrowseRuntime],
],
@[ // FLEXGlobalsSectionAppShortcuts
[self globalsEntryForRow:FLEXGlobalsRowBrowseBundle],
@@ -135,29 +121,19 @@ static __weak UIWindow *s_applicationWindow = nil;
]
];
NSMutableArray *tmp = [NSMutableArray array];
for (NSInteger i = 0; i < FLEXGlobalsSectionCount - 1; i++) { // Skip custom
sections = [NSArray flex_forEachUpTo:rowsBySection.count map:^FLEXGlobalsSection *(NSUInteger i) {
NSString *title = [self globalsTitleForSection:i];
[tmp addObject:[FLEXTableViewSection section:i title:title rows:rows[i]]];
}
sections = tmp.copy;
return [FLEXGlobalsSection title:title rows:rowsBySection[i]];
}];
});
return sections;
}
#pragma mark - Public
+ (void)setApplicationWindow:(UIWindow *)applicationWindow
{
s_applicationWindow = applicationWindow;
}
#pragma mark - UIViewController
- (void)viewDidLoad
{
- (void)viewDidLoad {
[super viewDidLoad];
self.title = @"💪 FLEX";
@@ -165,127 +141,98 @@ static __weak UIWindow *s_applicationWindow = nil;
self.searchBarDebounceInterval = kFLEXDebounceInstant;
// Table view data
_sections = [[self class] defaultGlobalSections];
_allSections = [[self class] defaultGlobalSections];
if ([FLEXManager sharedManager].userGlobalEntries.count) {
// Make custom section
NSString *title = [[self class] globalsTitleForSection:FLEXGlobalsSectionCustom];
FLEXTableViewSection *custom = [FLEXTableViewSection
section:FLEXGlobalsSectionCustom
FLEXGlobalsSection *custom = [FLEXGlobalsSection
title:title
rows:[FLEXManager sharedManager].userGlobalEntries
];
_sections = [_sections arrayByAddingObject:custom];
_allSections = [_allSections arrayByAddingObject:custom];
}
// Done button
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemDone
target:self
action:@selector(donePressed:)
];
self.sections = self.allSections;
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self disableToolbar];
}
#pragma mark - Search Bar
- (void)updateSearchResults:(NSString *)newText {
if (!newText.length) {
self.filteredSections = nil;
// Sections will adjust data based on this property
for (FLEXTableViewSection *section in self.allSections) {
section.filterText = newText;
}
// Recalculate empty sections
self.sections = [self nonemptySections];
// Refresh table view
if (self.isViewLoaded) {
[self.tableView reloadData];
return;
}
}
// Sections are a map of index to rows, since empty sections are omitted
NSMutableArray *filteredSections = [NSMutableArray array];
[self.sections enumerateObjectsUsingBlock:^(FLEXTableViewSection<FLEXGlobalsEntry *> *section, NSUInteger idx, BOOL *stop) {
section = [section newSectionWithRowsMatchingQuery:newText];
if (section) {
[filteredSections addObject:section];
}
#pragma mark - Private
- (NSArray<FLEXGlobalsSection *> *)nonemptySections {
return [self.allSections flex_filtered:^BOOL(FLEXTableViewSection *section, NSUInteger idx) {
return section.numberOfRows > 0;
}];
self.filteredSections = filteredSections.copy;
[self.tableView reloadData];
}
#pragma mark - Misc
- (void)donePressed:(id)sender
{
[self.delegate globalsViewControllerDidFinish:self];
}
#pragma mark - Table Data Helpers
- (FLEXGlobalsEntry *)globalEntryAtIndexPath:(NSIndexPath *)indexPath
{
if (self.filteredSections) {
return self.filteredSections[indexPath.section][indexPath.row];
} else {
return self.sections[indexPath.section][indexPath.row];
}
}
- (NSString *)titleForSection:(NSInteger)section
{
if (self.filteredSections) {
return self.filteredSections[section].title;
} else {
return self.sections[section].title;
}
}
- (NSString *)titleForRowAtIndexPath:(NSIndexPath *)indexPath
{
FLEXGlobalsEntry *entry = [self globalEntryAtIndexPath:indexPath];
return entry.entryNameFuture();
}
#pragma mark - Table View Data Source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return self.filteredSections ? self.filteredSections.count : self.sections.count;
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return self.sections.count;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
if (self.filteredSections) {
return self.filteredSections[section].count;
} else {
return self.sections[section].count;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.sections[section].numberOfRows;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = @"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
cell.textLabel.font = [FLEXUtility defaultFontOfSize:14.0];
cell.textLabel.font = [UIFont systemFontOfSize:14.0];
}
cell.textLabel.text = [self titleForRowAtIndexPath:indexPath];
[self.sections[indexPath.section] configureCell:cell forRow:indexPath.row];
return cell;
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
return [self titleForSection:section];
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
return self.sections[section].title;
}
#pragma mark - Table View Delegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
FLEXGlobalsEntry *entry = [self globalEntryAtIndexPath:indexPath];
if (entry.viewControllerFuture) {
[self.navigationController pushViewController:entry.viewControllerFuture() animated:YES];
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
FLEXTableViewSection *section = self.sections[indexPath.section];
void (^action)(UIViewController *) = [section didSelectRowAction:indexPath.row];
UIViewController *details = [section viewControllerToPushForRow:indexPath.row];
if (action) {
action(self);
[tableView deselectRowAtIndexPath:indexPath animated:YES];
} else if (details) {
[self.navigationController pushViewController:details animated:YES];
} else {
entry.rowAction(self);
[NSException raise:NSInternalInconsistencyException
format:@"Row is selectable but has no action or view controller"];
}
}
@@ -21,8 +21,7 @@
@implementation FLEXKeychainTableViewController
- (void)viewDidLoad
{
- (void)viewDidLoad {
[super viewDidLoad];
self.navigationItem.rightBarButtonItems = @[
@@ -38,18 +37,15 @@
[self updateHeaderTitle];
}
- (void)refreshkeychainItems
{
- (void)refreshkeychainItems {
self.keychainItems = [FLEXKeychain allAccounts].mutableCopy;
}
- (void)updateHeaderTitle
{
- (void)updateHeaderTitle {
self.headerTitle = [NSString stringWithFormat:@"%@ items", @(self.keychainItems.count)];
}
- (FLEXKeychainQuery *)queryForItemAtIndex:(NSInteger)idx
{
- (FLEXKeychainQuery *)queryForItemAtIndex:(NSInteger)idx {
NSDictionary *item = self.keychainItems[idx];
FLEXKeychainQuery *query = [FLEXKeychainQuery new];
@@ -60,8 +56,7 @@
return query;
}
- (void)deleteItem:(NSDictionary *)item
{
- (void)deleteItem:(NSDictionary *)item {
NSError *error = nil;
BOOL success = [FLEXKeychain
deletePasswordForService:item[kFLEXKeychainWhereKey]
@@ -80,8 +75,7 @@
#pragma mark Buttons
- (void)trashPressed
{
- (void)trashPressed {
[FLEXAlert makeSheet:^(FLEXAlert *make) {
make.title(@"Clear Keychain");
make.message(@"This will remove all keychain items for this app.\n");
@@ -98,8 +92,7 @@
} showFrom:self];
}
- (void)addPressed
{
- (void)addPressed {
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(@"Add Keychain Item");
make.textField(@"Service name, i.e. Instagram");
@@ -122,8 +115,7 @@
#pragma mark - FLEXGlobalsEntry
+ (NSString *)globalsEntryTitle:(FLEXGlobalsRow)row
{
+ (NSString *)globalsEntryTitle:(FLEXGlobalsRow)row {
return @"🔑 Keychain";
}
@@ -137,19 +129,17 @@
#pragma mark - Table View Data Source
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.keychainItems.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
static NSString *CellIdentifier = @"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
cell.textLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
cell.textLabel.font = UIFont.flex_defaultTableCellFont;
}
NSDictionary *item = self.keychainItems[indexPath.row];
@@ -167,13 +157,11 @@
return cell;
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
return self.headerTitle;
}
- (void)tableView:(UITableView *)tv commitEditingStyle:(UITableViewCellEditingStyle)style forRowAtIndexPath:(NSIndexPath *)ip
{
- (void)tableView:(UITableView *)tv commitEditingStyle:(UITableViewCellEditingStyle)style forRowAtIndexPath:(NSIndexPath *)ip {
if (style == UITableViewCellEditingStyleDelete) {
[self deleteItem:self.keychainItems[ip.row]];
[self.keychainItems removeObjectAtIndex:ip.row];
@@ -184,8 +172,7 @@
#pragma mark - Table View Delegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
FLEXKeychainQuery *query = [self queryForItemAtIndex:indexPath.row];
[FLEXAlert makeAlert:^(FLEXAlert *make) {
@@ -0,0 +1,44 @@
//
// FLEXRuntime.h
// FLEX
//
// Created by Tanner on 3/22/17.
// Copyright © 2017 Tanner Bennett. All rights reserved.
//
#import "TBToken.h"
@class FLEXMethod;
/// Accepts runtime queries given a token.
@interface TBRuntime : NSObject
+ (instancetype)runtime;
/// Called automatically when \c TBRuntime is first used.
/// You may call it again when you think a library has
/// been loaded since this method was first called.
- (void)reloadLibrariesList;
/// An array of strings representing the currently loaded libraries.
@property (nonatomic, readonly) NSArray<NSString*> *imageDisplayNames;
/// "Image name" is the path of the bundle
- (NSString *)shortNameForImageName:(NSString *)imageName;
/// "Image name" is the path of the bundle
- (NSString *)imageNameForShortName:(NSString *)imageName;
/// @return Bundle names for the UI
- (NSMutableArray<NSString *> *)bundleNamesForToken:(TBToken *)token;
/// @return Bundle paths for more queries
- (NSMutableArray<NSString *> *)bundlePathsForToken:(TBToken *)token;
/// @return Class names
- (NSMutableArray<NSString *> *)classesForToken:(TBToken *)token
inBundles:(NSMutableArray<NSString *> *)bundlePaths;
/// @return A list of lists of \c FLEXMethods where
/// each list corresponds to one of the given classes
- (NSArray<NSMutableArray<FLEXMethod *> *> *)methodsForToken:(TBToken *)token
instance:(NSNumber *)onlyInstanceMethods
inClasses:(NSArray<NSString*> *)classes;
@end

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