Compare commits

..

337 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
Tanner Bennett 8367342b25 Bump podspec version 2019-12-26 17:30:03 -06:00
Tanner Bennett 2df073a792 Keep search bar active between screens
Not sure why I ever added this code. Possibly to ignore a glitch on older versions of iOS? It works fine now on iOS 13, though.
2019-12-26 17:16:46 -06:00
Tanner Bennett 8236fc97cc Clean up cell reuse identifiers 2019-12-20 14:08:28 -06:00
Tanner Bennett 0364de36bd Add FLEXPluralString in FLEXUtility 2019-12-19 18:46:07 -06:00
Tanner Bennett 12195eb879 NSArray+Functional 2019-12-19 18:46:07 -06:00
Tanner Bennett acdc46c43f Add -subviews and -superview @properties to UIView 2019-12-19 18:46:07 -06:00
Tanner Bennett 52eed1b6f9 Add convenience init to some view controllers
Clean up libraries view controller
2019-12-19 18:46:07 -06:00
Tanner Bennett a91d1de9ad Change some private API accessors to use KVC
`performSelector:` can leak. The `KVC` methods are much safer and more reliable. If you pass it the exact method name, that will be called first, assuming there isn't a method with the same name but prefixed with `get`.
2019-12-19 18:46:07 -06:00
Tanner Bennett 492d2e49fe Fix bug in potentiallyUnwrapBoxedPointer: 2019-12-19 18:46:07 -06:00
Tanner Bennett 49bc439000 Delete unused class 2019-12-15 22:14:27 -06:00
Tanner Bennett 8c919cc26c Fix #359 a little better 2019-12-10 13:56:01 -06:00
Tanner Bennett 8e86ffccd6 Bump podspec version, add warning flag to podspec
Our podspec needs -Wno-unsupported-availability-guard for now
2019-12-09 15:34:20 -06:00
Tanner Bennett 228de102e7 Fix #359
Custom UIMenu callouts were not not appearing because the UITextEffectsWindow has a lower level than the FLEX window by default. Until a text field is activated, it would sit below the FLEX window, and custom callouts (copy, copy address) would not appear.
2019-12-09 14:54:41 -06:00
Tanner Bennett 37b5d1be2a Swipe to delete keychain items 2019-12-09 10:48:28 -06:00
renwei.chen ef8f866330 Bug fixes
- Fix root view controller global not working
- Fix crash in keychain viewer when viewing NSData
2019-12-09 10:48:28 -06:00
Tanner Bennett e862b81734 Update README.md 2019-12-05 17:26:10 -06:00
Tanner Bennett a3f66b3f87 Fix scope carousel not working properly on iOS 13 2019-12-02 23:13:38 -06:00
Tanner Bennett 3e12ad9887 Add explicit header titles to several screens
Some screens used the ugly "plain" table view or used a group table view with no title. Both look awful. This commit updates the cookies, keychain, libraries, and live objects screens to use grouped table views with a descriptive section header, like "5 cookies" or "123 of 456 objects, 30 MB"

Some screens, like live objects and keychain, would display the number of items in the navigation bar title. That has been removed; they now used a fixed title.

Also, rename keyChainItems → keychainItems
2019-11-27 17:00:35 -06:00
Tanner Bennett fe36b59b4c Restore search bar appearing initially
In c047fbc5 I made it so that the search bar would not appear initially—except of of course for the globals screen in iOS 13, where it shows by default no matter what. We did this by toggling navigationItem.hidesSearchBarWhenScrolling in `viewWillAppear:` and `viewDidAppear:`.

The method we were using to reveal the search bar initially while letting it hide as you scroll would cause a weird visual glitch where the search bar would stay pinned to the top of the screen as you scroll, but the navigation bar would be transparent.

I've managed to work around that bug by calling `setNeedsLayout` and `layoutIfNeeded` on `self.navigationController.view`
2019-11-27 16:48:32 -06:00
Tanner Bennett b735a69c1b Fix #248
Fixes view controllers and other objects not appearing in references
2019-11-26 18:08:47 -06:00
Tanner Bennett 7b1b6f9e24 iOS 13: Fix long-press copy menu in network pages 2019-11-26 12:09:30 -06:00
Tanner Bennett c047fbc581 Misc fixes
- Typo
- Remove <FLEX/...> import syntax added by #353 which causes compilation errors when building FLEX as a part of a different target (as opposed to building it as its own target)
- Fix iOS 13 glitch
2019-11-25 15:08:06 -06:00
Tanner Bennett 40239524d1 Fix #351 2019-11-25 12:13:10 -06:00
Dominik Pich 2f93050e2e Add Searchbar to Database Table Viewer Tables List #352
This Commit adds a Searchbar to Database Table Viewer Tables List.

For that it turns FLEXTableListViewController into a FLEXTableViewController sublcass, calls showSearchbar=YES and implements updateSearchResults
2019-11-19 13:33:09 -08:00
Tanner Bennett dc8ac6c195 UIView+Layout → UIVIew+FLEX_Layout 2019-11-13 20:23:20 -06:00
Tanner Bennett 8edf6b4ad6 Bump podspec verison 2019-11-13 20:09:39 -06:00
Tanner Bennett 4d019046bc Add missing availability guards 2019-11-13 20:09:39 -06:00
Javier Navarro 67359023f4 Remove double semicolon (#348) 2019-11-11 09:09:16 -06:00
GGLGeek a6cbfbd3fd Fix stack memory being used in async block (#344) 2019-11-08 13:01:32 -06:00
Javier Navarro b7cac1fe48 Address "This block declaration is not a prototype" warning (#345)
Thanks @jnavarrom
2019-11-08 11:12:36 -06:00
Tanner Bennett 226e0cd803 Update to recommended project settings 2019-10-26 15:55:41 -05:00
Elfred Pagan 53538bfead Add context menu to file browser in iOS 13.
The current implementation is deprecated, which limits the functionality
of the file browser.
2019-10-11 13:26:54 -07:00
LAgagggggg a78bf1b22f Fix crash during NSURLSession's resume 2019-10-10 13:39:12 -04:00
Tanner Bennett 0803b46f9d Fix #329
Whoops. Thanks @DGh0st

Also fix a build issue, a typo in some macros
2019-09-15 12:49:43 -05:00
Tanner Bennett f64e6ec3c9 Rename tests target 2019-09-14 15:24:23 -05:00
Tanner Bennett 46652ac73d Improve readability of FLEXRuntimeUtility 2019-09-14 15:24:23 -05:00
Tanner Bennett e30b1854fc Fix typos in globals screen
- Don't use class names for app delegate or root vc
- Reorder some globals rows, also
2019-09-14 15:22:33 -05:00
Tanner Bennett 8a5e57c1d2 Clean up runtime property additions code
Macros make the code easier to read and maintain. Also, add -constraints property to UIView.
2019-09-14 15:22:33 -05:00
Tanner Bennett f582c9ae0d Misc cleanup 2019-09-14 15:22:31 -05:00
Tanner Bennett 8a762a66ae Utilize NSURLQueryItem for storing query params 2019-09-14 15:11:17 -05:00
Tanner Bennett d537d3c79e Fix image viewer not always showing share sheet 2019-09-14 15:10:31 -05:00
Tanner Bennett 07f0b07a91 Fix #324, crash when viewing NSNumber 2019-08-27 18:20:07 -05:00
Tanner Bennett 555b57941d Add SSKeychain license 2019-08-20 19:46:16 -05:00
Tanner Bennett 4be2b119d1 Add more powerful alerts to the keychain screen 2019-08-20 19:46:16 -05:00
Tanner Bennett 8255c7fe79 General keychain cleanup
- KeyChain → Keychain
- Misc renames and refactorings
- Change title
- Tabs to spaces
- Indentation
2019-08-20 19:46:16 -05:00
ray a21e5ea158 Initial support for viewing keychain items 2019-08-20 19:46:16 -05:00
Tanner Bennett ac4c50b62c Adopt FLEXAlert
- Add FLEXAlert, a builder-oriented UIAlertController wrapper
- Replace all uses of UIAlertController with FLEXAlert
- Moves some alert methods from FLEXUtility to FLEXAlert
2019-08-20 19:07:28 -05:00
Tanner Bennett ca919a4188 Silence deprecation warnings in UICatalog
We will rewrite the whole project eventually per #314
2019-08-20 16:14:05 -05:00
Iulian Onofrei b9c9af5509 Fix explorer not showing up when not specifying a scene 2019-08-20 09:47:01 -05:00
Chaoshuai Lu 6cbfa63d48 Add named struct field parsing support to fix the struct iVar issues 2019-08-19 12:57:21 -05:00
Ryan Olson c3066a7847 Convert property editor alert view to UIAlertController 2019-08-19 10:37:02 -07:00
Ryan Olson 16fbab783e Convert file browser delete/rename to UIAlertController 2019-08-19 10:37:02 -07:00
Ryan Olson a58314a825 Convert alerts in file browser/FLEXUtility to UIAlertController 2019-08-19 10:37:02 -07:00
Ryan Olson 89b9ece45d Convert network settings action sheet to UIAlertController 2019-08-19 10:37:02 -07:00
Ryan Olson b5d5867bc1 Convert alerts in FLEXNetworkTransactionDetailTVC to UIAlertController 2019-08-19 10:37:02 -07:00
Chaoshuai Lu 5a54f5808d Add long double support in FLEXRuntimeUtility 2019-08-19 11:51:09 -05:00
Chaoshuai Lu 664a39e0f1 Add CGVector and NSDirectionalEdgeInsets support 2019-08-19 11:48:04 -05:00
Chaoshuai Lu 9935860efb Fix struct field offset calculation when enumerating struct encoding 2019-08-19 11:17:13 -05:00
Chaoshuai Lu 25a05eec1a Fix issues in FLEXRuntimeUtility 2019-08-18 19:09:25 -07:00
Ryan Olson 32c0983bb7 Let Xcode make the changes it wants to the project files 2019-08-18 14:15:11 -07:00
Ryan Olson 3650da6c12 Remove support for long-deprecated global status bar management 2019-08-18 13:22:30 -07:00
Ryan Olson (IG) 40b52120d0 Update version numbers in readme 2019-08-18 13:14:46 -07:00
Chaoshuai Lu 74eb6e180b Use FLEXTypeEncoding enum and kFLEXUtilityAttribute whenever possible 2019-08-18 13:06:27 -07:00
Ryan Olson (IG) 4c2e921f9e Bump deployment target to 9.0 2019-08-18 12:59:25 -07:00
Chaoshuai Lu bba5b8b72c Remove absolute path from FLEX.codeproj 2019-08-18 12:54:58 -07:00
Iulian Onofrei e7290bc84f Fix build error 2019-08-16 18:12:37 -05:00
Iulian Onofrei f7619cdbf2 Fix build errors when using UIKit for Mac (Catalyst) 2019-08-16 16:01:48 -05:00
Tanner Bennett bff9f1dd89 Remove redundant property attributes
Object properties are strong by default, and primitive ones are assign by default. Verbosity is nice, but in this case it introduces unnecessary cognitive load.

Remove all usage of `strong` and `assign` property attributes
2019-08-16 12:48:35 -05:00
Tanner Bennett 81b7ccea22 Avoid using [[* alloc] init*] where possible
Prefer shorthand initializers, like +new or +stringWithCString:encoding:
2019-08-16 12:48:34 -05:00
Tanner Bennett f2c8ede0e0 Use dot syntax for properties
Replaces the following method calls with dot syntax:
- count, length, UTF8String, CGColor, contentOffset, firstObject, lastObject, allObjects, allKeys, allValues, subviews, scale, frame, bounds, bytes

Also replaces various UIKit and Foundation singleton method calls with dot syntax, such as UIApplication.sharedApplication. These are all `class` properties now and Xcode will autocomplete them.

Also fixes a couple warnings.
2019-08-16 12:33:11 -05:00
Iulian Onofrei adf2fc56e8 Add support for new UIScene APIs (#304) 2019-08-16 11:39:42 -05:00
Tanner Bennett 78a34a8437 Fix bug in FLEXArgumentInputObjectView
Class was not properly detecting JSON encodable classes. We now parse class names out of the type encoding and turn them into Class objects to check where they fall in the class hierarchy.
2019-08-14 11:50:53 -05:00
Tanner Bennett aa6bbfb7e7 Fix FLEXTableView table header behavior on iOS 13 2019-08-14 11:35:24 -05:00
Tanner Bennett c907a98099 Remove FLEX_AT_LEAST_IOS11_SDK
Also @available formatting cleanup
2019-08-13 18:13:02 -05:00
Tanner Bennett d3ae20bebe Custom scope bar
Adds a custom "segmented control" for use on the object explorers as well as the heap explorer, due to bugs with the default implementation of the UISearchBar scope bar, and the limited size of the scope bar.

The new scope bar will scroll and display the entire class hierarchy for any object. As for the heap explorer, we can switch back to the native scope bar for iOS 13 since iOS 13 has fixed the biggest bugs.
2019-08-13 17:06:15 -05:00
Tanner Bennett b69560e62e More global screen upgrades
Refactored FLEXGlobalsEntry to allow FLEXObjectExplorerFactory to conform to it and provide a different object based on the row type

Also added new rows: NSProcessInfo.processInfo, UIPasteboard.generalPasteboard, explore bundle/container
2019-08-08 17:22:51 -05:00
Tanner Bennett b01309678c Make FLEXGlobals* enums public 2019-08-08 17:22:51 -05:00
Tanner Bennett eb4160636f Upgrades to the globals screen
- Refactor FLEXGlobalsTableViewController
- Add search to FLEXGlobalsTableViewController
- FLEXGlobalsTableViewControllerEntry → FLEXGlobalsEntry
- Add FLEXTableViewSection to help with new search code
- Add `hidesSearchBarInitially` to FLEXTableViewController
2019-08-08 17:22:51 -05:00
Tanner Bennett 6d489e72c5 Fix scope bar not notifying on change 2019-08-08 17:08:34 -05:00
Tanner Bennett 156eb8cbe1 Add a placeholder for object argument input fields 2019-08-08 13:19:26 -05:00
Tanner Bennett e98abb1e23 Allow passing addresses as params for id args
Allow passing addresses as params for id args
2019-08-08 13:19:26 -05:00
Tanner Bennett e88c286f9e ...JSONObjectView → FLEXArgumentInputObjectView 2019-08-08 13:19:26 -05:00
德夫 0de3a9d65c More beta SDK compatability checks
Shield more beta APIs in FLEX_AT_LEAST_IOS13_SDK
2019-07-30 10:41:36 -05:00
Iulian Onofrei f98e0622b5 Migrate to WKWebView 2019-07-25 16:57:25 -05:00
Tanner Bennett 85cc51bfd3 Re-organize + group the globals list into sections 2019-07-11 13:19:46 -05:00
Tanner Bennett c3bb4ff0d3 Refactor the globals view controller list
FLEXGlobalsTableViewControllerEntry now has a method to be initialized with a class conforming to a new protocol, FLEXGlobalsTableViewControllerEntry, which itself provides the properties the entry needs. This removes a lot of boilerplate from the globals view controller itself.
2019-07-10 14:41:30 -05:00
Tanner Bennett 55b579a34e Copy attribute has no effect on read-only properties 2019-07-10 14:33:24 -05:00
Tanner Bennett 3a5a242346 Group file browser classes together 2019-07-10 14:33:24 -05:00
Tanner Bennett e3ac0d6ecc Show explorer by default #if DEBUG in example proj 2019-07-10 13:23:31 -05:00
Tanner Bennett 211e2956ca Hide useless standardUserDefaults keys 2019-07-10 13:23:31 -05:00
Tanner Bennett efed18c386 Fix file browser not deserializing certain objects 2019-07-10 13:23:31 -05:00
Tanner Bennett 3dd9a5705d Remove superfluous table section header styling 2019-07-10 13:23:31 -05:00
Tanner Bennett 462b38a473 Refactor file browser
- Rename reload/update methods for clarity
- `updateSearchPaths` will only refresh the table as new results come in, instead of right away which would result in the table showing 0 items while results were being gathered
2019-07-10 13:23:31 -05:00
Tanner Bennett 7979fcd896 Adopt FLEXTableViewController 2019-07-10 13:23:31 -05:00
Tanner Bennett 642b1810c5 Add FLEXTableViewController 2019-07-09 16:42:46 -05:00
Tanner Bennett e0b6eec03c Fix folder links in project
Disable strict prototypes warning
2019-07-09 16:42:46 -05:00
Tanner Bennett 9c4ff2ddd8 typeof → __typeof
Also, obtain strong reference to weakSelf once inside block
2019-07-09 16:42:46 -05:00
Tanner Bennett 0ddd202852 Add missing braces to globals VC switch 2019-07-09 16:42:46 -05:00
Tanner Bennett 81be8e1316 Wrap iOS 13 UIColor APIs in #if FLEX_AT_LEAST_IOS13_SDK 2019-07-09 16:35:06 -05:00
Tanner Bennett 4e2e05b451 Support dark mode in system log 2019-07-07 23:17:03 -05:00
Tanner Bennett 4262074948 Show RGB / HLS / A when exploring a color 2019-07-07 23:17:03 -05:00
Tanner Bennett 0f98a35643 Update UICatalog for dark mode
Wouldn't have been a problem if they were just using "default" everywhere, but for some reason they set all the labels to "Dark Text Color" and the backgrounds to white
2019-07-07 23:17:03 -05:00
Tanner Bennett 4c1fceac54 Fix color explorer "color" row bug 2019-07-07 23:17:03 -05:00
Tanner Bennett 45996df0c2 Refactor FLEXToolbarItem
- Use "system" style instead of "custom" style
   - This alleviates the need for using NSAttributedString at all
- Use FLEXColor
2019-07-07 23:17:03 -05:00
Tanner Bennett 1669b205da Refactor FLEXColor
- More consistent method names
- Use dynamic system colors instead of hard-coding dynamic colors
   - If we run under iOS 12 or earlier, we only have light mode anyway
2019-07-07 23:17:03 -05:00
Benny Wong 9f1d988651 Dark Mode: Add support for icons (#290)
* [DarkMode] Add comments and rearrange flex color header

* [DarkMode] Load icons as templates

* This will allow us to tint them appropriately for dark mode

* [DarkMode] Add support for dark mode for the icons

* [DarkMode] Add support for dark mode for hierarchy indent

* [DarkMode] Add dark mode support for property editor
2019-06-21 11:23:26 -07:00
Ryan Olson (IG) 5541e3c683 Add missing static to fix build 2019-06-18 19:41:54 -07:00
Benny Wong 6632703b70 Initial Dark Mode work (#289)
* [DarkMode] Migrate existing colors to semantic colors

* This commit introduces FLEXColor that defines the semantic colors
* This migrates most, not all, of the spots that need dynamic colors

* [DarkMode] Use dynamic color providers for iOS13+

* This colorWithDynamicProvider API is intentionally scoped only
  to handle light and dark mode (and not handling contrast, level,
  weight, etc to keep things simple

* [DarkMode] Remove last reference for scrollViewGrayColor

* [DarkMode] Rename backgroundColor* to systemBackgroundColor*

* This matches more closely with iOS13 UI element color naming scheme

* [DarkMode] Update network history rows to support dark mode

* [DarkMode] Update network history cell text color
2019-06-18 19:26:40 -07:00
Ryan Olson (IG) eebfffe4a6 Remove unused method
Came across this while debugging something else, minor cleanup.
2019-06-17 13:47:45 -07:00
Tanner Bennett 70264c1cd5 Fix FLEXPointerIsValidObjcObject sometimes failing
We now check if the class returned from object_getClass is readable because object_getClass can return a garbage value when given a non-nil pointer to a non-object
2019-06-13 17:54:39 -05:00
Tanner Bennett 0a124a2424 Add bundle explorer with NSBundle shortcuts
Useful for exploring the contents of the .app or some other bundle
2019-06-13 17:38:37 -05:00
Tanner Bennett 2a34f21667 Refactor object explorers—shortcuts
The root object explorer class now provides a mechanism to easily add property shortcuts in a subclass without a lot of boilerplate. Every shortcut is drillable by default.

Quite a bit of code was lifted out of the view explorer for this.
2019-06-13 17:38:01 -05:00
Tanner Bennett 62b26036ed Add images to example project
To be used for testing the image viewer and share sheets
2019-06-13 17:35:38 -05:00
Tanner Bennett 611b861678 Clean up file browser vc
Long press to copy item path
2019-06-13 17:34:42 -05:00
Tanner Bennett c7bc875b0e Share actual file instead of file path 2019-06-13 17:32:24 -05:00
EuanChan 841f3f9775 Show document share sheet for folders and files 2019-06-13 17:32:24 -05:00
Ryan Olson (IG) 8b225d2046 Fix retain cycle caught by infer
Retain cycle at FLEXSystemLogTableViewController.m line 47, column 9 involving the following objects:
(1) a block that captures self
(2) object of type FLEXSystemLogTableViewController* --> _logController, last assigned on line 47
(3) object returned by withUpdateHandler of type FLEXOSLogController* --> _updateHandler, last assigned on line 47.
2019-05-31 17:32:53 -07:00
Ryan Olson (IG) cf9bd2335b Fix null dereference infer warnings 2019-05-31 17:25:52 -07:00
Ryan Olson (IG) e07bfa8d5f Push editor for readonly properties that actually have setters
There's a weird case with frame and bounds on UIView where the properties are marked as readonly but setters exist. If we can call the setter, it more useful to push an editor for properties like these.
2019-05-30 19:38:20 -07:00
Antoine Cœur 17e194b69d various typos 2019-05-16 10:21:04 -05:00
Javier Navarro f86cb8a81f Improve image detection
Not all images have an extension
2019-05-16 10:13:32 -05:00
EuanChan 739c28cf81 fix missing system frameworks dependence 2019-05-10 13:11:09 -05:00
Tanner Bennett fcdb33fce2 Clean up FLEXObjcInternal.mm 2019-04-24 12:27:41 -05:00
Tanner Bennett 1eb8e4f430 Fix various warnings 2019-04-24 11:32:08 -05:00
Tanner Bennett 1d937777c0 Add address explorer 2019-04-24 11:11:25 -05:00
Tanner Bennett 308afda5c2 Improve FLEXPointerIsValidObjcObject
Previously, it assumed any address you gave it was valid and readable. It no longer makes this assumption.
2019-04-24 11:11:25 -05:00
Tanner Bennett f7b00e02ee Change UICatalog bundle ID 2019-04-24 11:11:25 -05:00
Tanner Bennett 0ff29e1f90 Add +[FLEXUtility alert:message:from:] 2019-04-24 11:11:25 -05:00
Tanner Bennett 3fe31e8628 Always display a description
Except while searching of course
2019-04-18 09:34:08 -05:00
Tanner Bennett 33263bfcfa Allow copying object address
Long press on the description row in an explorer screen
2019-04-18 09:34:08 -05:00
Tanner Bennett d6caab29dc Organize ObjectExplorers/
- Group classes into folders (Views, Controllers)
- Add FLEXTableViewCell, FLEXSubtitleTableViewCell
- FLEXMultilineTableViewCell inherits from FLEXTableViewCell
- FLEXTableViewCell makes it easy to add custom UIMenuItem commands to any cell without subclassing it or exposing any state to it
2019-04-18 09:34:08 -05:00
Tanner Bennett 5d181adcb8 Add example archived object in demo app 2019-04-17 11:52:45 -05:00
zhangpeng 4ba2fdc289 Support viewing archived objects in file browser 2019-04-17 11:52:45 -05:00
Tanner Bennett 140dc32775 Update pod version and maintainer 2019-04-16 11:21:05 -05:00
Tanner Bennett 78568cd5be Fix FLEX not being embedded into example app 2019-04-16 11:14:52 -05:00
Tanner Bennett b010cdb072 Refactor file browser view controller
Fix #251
2019-04-16 11:07:35 -05:00
Tanner Bennett 37e299733b Fix #261
Crash: +[NSString stringWithUTF8String:]: NULL cString
2019-04-12 13:46:56 -05:00
Tanner Bennett f3a1587cf1 Replace UICatalog launch images with launch xib 2019-04-12 13:34:11 -05:00
Tanner Bennett 821ca1683b Update project settings, Xcode 10.2 2019-04-12 13:25:00 -05:00
Chengming Liao 7e13ca2757 Update FLEXMultiColumnTableView.m
indentation
2019-04-12 10:13:03 -05:00
Chengming Liao 129c91c876 add compiler flags 2019-04-12 10:13:03 -05:00
Chengming Liao d2f6ff0b40 Fix safeArea for database content view 2019-04-12 10:13:03 -05:00
Alexander Leontev 69414e4174 Don't shorten curl 2019-04-12 10:12:09 -05:00
Lanbo Zhang 0654fb4b5f podfile should include .mm file 2019-04-12 10:10:09 -05:00
Tanner Bennett d7d40e6d27 Fix #140, system log messages work
more progress
2019-03-30 16:56:46 -05:00
Tanner Bennett bec7e0c229 Correct header comments
Some headers were reading UICatalog inside the FLEX project
2019-03-30 15:12:55 -05:00
Tanner Bennett 82a19e41e7 Fix prettyArgumentComponentsForMethod: bug
#178 made methods with no arguments appear to take one argument by forcibly returning the selector name as the only argument. This is not desired behavior. Updating the test to reflect desired behavior reveals this.

This commit makes this method return an empty array when selectors consist of one component, and does some housekeeping on the tests added in #178.
2019-03-30 15:10:59 -05:00
Tanner Bennett 867ae614e5 Detect and unbox pointers to objects from void *
- Also unbox C strings into NSString
- Also adds return type encoding string to method calling view controller
2019-03-30 15:10:58 -05:00
Tanner Bennett 22b7c6ccc7 Add helper methods to FLEXRuntimeUtility
- Add FLEXTypeEncoding enum
- Can check whether arbitrary poiner is valid object
- Can get return type encoding for method
- Can unbox raw pointers from NSValue into actual objects, or unbox C strings into NSStrings

Code copied from the Objc runtime complies with ASPL
2019-03-30 13:35:39 -05:00
ThePantsThief 9e9704580a Fix #168 by restructuring try-catch branching 2019-03-30 13:19:17 -05:00
Tanner Bennett 1ef608cf8a Fix #245
`#if __arm64__` is not a sufficient check for whether a platform is 64 bit. `__LP64__` appears to be a better candidate.

`MAX_REALISTIC_ADDRESS` was wrongly being set to `INT_MAX` on some 64 bit platforms.
2018-11-23 01:20:55 -06:00
Tanner Bennett b64cd37ec6 Add "Get" to readwrite editor screens, fix #235
Previously you could only "Set" mutable ivars or properties. This commit adds a "Get" button to the same screen to allow you to view the current value instead. Also works in the user defaults explorer.

It may be worth considering other approaches to this entirely, such as an alert that asks you if you want to get or set the ivar/property before a new screen is even pushed, or maybe a "Get" button as an accessory view on the rows of mutable ivars/properties.
2018-11-23 00:00:30 -06:00
Tanner Bennett 44e9d55fb8 Fix crash surrounding SwiftObject subclasses
SwiftObject subclasses cannot be safely inspected with the Objc runtime, attempts to do so sometimes lead to crashes
2018-11-22 16:09:21 -06:00
Geor Kasapidi ab9515caaf Allow registration of content type viewers (#241) 2018-11-09 06:07:09 -06:00
Tanner Bennett 0dd0fc9418 Update to recommended project settings
Silence warning for "implementing deprecated method" in Apple's own sample code.
2018-11-04 02:34:19 -06:00
Tanner Bennett 24d5f3e9b2 Add FLEXColorExplorerViewController
Provides a visual of the color for all UIColor objects.
2018-11-04 02:34:19 -06:00
Terry Lewis 693f57eef7 Added view-color indicator to cells within view-hierarchy list. (#239) 2018-11-04 02:30:40 -06:00
Tanner Bennett 7c17ce0787 Fix Instances table view bug
Accidentally returned a FLEXObjectRef and not the object itself
2018-10-17 16:52:40 -05:00
Tanner Bennett 400a3ccd1c Add +[NSBundle mainBundle] to global list 2018-10-17 16:52:40 -05:00
Colin Humber a8cdac1872 Fixed strict prototype warnings (#231) 2018-08-23 14:25:31 -05:00
zhaogyrain dedac1f98d Fix #225
Fixes the SQL editor clipping under the navigation bar on iPhone X
2018-08-08 23:35:40 -05:00
Tanner Bennett efa317f0d1 Merge pull request #227 from NSExceptional/organize-refs-pr
Group similar "objects with ivars referencing this object"
2018-07-30 18:08:52 -05:00
Tanner Bennett 9b55bb10de Group similar "objects referencing this object" 2018-07-24 20:20:30 -05:00
Tanner Bennett 122fb41fa8 Merge pull request #228 from NSExceptional/inheritance-pr
Additional object explorer scopes
2018-07-19 16:29:48 -05:00
Tanner Bennett d6b5e8c77d Additional object explorer scopes
Scopes now include:
- No inheritance (base class)
- Base class with parent attributes
- Base class with all inherited attributes, except NSObject
- NSObject attributes only

It is unusual that you need to see anything but the parent's attributes alongside those of the base class, and you especially rarely need to see NSObject attributes.
2018-07-19 16:29:15 -05:00
Tanner Bennett a6ad98dd53 Catch exceptions thrown by [value description] 2018-07-12 22:33:07 -05:00
Tanner Bennett cc35f2086a Recommended project settings (9.2) 2018-07-12 22:32:05 -05:00
Tanner Bennett 7038aae6db ExplorerToolbar → Toolbar 2018-07-12 22:31:23 -05:00
Tanner Bennett f5433153d0 Only scroll log view if new messages have arrived
This fixes a bug where being scrolled to the bottom of the system log screen would make the table view stutter repeatedly because it was trying to scroll to the bottom while already being at the bottom.
2018-07-12 22:22:12 -05:00
Tanner Bennett 8b7c59d949 Fix #117: limit network response size
Limit cached network response size to 50 MB
2018-07-12 22:22:12 -05:00
Tanner Bennett faef524b6c Merge pull request #220 from bharat/master
Resolve Xcode 9.3 warnings and deprecations
2018-07-11 14:19:59 -05:00
Tanner Bennett a2bdc03684 Merge pull request #188 from NSExceptional/master
Simplify view controller presentation code
2018-07-10 20:19:36 -05:00
Tanner Bennett bd5f9740b7 Simplify view controller presentation code
Removes duplicate code related to presenting a view controller from the
FLEX window (usually triggered by a toolbar button).

This adds `-[FLEXExplorerViewController
presentOrDismissViewControllerFromToolbar:shouldDismiss:completion:]`
which also makes it easier for others to add their own toolbar buttons
to present some other kind of screen, or for FLEX itself to utilize
again in the future.
2018-07-10 17:57:04 -05:00
Ryan Olson 505bb2ca41 Merge pull request #198 from NSExceptional/exception-pr
Reveal exception reason in dialog
2018-07-10 10:07:37 -07:00
Ryan Olson 009711ab3f Merge pull request #199 from NSExceptional/log-pr
System log will cache messages
2018-07-10 10:06:10 -07:00
Ryan Olson 7ad7653cdf Merge pull request #226 from pujiaxin33/feature_AddShareToFileBrowser
add share  action to FLEXFileBrowserTableViewController
2018-07-10 10:02:35 -07:00
Ryan Olson e5f51e4dfa Merge pull request #222 from NSExceptional/heap-enum-fix
Fix heap enumeration crash
2018-07-10 09:49:08 -07:00
pjx 92029d2b43 add share action to FLEXFileBrowserTableViewController 2018-07-06 12:01:50 +08:00
Bharat Mediratta 7da059791e Further amendment to macro definition 2018-06-04 14:00:18 -07:00
Bharat Mediratta af57527961 Minor cleanups in macros and indentation 2018-06-04 13:57:41 -07:00
Bharat Mediratta 9a8f45663e Revert update to Xcode 9.3; Stay at Xcode 8 for now 2018-06-04 13:56:54 -07:00
Tanner Bennett 8528c8a1f6 System log will cache messages
fix paren
2018-05-21 06:51:17 -05:00
Tanner Bennett d682fd0ace Fix heap enumeration crash
We only need to enumerate the `DefaultMallocZone` zone to find objects we care about.
2018-05-20 02:16:22 -05:00
Tanner Bennett 31af87a81e Reveal exception reason in dialog
When a user-invoked method call fails, FLEX presents a dialog informing you that it failed.

In practice, it is more useful to see the exception name and reason than the (potentially crazy long) object description.
2018-05-07 20:15:55 -05:00
Bharat Mediratta 386d6ae06a UITouch.maximumForcePossible is only available in iOS 9.0+ 2018-05-03 12:33:00 -07:00
Bharat Mediratta df79ae7971 Fix deprecation warnings
Replace:
 - (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation
 - (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation

With:
 - (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
2018-05-03 12:18:55 -07:00
Bharat Mediratta d30c642707 Adjust FLEX_AT_LEAST_IOS11_SDK to work with -Wexpansion-to-defined 2018-05-03 12:10:22 -07:00
Bharat Mediratta f463e2b43e Xcode 9.3 automated fixes 2018-05-03 12:07:46 -07:00
Shoaib Meenai ed49a4fc89 Use FOUNDATION_EXTERN for global variable
Global variables that can be accessed from both C (or Objective-C) and
C++ (or Objective-C++) source files should be marked `extern "C"` when
in C++ mode (which is what `FOUNDATION_EXTERN` does), to ensure
consistent access across languages.
2018-04-03 21:40:04 -07:00
Chaoshuai Lu b897250fde Fix generics 2018-03-22 16:41:55 -07:00
Greg Heo 06709a5afe Reorder to match order in Installation section 2018-01-23 23:07:47 -08:00
Greg Heo 6eee9e6080 Refactor Installation section 2018-01-23 23:07:47 -08:00
Greg Heo c50e6e51c5 Add Swift example 2018-01-23 23:07:47 -08:00
Marcus Ficner d1e9248695 Fix typo 2018-01-23 23:05:10 -08:00
Chaoshuai Lu a535f10d0c Add generics to foundation collection classes 2018-01-23 23:04:33 -08:00
Chaoshuai Lu 009aa5e3f9 Add iPhone X safe area handling to layout the toolbar correctly 2017-12-17 23:12:41 -08:00
Chaoshuai Lu 675f03fc71 Make Travis use xcodebuild (and xcpretty) instead of (deprecated) xctool 2017-12-17 22:48:15 -08:00
Ryan Olson 99eccdf4c3 Merge pull request #178 from tikoyesayan/master
Fix crash in FLEXRuntimeUtility
2017-08-27 17:22:00 -07:00
Ryan Olson 29afa5e80f Merge pull request #177 from CodeLife2012/networkbug
Should keep the same completion logic
2017-08-27 16:53:20 -07:00
Ryan Olson bf26bc6539 Fix formatting from 62ef95f 2017-08-27 15:56:08 -07:00
Ryan Olson f7b40646e2 Fix network search bar showing over pushed request detail pages 2017-08-27 15:52:22 -07:00
Ryan Olson 84c1fb159b Merge pull request #194 from adysart/xcode9
Xcode9
2017-08-27 15:29:13 -07:00
Aidan Dysart 62ef95ff93 satisfy the -Wstrict-prototypes clang warning 2017-08-08 11:01:10 -07:00
Aidan Dysart 731b729db7 Upgrade project and scheme for Xcode 9, enable default warnings 2017-08-08 11:00:48 -07:00
Tigran Yesayan a1c464d1a7 Add missing free function in testMethodListForClass: method in unit test 2017-07-11 12:48:05 +04:00
Tigran Yesayan eb2ecbf9b3 Added unit test to check the new way of getting method components 2017-07-09 14:56:56 +04:00
Tigran Yesayan 16fab66f7b Fix crash in FLEXRuntimeUtility 2017-07-09 14:52:41 +04:00
Ryan Olson b3e70ac491 Merge pull request #189 from NSExceptional/refactor-toolbar
Make FLEXExplorerToolbar more extensible
2017-07-05 08:58:24 -07:00
Tanner Bennett d5177bb049 Make FLEXExplorerToolbar more extensible
Exposes `FLEXExplorerToolbar.toolbarItems` as a public API so that others can more easily, safely, and reliably add their own items to the FLEX toolbar.
2017-07-03 23:50:46 -05:00
Ryan Olson bb0faeb3cf Merge pull request #184 from revolter/hotfix/code-style
Fix code style
2017-07-03 18:10:17 -07:00
Ryan Olson 761feef3c0 Merge pull request #190 from showbie/request-error-in-red
Highlight request name in red if status code is a 4xx or 5xx
2017-07-03 17:47:00 -07:00
Colin Humber 352bae03ea Highlight request name in red if status code is a 4xx or 5xx 2017-06-29 12:58:01 -06:00
Ryan Olson b0085cae7d Merge pull request #187 from nicked/patch-1
Fixed keyboard shortcuts in Xcode 9 simulator
2017-06-19 17:41:44 -07:00
nicked b2f93f1752 Fixed keyboard shortcuts in Xcode 9 simulator
Pressing Cmd+S in the Simulator normally just takes a screenshot, but in Xcode 9, it also triggers the default "s" keyboard shortcut which enables the FLEX Select tool. This is because the keyboard flags is ANDed with a logical NOT of UIKeyModifierShift instead of the bitwise inverse
2017-06-14 18:03:26 +02:00
Ryan Olson 354510f2c4 Merge pull request #185 from showbie/host-blacklist
Add ability to filter out noisy network requests from being recorded
2017-05-31 22:23:26 -07:00
Colin Humber b8c6175193 Added configuration to filter out network requests for particular hosts from being recorded 2017-05-31 14:18:46 -06:00
Iulian Onofrei d409b110f5 Fix code style 2017-05-23 10:31:02 +03:00
Ryan Olson 5c73220158 Merge pull request #182 from NSExceptional/fix-heap
Fix #119
2017-05-21 22:33:26 -07:00
Tanner Bennett 397721e7ea Fix #119 2017-05-21 18:20:42 -05:00
Ryan Olson 5714275bcd Merge pull request #176 from NSIRLConnection/master
Fix #175
2017-05-21 15:27:56 -07:00
Ryan Olson 833c584e41 Merge pull request #180 from revolter/feature/database-password
Add support for SQLCipher protected databases
2017-05-21 15:22:31 -07:00
Ryan Olson 49b24487c5 Merge pull request #181 from revolter/feature/toolbar-position
Add toolbar position persistence
2017-05-21 15:17:49 -07:00
Iulian Onofrei 6d4eb01a07 Add toolbar position persistence 2017-05-10 17:33:42 +03:00
Iulian Onofrei a752203ff9 Add support for SQLCipher protected databases 2017-05-10 17:12:24 +03:00
Karl Peng c69427613d should keep the same completion logic 2017-03-24 17:29:08 +08:00
Michael 5d75a83568 Fix #175 by checking if the objects respond to compare: 2017-03-09 14:06:47 -05:00
Ryan Olson 7642a0632d Merge pull request #160 from defagos/carthage-instructions-image
Add image for Carthage FLEX exclusion setup
2017-01-15 12:07:48 -08:00
Ryan Olson 841054a713 Merge pull request #161 from unixzii/master
Add a convenient action button in FLEXImagePreviewViewController
2017-01-15 12:05:37 -08:00
杨弘宇 49f368fd63 replace Copy button with Action button in FLEXImagePreviewViewController's navigation item 2016-11-08 20:58:57 +08:00
Samuel Défago 1dc99250c8 Add image for Carthage FLEX exclusion setup 2016-11-07 09:09:19 +01:00
Ryan Olson 00edccf326 Merge pull request #159 from defagos/carthage-instructions
Add Carthage instructions for FLEX exclusion from release builds
2016-11-04 16:54:32 -07:00
Samuel Défago 7f3af90645 Add Carthage instructions for FLEX exclusion from release builds 2016-11-04 07:43:09 +01:00
Ryan Olson 1a030f06cd Merge pull request #157 from froody/size
[FLEX] Add size to heap objects view
2016-10-20 19:48:28 -07:00
Tom Birch 0477858bed [FLEX] Add size to heap objects view
Change-Id: I757714b2570d400886959caa72e731ef65a925b5
2016-10-19 19:55:19 -07:00
Ryan Olson 000e061d00 Merge pull request #152 from rtyu128/rtyu128-FLEXNetworkObserver-Fixed
Fix #150.
2016-10-10 08:16:59 -07:00
Anchor 224978b31b Fixed #150.
Use @selector(URLSession:dataTask:didBecomeDownloadTask:) instead of @selector(URLSession:dataTask:didBecomeDownloadTask:delegate:) in method search.
2016-10-08 10:58:56 +08:00
Ryan Olson 7fd133f13b Bump version in podspec 2016-10-01 15:46:38 -07:00
514 changed files with 44020 additions and 9225 deletions
+6 -2
View File
@@ -1,8 +1,12 @@
language: objective-c
xcode_workspace: FLEX.xcworkspace
xcode_sdk: iphonesimulator
before_install:
- gem install xcpretty
matrix:
include:
- xcode_scheme: UICatalog
xcode_sdk: iphonesimulator
- xcode_scheme: FLEX
xcode_sdk: iphonesimulator
script:
- set -o pipefail
- xcodebuild -workspace $TRAVIS_XCODE_WORKSPACE -scheme $TRAVIS_XCODE_SCHEME -sdk $TRAVIS_XCODE_SDK build | xcpretty
@@ -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
@@ -0,0 +1,120 @@
//
// FLEXTableViewController.h
// FLEX
//
// Created by Tanner on 7/5/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "FLEXTableView.h"
@class FLEXScopeCarousel, FLEXWindow;
typedef CGFloat FLEXDebounceInterval;
/// No delay, all events delivered
extern CGFloat const kFLEXDebounceInstant;
/// Small delay which makes UI seem smoother by avoiding rapid events
extern CGFloat const kFLEXDebounceFast;
/// Slower than Fast, faster than ExpensiveIO
extern CGFloat const kFLEXDebounceForAsyncSearch;
/// The least frequent, at just over once per second; for I/O or other expensive operations
extern CGFloat const kFLEXDebounceForExpensiveIO;
@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;
/// Defaults to NO.
///
/// Setting this to YES will initialize the carousel and the view.
@property (nonatomic) BOOL showsCarousel;
/// A horizontally scrolling list with functionality similar to
/// that of a search bar's scope bar. You'd want to use this when
/// you have potentially more than 4 scope options.
@property (nonatomic) FLEXScopeCarousel *carousel;
/// Defaults to NO.
///
/// Setting this to YES will initialize searchController and the view.
@property (nonatomic) BOOL showsSearchBar;
/// Defaults to NO.
///
/// Setting this to YES will make the search bar appear whenever the view appears.
/// Otherwise, iOS will only show the search bar when you scroll up.
@property (nonatomic) BOOL showSearchBarInitially;
/// nil unless showsSearchBar is set to YES.
///
/// self is used as the default search results updater and delegate.
/// 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.
/// Defaults to NO.
@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.
///
/// Does nothing pre-iOS 13, safe to call on any version.
@property (nonatomic) BOOL automaticallyShowsSearchBarCancelButton;
/// 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) 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.
- (void)updateSearchResults:(NSString *)newText;
/// Convenient for doing some async processor-intensive searching
/// 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
+132
View File
@@ -0,0 +1,132 @@
//
// FLEXTableViewSection.h
// FLEX
//
// Created by Tanner on 1/29/20.
// Copyright © 2020 Flipboard. All rights reserved.
//
#import "FLEXUtility.h"
@class FLEXTableView;
NS_ASSUME_NONNULL_BEGIN
#pragma mark FLEXTableViewSection
/// 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;
}
#pragma mark - Data
/// 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;
/// 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;
/// 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;
#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;
@end
NS_ASSUME_NONNULL_END
+127
View File
@@ -0,0 +1,127 @@
//
// FLEXTableViewSection.m
// FLEX
//
// 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
- (NSInteger)numberOfRows {
return 0;
}
- (void)reloadData { }
- (NSDictionary<NSString *,Class> *)cellRegistrationMapping {
return nil;
}
- (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
#pragma clang diagnostic pop
@@ -0,0 +1,15 @@
//
// FLEXCarouselCell.h
// FLEX
//
// Created by Tanner Bennett on 7/17/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface FLEXCarouselCell : UICollectionViewCell
@property (nonatomic, copy) NSString *title;
@end
@@ -0,0 +1,91 @@
//
// FLEXCarouselCell.m
// FLEX
//
// Created by Tanner Bennett on 7/17/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import "FLEXCarouselCell.h"
#import "FLEXColor.h"
#import "UIView+FLEX_Layout.h"
@interface FLEXCarouselCell ()
@property (nonatomic, readonly) UILabel *titleLabel;
@property (nonatomic, readonly) UIView *selectionIndicatorStripe;
@property (nonatomic) BOOL constraintsInstalled;
@end
@implementation FLEXCarouselCell
- (instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
_titleLabel = [UILabel new];
_selectionIndicatorStripe = [UIView new];
self.titleLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];
self.titleLabel.adjustsFontForContentSizeCategory = YES;
self.selectionIndicatorStripe.backgroundColor = self.tintColor;
[self.contentView addSubview:self.titleLabel];
[self.contentView addSubview:self.selectionIndicatorStripe];
[self installConstraints];
[self updateAppearance];
}
return self;
}
- (void)updateAppearance {
self.selectionIndicatorStripe.hidden = !self.selected;
if (self.selected) {
self.titleLabel.textColor = self.tintColor;
} else {
self.titleLabel.textColor = [FLEXColor deemphasizedTextColor];
}
}
#pragma mark Public
- (NSString *)title {
return self.titleLabel.text;
}
- (void)setTitle:(NSString *)title {
self.titleLabel.text = title;
[self.titleLabel sizeToFit];
[self setNeedsLayout];
}
#pragma mark Overrides
- (void)prepareForReuse {
[super prepareForReuse];
[self updateAppearance];
}
- (void)installConstraints {
CGFloat stripeHeight = 2;
self.titleLabel.translatesAutoresizingMaskIntoConstraints = NO;
self.selectionIndicatorStripe.translatesAutoresizingMaskIntoConstraints = NO;
UIView *superview = self.contentView;
[self.titleLabel pinEdgesToSuperviewWithInsets:UIEdgeInsetsMake(10, 15, 8 + stripeHeight, 15)];
[self.selectionIndicatorStripe.leadingAnchor constraintEqualToAnchor:superview.leadingAnchor].active = YES;
[self.selectionIndicatorStripe.bottomAnchor constraintEqualToAnchor:superview.bottomAnchor].active = YES;
[self.selectionIndicatorStripe.trailingAnchor constraintEqualToAnchor:superview.trailingAnchor].active = YES;
[self.selectionIndicatorStripe.heightAnchor constraintEqualToConstant:stripeHeight].active = YES;
}
- (void)setSelected:(BOOL)selected {
super.selected = selected;
[self updateAppearance];
}
@end
@@ -0,0 +1,20 @@
//
// FLEXScopeCarousel.h
// FLEX
//
// Created by Tanner Bennett on 7/17/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
/// Only use on iOS 10 and up. Requires iOS 10 APIs for calculating row sizes.
@interface FLEXScopeCarousel : UIControl
@property (nonatomic, copy) NSArray<NSString *> *items;
@property (nonatomic) NSInteger selectedIndex;
@property (nonatomic) void(^selectedIndexChangedAction)(NSInteger idx);
- (void)registerBlockForDynamicTypeChanges:(void(^)(FLEXScopeCarousel *))handler;
@end
@@ -0,0 +1,204 @@
//
// FLEXScopeCarousel.m
// FLEX
//
// Created by Tanner Bennett on 7/17/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import "FLEXScopeCarousel.h"
#import "FLEXCarouselCell.h"
#import "FLEXColor.h"
#import "UIView+FLEX_Layout.h"
const CGFloat kCarouselItemSpacing = 0;
NSString * const kCarouselCellReuseIdentifier = @"kCarouselCellReuseIdentifier";
@interface FLEXScopeCarousel () <UICollectionViewDelegate, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout>
@property (nonatomic, readonly) UICollectionView *collectionView;
@property (nonatomic, readonly) FLEXCarouselCell *sizingCell;
@property (nonatomic, readonly) id dynamicTypeObserver;
@property (nonatomic, readonly) NSMutableArray *dynamicTypeHandlers;
@property (nonatomic) BOOL constraintsInstalled;
@end
@implementation FLEXScopeCarousel
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = [FLEXColor primaryBackgroundColor];
self.autoresizingMask = UIViewAutoresizingFlexibleWidth;
self.translatesAutoresizingMaskIntoConstraints = YES;
_dynamicTypeHandlers = [NSMutableArray new];
CGSize itemSize = CGSizeZero;
if (@available(iOS 10.0, *)) {
itemSize = UICollectionViewFlowLayoutAutomaticSize;
}
// Collection view layout
UICollectionViewFlowLayout *layout = ({
UICollectionViewFlowLayout *layout = [UICollectionViewFlowLayout new];
layout.scrollDirection = UICollectionViewScrollDirectionHorizontal;
layout.sectionInset = UIEdgeInsetsZero;
layout.minimumLineSpacing = kCarouselItemSpacing;
layout.itemSize = itemSize;
layout.estimatedItemSize = itemSize;
layout;
});
// Collection view
_collectionView = ({
UICollectionView *cv = [[UICollectionView alloc]
initWithFrame:CGRectZero
collectionViewLayout:layout
];
cv.showsHorizontalScrollIndicator = NO;
cv.backgroundColor = UIColor.clearColor;
cv.delegate = self;
cv.dataSource = self;
[cv registerClass:[FLEXCarouselCell class] forCellWithReuseIdentifier:kCarouselCellReuseIdentifier];
[self addSubview:cv];
cv;
});
// Sizing cell
_sizingCell = [FLEXCarouselCell new];
self.sizingCell.title = @"NSObject";
// Dynamic type
__weak __typeof(self) weakSelf = self;
_dynamicTypeObserver = [NSNotificationCenter.defaultCenter
addObserverForName:UIContentSizeCategoryDidChangeNotification
object:nil queue:nil usingBlock:^(NSNotification *note) {
[self.collectionView setNeedsLayout];
[self setNeedsUpdateConstraints];
// Notify observers
__typeof(self) self = weakSelf;
for (void (^block)(FLEXScopeCarousel *) in self.dynamicTypeHandlers) {
block(self);
}
}
];
}
return self;
}
- (void)dealloc {
[NSNotificationCenter.defaultCenter removeObserver:self.dynamicTypeObserver];
}
#pragma mark - Overrides
- (void)drawRect:(CGRect)rect {
[super drawRect:rect];
CGFloat width = 1.f / UIScreen.mainScreen.scale;
// Draw hairline
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetStrokeColorWithColor(context, [FLEXColor hairlineColor].CGColor);
CGContextSetLineWidth(context, width);
CGContextMoveToPoint(context, 0, rect.size.height - width);
CGContextAddLineToPoint(context, rect.size.width, rect.size.height - width);
CGContextStrokePath(context);
}
+ (BOOL)requiresConstraintBasedLayout {
return YES;
}
- (void)updateConstraints {
if (!self.constraintsInstalled) {
self.collectionView.translatesAutoresizingMaskIntoConstraints = NO;
[self.collectionView pinEdgesToSuperview];
self.constraintsInstalled = YES;
}
[super updateConstraints];
}
- (CGSize)intrinsicContentSize {
return CGSizeMake(
UIViewNoIntrinsicMetric,
[self.sizingCell systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height
);
}
#pragma mark - Public
- (void)setItems:(NSArray<NSString *> *)items {
NSParameterAssert(items.count);
_items = items.copy;
// Refresh list, select first item initially
[self.collectionView reloadData];
self.selectedIndex = 0;
}
- (void)setSelectedIndex:(NSInteger)idx {
NSParameterAssert(idx < self.items.count);
_selectedIndex = idx;
NSIndexPath *path = [NSIndexPath indexPathForItem:idx inSection:0];
[self.collectionView selectItemAtIndexPath:path
animated:YES
scrollPosition:UICollectionViewScrollPositionCenteredHorizontally];
[self collectionView:self.collectionView didSelectItemAtIndexPath:path];
}
- (void)registerBlockForDynamicTypeChanges:(void (^)(FLEXScopeCarousel *))handler {
[self.dynamicTypeHandlers addObject:handler];
}
#pragma mark - UICollectionView
- (CGSize)collectionView:(UICollectionView *)collectionView layout:(UICollectionViewLayout *)collectionViewLayout sizeForItemAtIndexPath:(NSIndexPath *)indexPath {
// if (@available(iOS 10.0, *)) {
// return UICollectionViewFlowLayoutAutomaticSize;
// }
self.sizingCell.title = self.items[indexPath.item];
return [self.sizingCell systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
}
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
return self.items.count;
}
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView
cellForItemAtIndexPath:(NSIndexPath *)indexPath {
FLEXCarouselCell *cell = (id)[collectionView dequeueReusableCellWithReuseIdentifier:kCarouselCellReuseIdentifier
forIndexPath:indexPath];
cell.title = self.items[indexPath.row];
return cell;
}
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
_selectedIndex = indexPath.item; // In case self.selectedIndex didn't trigger this call
if (self.selectedIndexChangedAction) {
self.selectedIndexChangedAction(indexPath.row);
}
// TODO: dynamically choose a scroll position. Very wide items should
// get "Left" while smaller items should not scroll at all, unless
// they are only partially on the screen, in which case they
// should get "HorizontallyCentered" to bring them onto the screen.
// For now, everything goes to the left, as this has a similar effect.
[collectionView scrollToItemAtIndexPath:indexPath
atScrollPosition:UICollectionViewScrollPositionLeft
animated:YES];
[self sendActionsForControlEvents:UIControlEventValueChanged];
}
@end
@@ -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
@@ -0,0 +1,24 @@
//
// FLEXMultilineTableViewCell.h
// FLEX
//
// Created by Ryan Olson on 2/13/15.
// Copyright (c) 2015 f. All rights reserved.
//
#import "FLEXTableViewCell.h"
/// A cell with both labels set to be multi-line capable.
@interface FLEXMultilineTableViewCell : FLEXTableViewCell
+ (CGFloat)preferredHeightWithAttributedText:(NSAttributedString *)attributedText
maxWidth:(CGFloat)contentViewWidth
style:(UITableViewStyle)style
showsAccessory:(BOOL)showsAccessory;
@end
/// A \c FLEXMultilineTableViewCell initialized with \c UITableViewCellStyleSubtitle
@interface FLEXMultilineDetailTableViewCell : FLEXMultilineTableViewCell
@end
@@ -0,0 +1,67 @@
//
// FLEXMultilineTableViewCell.m
// FLEX
//
// Created by Ryan Olson on 2/13/15.
// Copyright (c) 2015 f. All rights reserved.
//
#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
- (void)postInit {
[super postInit];
self.titleLabel.numberOfLines = 0;
self.subtitleLabel.numberOfLines = 0;
}
+ (UIEdgeInsets)labelInsets {
return UIEdgeInsetsMake(10.0, 15.0, 10.0, 15.0);
}
+ (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) {
labelWidth -= 34.0;
}
UIEdgeInsets labelInsets = [self labelInsets];
labelWidth -= (labelInsets.left + labelInsets.right);
CGSize constrainSize = CGSizeMake(labelWidth, CGFLOAT_MAX);
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;
}
@end
@implementation FLEXMultilineDetailTableViewCell
- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
return [super initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:reuseIdentifier];
}
@end
@@ -0,0 +1,14 @@
//
// FLEXSubtitleTableViewCell.h
// FLEX
//
// Created by Tanner on 4/17/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import "FLEXTableViewCell.h"
/// A cell initialized with \c UITableViewCellStyleSubtitle
@interface FLEXSubtitleTableViewCell : FLEXTableViewCell
@end
@@ -0,0 +1,17 @@
//
// FLEXSubtitleTableViewCell.m
// FLEX
//
// Created by Tanner on 4/17/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import "FLEXSubtitleTableViewCell.h"
@implementation FLEXSubtitleTableViewCell
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
return [super initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:reuseIdentifier];
}
@end
@@ -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
@@ -0,0 +1,92 @@
//
// FLEXTableViewCell.m
// FLEX
//
// Created by Tanner on 4/17/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import "FLEXTableViewCell.h"
#import "FLEXUtility.h"
#import "FLEXColor.h"
#import "FLEXTableView.h"
@interface UITableView (Internal)
// Exists at least since iOS 5
- (BOOL)_canPerformAction:(SEL)action forCell:(UITableViewCell *)cell sender:(id)sender;
- (void)_performAction:(SEL)action forCell:(UITableViewCell *)cell sender:(id)sender;
@end
@interface UITableViewCell (Internal)
// Exists at least since iOS 5
@property (nonatomic, readonly) FLEXTableView *_tableView;
@end
@implementation FLEXTableViewCell
- (id)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier {
self = [super initWithStyle:style reuseIdentifier:reuseIdentifier];
if (self) {
[self postInit];
}
return self;
}
- (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];
}
/// We use this to allow our table view to allow its delegate
/// to handle any action it chooses to support, without
/// explicitly implementing the method ourselves.
///
/// Alternative considered: override respondsToSelector
/// to return NO. I decided against this for simplicity's
/// 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 {
// Must be unretained to avoid over-releasing
__unsafe_unretained id sender;
[invocation getArgument:&sender atIndex:2];
SEL action = invocation.selector;
// [self._tableView _performAction:action forCell:[self retain] sender:[sender retain]];
invocation.selector = @selector(_performAction:forCell:sender:);
[invocation setArgument:&action atIndex:2];
[invocation setArgument:(void *)&self atIndex:3];
[invocation setArgument:(void *)&sender atIndex:4];
[invocation invokeWithTarget:self._tableView];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
if ([self canPerformAction:selector withSender:nil]) {
return [self._tableView methodSignatureForSelector:@selector(_performAction:forCell:sender:)];
}
return [super methodSignatureForSelector:selector];
}
@end
+44
View File
@@ -0,0 +1,44 @@
//
// FLEXTableView.h
// FLEX
//
// Created by Tanner on 4/17/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
#pragma mark Reuse identifiers
typedef NSString * FLEXTableViewCellReuseIdentifier;
/// A regular \c FLEXTableViewCell initialized with \c UITableViewCellStyleDefault
extern FLEXTableViewCellReuseIdentifier const kFLEXDefaultCell;
/// A \c FLEXSubtitleTableViewCell initialized with \c UITableViewCellStyleSubtitle
extern FLEXTableViewCellReuseIdentifier const kFLEXDetailCell;
/// A \c FLEXMultilineTableViewCell initialized with \c UITableViewCellStyleDefault
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
+ (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
/// a custom cell for any of those reuse identifiers. By default, \c FLEXTableViewCell,
/// \c FLEXSubtitleTableViewCell, and \c FLEXMultilineTableViewCell are used, respectively.
///
/// @param registrationMapping A map of reuse identifiers to \c UITableViewCell (sub)class objects.
- (void)registerCells:(NSDictionary<NSString *, Class> *)registrationMapping;
@end
+115
View File
@@ -0,0 +1,115 @@
//
// FLEXTableView.m
// FLEX
//
// Created by Tanner on 4/17/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import "FLEXTableView.h"
#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
@interface UITableView (Private)
- (CGFloat)_heightForHeaderInSection:(NSInteger)section;
- (NSString *)_titleForHeaderInSection:(NSInteger)section;
@end
@implementation FLEXTableView
+ (instancetype)flexDefaultTableView {
#if FLEX_AT_LEAST_IOS13_SDK
if (@available(iOS 13.0, *)) {
return [[self alloc] initWithFrame:CGRectZero style:UITableViewStyleInsetGrouped];
} else {
return [[self alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];
}
#else
return [[self alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];
#endif
}
- (CGFloat)_heightForHeaderInSection:(NSInteger)section {
CGFloat height = [super _heightForHeaderInSection:section];
if (section == 0) {
NSString *title = [self _titleForHeaderInSection:section];
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;
}
}
}
return height;
}
#pragma mark - Initialization
+ (id)groupedTableView {
#if FLEX_AT_LEAST_IOS13_SDK
if (@available(iOS 13.0, *)) {
return [[self alloc] initWithFrame:CGRectZero style:UITableViewStyleInsetGrouped];
} else {
return [[self alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];
}
#else
return [[self alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];
#endif
}
+ (id)plainTableView {
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) {
[self registerCells:@{
kFLEXDefaultCell : [FLEXTableViewCell class],
kFLEXDetailCell : [FLEXSubtitleTableViewCell class],
kFLEXMultilineCell : [FLEXMultilineTableViewCell class],
kFLEXMultilineDetailCell : [FLEXMultilineDetailTableViewCell class],
kFLEXKeyValueCell : [FLEXKeyValueTableViewCell class],
kFLEXCodeFontCell : [FLEXCodeFontCell class],
}];
}
return self;
}
#pragma mark - Public
- (void)registerCells:(NSDictionary<NSString*, Class> *)registrationMapping {
[registrationMapping enumerateKeysAndObjectsUsingBlock:^(NSString *identifier, Class cellClass, BOOL *stop) {
[self registerClass:cellClass forCellReuseIdentifier:identifier];
}];
}
@end
@@ -14,8 +14,8 @@
@interface FLEXColorComponentInputView : UIView
@property (nonatomic, strong) UISlider *slider;
@property (nonatomic, strong) UILabel *valueLabel;
@property (nonatomic) UISlider *slider;
@property (nonatomic) UILabel *valueLabel;
@property (nonatomic, weak) id <FLEXColorComponentInputViewDelegate> delegate;
@@ -30,18 +30,16 @@
@implementation FLEXColorComponentInputView
- (id)initWithFrame:(CGRect)frame
{
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.slider = [[UISlider alloc] init];
self.slider.backgroundColor = self.backgroundColor;
self.slider = [UISlider new];
[self.slider addTarget:self action:@selector(sliderChanged:) forControlEvents:UIControlEventValueChanged];
[self addSubview:self.slider];
self.valueLabel = [[UILabel alloc] init];
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);
}
@@ -94,52 +87,48 @@
@interface FLEXColorPreviewBox : UIView
@property (nonatomic, strong) UIColor *color;
@property (nonatomic) UIColor *color;
@property (nonatomic, strong) UIView *colorOverlayView;
@property (nonatomic) UIView *colorOverlayView;
@end
@implementation FLEXColorPreviewBox
- (id)initWithFrame:(CGRect)frame
{
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.layer.borderWidth = 1.0;
self.layer.borderColor = [[UIColor blackColor] CGColor];
self.layer.borderColor = UIColor.blackColor.CGColor;
self.backgroundColor = [UIColor colorWithPatternImage:[[self class] backgroundPatternImage]];
self.colorOverlayView = [[UIView alloc] initWithFrame:self.bounds];
self.colorOverlayView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
self.colorOverlayView.backgroundColor = [UIColor clearColor];
self.colorOverlayView.backgroundColor = UIColor.clearColor;
[self addSubview:self.colorOverlayView];
}
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);
UIGraphicsBeginImageContextWithOptions(imageSize, YES, [[UIScreen mainScreen] scale]);
UIGraphicsBeginImageContextWithOptions(imageSize, YES, UIScreen.mainScreen.scale);
[[UIColor whiteColor] setFill];
[UIColor.whiteColor setFill];
UIRectFill(CGRectMake(0, 0, imageSize.width, imageSize.height));
[[UIColor grayColor] setFill];
[UIColor.grayColor setFill];
UIRectFill(CGRectMake(squareSize.width, 0, squareSize.width, squareSize.height));
UIRectFill(CGRectMake(0, squareSize.height, squareSize.width, squareSize.height));
@@ -153,55 +142,53 @@
@interface FLEXArgumentInputColorView () <FLEXColorComponentInputViewDelegate>
@property (nonatomic, strong) FLEXColorPreviewBox *colorPreviewBox;
@property (nonatomic, strong) UILabel *hexLabel;
@property (nonatomic, strong) FLEXColorComponentInputView *alphaInput;
@property (nonatomic, strong) FLEXColorComponentInputView *redInput;
@property (nonatomic, strong) FLEXColorComponentInputView *greenInput;
@property (nonatomic, strong) FLEXColorComponentInputView *blueInput;
@property (nonatomic) FLEXColorPreviewBox *colorPreviewBox;
@property (nonatomic) UILabel *hexLabel;
@property (nonatomic) FLEXColorComponentInputView *alphaInput;
@property (nonatomic) FLEXColorComponentInputView *redInput;
@property (nonatomic) FLEXColorComponentInputView *greenInput;
@property (nonatomic) FLEXColorComponentInputView *blueInput;
@end
@implementation FLEXArgumentInputColorView
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
{
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
self = [super initWithArgumentTypeEncoding:typeEncoding];
if (self) {
self.colorPreviewBox = [[FLEXColorPreviewBox alloc] init];
self.colorPreviewBox = [FLEXColorPreviewBox new];
[self addSubview:self.colorPreviewBox];
self.hexLabel = [[UILabel alloc] init];
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 alloc] init];
self.alphaInput.slider.minimumTrackTintColor = [UIColor blackColor];
self.alphaInput = [FLEXColorComponentInputView new];
self.alphaInput.slider.minimumTrackTintColor = UIColor.blackColor;
self.alphaInput.delegate = self;
[self addSubview:self.alphaInput];
self.redInput = [[FLEXColorComponentInputView alloc] init];
self.redInput.slider.minimumTrackTintColor = [UIColor redColor];
self.redInput = [FLEXColorComponentInputView new];
self.redInput.slider.minimumTrackTintColor = UIColor.redColor;
self.redInput.delegate = self;
[self addSubview:self.redInput];
self.greenInput = [[FLEXColorComponentInputView alloc] init];
self.greenInput.slider.minimumTrackTintColor = [UIColor greenColor];
self.greenInput = [FLEXColorComponentInputView new];
self.greenInput.slider.minimumTrackTintColor = UIColor.greenColor;
self.greenInput.delegate = self;
[self addSubview:self.greenInput];
self.blueInput = [[FLEXColorComponentInputView alloc] init];
self.blueInput.slider.minimumTrackTintColor = [UIColor blueColor];
self.blueInput = [FLEXColorComponentInputView new];
self.blueInput.slider.minimumTrackTintColor = UIColor.blueColor;
self.blueInput.delegate = self;
[self addSubview:self.blueInput];
}
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;
@@ -221,14 +207,14 @@
[self.hexLabel sizeToFit];
const CGFloat kLabelVerticalOutsetAmount = 0.0;
const CGFloat kLabelHorizonalOutsetAmount = 2.0;
UIEdgeInsets labelOutset = UIEdgeInsetsMake(-kLabelVerticalOutsetAmount, -kLabelHorizonalOutsetAmount, -kLabelVerticalOutsetAmount, -kLabelHorizonalOutsetAmount);
const CGFloat kLabelHorizontalOutsetAmount = 2.0;
UIEdgeInsets labelOutset = UIEdgeInsetsMake(-kLabelVerticalOutsetAmount, -kLabelHorizontalOutsetAmount, -kLabelVerticalOutsetAmount, -kLabelHorizontalOutsetAmount);
self.hexLabel.frame = UIEdgeInsetsInsetRect(self.hexLabel.frame, labelOutset);
CGFloat hexLabelOriginX = self.colorPreviewBox.layer.borderWidth;
CGFloat hexLabelOriginY = CGRectGetMaxY(self.colorPreviewBox.frame) - self.colorPreviewBox.layer.borderWidth - self.hexLabel.frame.size.height;
self.hexLabel.frame = CGRectMake(hexLabelOriginX, hexLabelOriginY, self.hexLabel.frame.size.width, self.hexLabel.frame.size.height);
NSArray *colorComponentInputViews = @[self.alphaInput, self.redInput, self.greenInput, self.blueInput];
NSArray<FLEXColorComponentInputView *> *colorComponentInputViews = @[self.alphaInput, self.redInput, self.greenInput, self.blueInput];
for (FLEXColorComponentInputView *inputView in colorComponentInputViews) {
CGSize fitSize = [inputView sizeThatFits:constrainSize];
inputView.frame = CGRectMake(0, runningOriginY, fitSize.width, fitSize.height);
@@ -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
@@ -11,17 +11,16 @@
@interface FLEXArgumentInputDateView ()
@property (nonatomic, strong) UIDatePicker *datePicker;
@property (nonatomic) UIDatePicker *datePicker;
@end
@implementation FLEXArgumentInputDateView
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
{
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
self = [super initWithArgumentTypeEncoding:typeEncoding];
if (self) {
self.datePicker = [[UIDatePicker alloc] init];
self.datePicker = [UIDatePicker new];
self.datePicker.datePickerMode = UIDatePickerModeDateAndTime;
// Using UTC, because that's what the NSDate description prints
self.datePicker.calendar = [NSCalendar calendarWithIdentifier:NSCalendarIdentifierGregorian];
@@ -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
@@ -13,25 +13,22 @@
@interface FLEXArgumentInputFontView ()
@property (nonatomic, strong) FLEXArgumentInputView *fontNameInput;
@property (nonatomic, strong) FLEXArgumentInputView *pointSizeInput;
@property (nonatomic) FLEXArgumentInputView *fontNameInput;
@property (nonatomic) FLEXArgumentInputView *pointSizeInput;
@end
@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
@@ -1,6 +1,6 @@
//
// FLEXArgumentInputFontsPickerView.h
// UICatalog
// FLEX
//
// Created by 啟倫 陳 on 2014/7/27.
// Copyright (c) 2014年 f. All rights reserved.
@@ -1,6 +1,6 @@
//
// FLEXArgumentInputFontsPickerView.m
// UICatalog
// FLEX
//
// Created by on 2014/7/27.
// Copyright (c) 2014 f. All rights reserved.
@@ -11,15 +11,14 @@
@interface FLEXArgumentInputFontsPickerView ()
@property (nonatomic, strong) NSMutableArray *availableFonts;
@property (nonatomic) NSMutableArray<NSString *> *availableFonts;
@end
@implementation FLEXArgumentInputFontsPickerView
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
{
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
self = [super initWithArgumentTypeEncoding:typeEncoding];
if (self) {
self.targetSize = FLEXArgumentInputViewSizeSmall;
@@ -29,24 +28,21 @@
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];
}
[(UIPickerView*)self.inputTextView.inputView selectRow:[self.availableFonts indexOfObject:inputValue] inComponent:0 animated:NO];
[(UIPickerView *)self.inputTextView.inputView selectRow:[self.availableFonts indexOfObject:inputValue] inComponent:0 animated:NO];
}
- (id)inputValue
{
return [self.inputTextView.text length] > 0 ? [self.inputTextView.text copy] : nil;
- (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,9 +50,8 @@
return fontsPicker;
}
- (void)createAvailableFonts
{
NSMutableArray *unsortedFontsArray = [NSMutableArray array];
- (void)createAvailableFonts {
NSMutableArray<NSString *> *unsortedFontsArray = [NSMutableArray array];
for (NSString *eachFontFamily in [UIFont familyNames]) {
for (NSString *eachFontName in [UIFont fontNamesForFamilyName:eachFontFamily]) {
[unsortedFontsArray addObject:eachFontName];
@@ -67,38 +62,34 @@
#pragma mark - UIPickerViewDataSource
- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView
{
- (NSInteger)numberOfComponentsInPickerView:(UIPickerView *)pickerView {
return 1;
}
- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component
{
return [self.availableFonts count];
- (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];
fontLabel.backgroundColor = [UIColor clearColor];
fontLabel.backgroundColor = UIColor.clearColor;
fontLabel.textAlignment = NSTextAlignmentCenter;
} else {
fontLabel = (UILabel*)view;
}
UIFont *font = [UIFont fontWithName:self.availableFonts[row] size:15.0];
NSDictionary *attributesDictionary = [NSDictionary dictionaryWithObject:font forKey:NSFontAttributeName];
NSDictionary<NSString *, id> *attributesDictionary = [NSDictionary<NSString *, id> dictionaryWithObject:font forKey:NSFontAttributeName];
NSAttributedString *attributesString = [[NSAttributedString alloc] initWithString:self.availableFonts[row] attributes:attributesDictionary];
fontLabel.attributedText = attributesString;
[fontLabel sizeToFit];
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];
}
@@ -1,65 +0,0 @@
//
// FLEXArgumentInputJSONObjectView.m
// Flipboard
//
// Created by Ryan Olson on 6/15/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import "FLEXArgumentInputJSONObjectView.h"
#import "FLEXRuntimeUtility.h"
@implementation FLEXArgumentInputJSONObjectView
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
{
self = [super initWithArgumentTypeEncoding:typeEncoding];
if (self) {
// Start with the numbers and punctuation keyboard since quotes, curly braces, or
// square brackets are likely to be the first characters type for the JSON.
self.inputTextView.keyboardType = UIKeyboardTypeNumbersAndPunctuation;
self.targetSize = FLEXArgumentInputViewSizeLarge;
}
return self;
}
- (void)setInputValue:(id)inputValue
{
self.inputTextView.text = [FLEXRuntimeUtility editableJSONStringForObject:inputValue];
}
- (id)inputValue
{
return [FLEXRuntimeUtility objectValueFromEditableJSONString:self.inputTextView.text];
}
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
{
// Must be object type.
BOOL supported = type && type[0] == '@';
if (supported) {
if (value) {
// If there's a current value, it must be serializable to JSON
supported = [FLEXRuntimeUtility editableJSONStringForObject:value] != nil;
} else {
// Otherwise, see if we have more type information than just 'id'.
// If we do, make sure the encoding is something serializable to JSON.
// Properties and ivars keep more detailed type encoding information than method arguments.
if (strcmp(type, @encode(id)) != 0) {
BOOL isJSONSerializableType = NO;
// Note: we can't use @encode(NSString) here because that drops the string information and just goes to @encode(id).
isJSONSerializableType = isJSONSerializableType || strcmp(type, FLEXEncodeClass(NSString)) == 0;
isJSONSerializableType = isJSONSerializableType || strcmp(type, FLEXEncodeClass(NSNumber)) == 0;
isJSONSerializableType = isJSONSerializableType || strcmp(type, FLEXEncodeClass(NSArray)) == 0;
isJSONSerializableType = isJSONSerializableType || strcmp(type, FLEXEncodeClass(NSDictionary)) == 0;
supported = isJSONSerializableType;
}
}
}
return supported;
}
@end
@@ -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,21 +20,20 @@
return self;
}
- (void)setInputValue:(id)inputValue
{
- (void)setInputValue:(id)inputValue {
if ([inputValue respondsToSelector:@selector(stringValue)]) {
self.inputTextView.text = [inputValue stringValue];
}
}
- (id)inputValue
{
return [FLEXRuntimeUtility valueForNumberWithObjCType:[self.typeEncoding UTF8String] fromInputString:self.inputTextView.text];
- (id)inputValue {
return [FLEXRuntimeUtility valueForNumberWithObjCType:self.typeEncoding.UTF8String fromInputString:self.inputTextView.text];
}
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
{
static NSArray *primitiveTypes = nil;
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value {
NSParameterAssert(type);
static NSArray<NSString *> *primitiveTypes = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
primitiveTypes = @[@(@encode(char)),
@@ -49,7 +47,8 @@
@(@encode(unsigned long)),
@(@encode(unsigned long long)),
@(@encode(float)),
@(@encode(double))];
@(@encode(double)),
@(@encode(long double))];
});
return type && [primitiveTypes containsObject:@(type)];
}
@@ -1,5 +1,5 @@
//
// FLEXArgumentInputJSONObjectView.h
// FLEXArgumentInputObjectView.h
// Flipboard
//
// Created by Ryan Olson on 6/15/14.
@@ -8,6 +8,6 @@
#import "FLEXArgumentInputTextView.h"
@interface FLEXArgumentInputJSONObjectView : FLEXArgumentInputTextView
@interface FLEXArgumentInputObjectView : FLEXArgumentInputTextView
@end
@@ -0,0 +1,232 @@
//
// FLEXArgumentInputJSONObjectView.m
// Flipboard
//
// Created by Ryan Olson on 6/15/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import "FLEXArgumentInputObjectView.h"
#import "FLEXRuntimeUtility.h"
static const CGFloat kSegmentInputMargin = 10;
typedef NS_ENUM(NSUInteger, FLEXArgInputObjectType) {
FLEXArgInputObjectTypeJSON,
FLEXArgInputObjectTypeAddress
};
@interface FLEXArgumentInputObjectView ()
@property (nonatomic) UISegmentedControl *objectTypeSegmentControl;
@property (nonatomic) FLEXArgInputObjectType inputType;
@end
@implementation FLEXArgumentInputObjectView
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
self = [super initWithArgumentTypeEncoding:typeEncoding];
if (self) {
// Start with the numbers and punctuation keyboard since quotes, curly braces, or
// square brackets are likely to be the first characters type for the JSON.
self.inputTextView.keyboardType = UIKeyboardTypeNumbersAndPunctuation;
self.targetSize = FLEXArgumentInputViewSizeLarge;
self.objectTypeSegmentControl = [[UISegmentedControl alloc] initWithItems:@[@"Value", @"Address"]];
[self.objectTypeSegmentControl addTarget:self action:@selector(didChangeType) forControlEvents:UIControlEventValueChanged];
self.objectTypeSegmentControl.selectedSegmentIndex = 0;
[self addSubview:self.objectTypeSegmentControl];
self.inputType = [[self class] preferredDefaultTypeForObjCType:typeEncoding withCurrentValue:nil];
self.objectTypeSegmentControl.selectedSegmentIndex = self.inputType;
}
return self;
}
- (void)didChangeType {
self.inputType = self.objectTypeSegmentControl.selectedSegmentIndex;
if (super.inputValue) {
// Trigger an update to the text field to show
// the address of the stored object we were given,
// or to show a JSON representation of the object
[self populateTextAreaFromValue:super.inputValue];
} else {
// Clear the text field
[self populateTextAreaFromValue:nil];
}
}
- (void)setInputType:(FLEXArgInputObjectType)inputType {
if (_inputType == inputType) return;
_inputType = inputType;
// Resize input view
switch (inputType) {
case FLEXArgInputObjectTypeJSON:
self.targetSize = FLEXArgumentInputViewSizeLarge;
break;
case FLEXArgInputObjectTypeAddress:
self.targetSize = FLEXArgumentInputViewSizeSmall;
break;
}
// Change placeholder
switch (inputType) {
case FLEXArgInputObjectTypeJSON:
self.inputPlaceholderText =
@"You can put any valid JSON here, such as a string, number, array, or dictionary:"
"\n\"This is a string\""
"\n1234"
"\n{ \"name\": \"Bob\", \"age\": 47 }"
"\n["
"\n 1, 2, 3"
"\n]";
break;
case FLEXArgInputObjectTypeAddress:
self.inputPlaceholderText = @"0x0000deadb33f";
break;
}
[self setNeedsLayout];
[self.superview setNeedsLayout];
}
- (void)setInputValue:(id)inputValue {
super.inputValue = inputValue;
[self populateTextAreaFromValue:inputValue];
}
- (id)inputValue {
switch (self.inputType) {
case FLEXArgInputObjectTypeJSON:
return [FLEXRuntimeUtility objectValueFromEditableJSONString:self.inputTextView.text];
case FLEXArgInputObjectTypeAddress: {
NSScanner *scanner = [NSScanner scannerWithString:self.inputTextView.text];
unsigned long long objectPointerValue;
if ([scanner scanHexLongLong:&objectPointerValue]) {
return (__bridge id)(void *)objectPointerValue;
}
return nil;
}
}
}
- (void)populateTextAreaFromValue:(id)value {
if (!value) {
self.inputTextView.text = nil;
} else {
if (self.inputType == FLEXArgInputObjectTypeJSON) {
self.inputTextView.text = [FLEXRuntimeUtility editableJSONStringForObject:value];
} else if (self.inputType == FLEXArgInputObjectTypeAddress) {
self.inputTextView.text = [NSString stringWithFormat:@"%p", value];
}
}
// Delegate methods are not called for programmatic changes
[self textViewDidChange:self.inputTextView];
}
- (CGSize)sizeThatFits:(CGSize)size {
CGSize fitSize = [super sizeThatFits:size];
fitSize.height += [self.objectTypeSegmentControl sizeThatFits:size].height + kSegmentInputMargin;
return fitSize;
}
- (void)layoutSubviews {
CGFloat segmentHeight = [self.objectTypeSegmentControl sizeThatFits:self.frame.size].height;
self.objectTypeSegmentControl.frame = CGRectMake(
0.0,
// Our segmented control is taking the position
// of the text view, as far as super is concerned,
// and we override this property to be different
super.topInputFieldVerticalLayoutGuide,
self.frame.size.width,
segmentHeight
);
[super layoutSubviews];
}
- (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 {
NSParameterAssert(type);
// Must be object type
return type[0] == FLEXTypeEncodingObjcObject || type[0] == FLEXTypeEncodingObjcClass;
}
+ (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
// to display the JSON editor. Otherwise display the address field.
if ([FLEXRuntimeUtility editableJSONStringForObject:value]) {
return FLEXArgInputObjectTypeJSON;
} else {
return FLEXArgInputObjectTypeAddress;
}
} else {
// Otherwise, see if we have more type information than just 'id'.
// If we do, make sure the encoding is something serializable to JSON.
// Properties and ivars keep more detailed type encoding information than method arguments.
if (strcmp(type, @encode(id)) != 0) {
BOOL isJSONSerializableType = NO;
// Parse class name out of the string,
// which is in the form `@"ClassName"`
Class cls = NSClassFromString(({
NSString *className = nil;
NSScanner *scan = [NSScanner scannerWithString:@(type)];
NSCharacterSet *allowed = [NSCharacterSet
characterSetWithCharactersInString:@"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_$"
];
// Skip over the @" then scan the name
if ([scan scanString:@"@\"" intoString:nil]) {
[scan scanCharactersFromSet:allowed intoString:&className];
}
className;
}));
// Note: we can't use @encode(NSString) here because that drops
// the class information and just goes to @encode(id).
NSArray<Class> *jsonTypes = @[
[NSString class],
[NSNumber class],
[NSArray class],
[NSDictionary class],
];
// Look for matching types
for (Class jsonClass in jsonTypes) {
if ([cls isSubclassOfClass:jsonClass]) {
isJSONSerializableType = YES;
break;
}
}
if (isJSONSerializableType) {
return FLEXArgInputObjectTypeJSON;
} else {
return FLEXArgInputObjectTypeAddress;
}
} else {
return FLEXArgInputObjectTypeAddress;
}
}
}
@end
@@ -11,35 +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
{
// Interpret empty string as nil. We loose the ablitiy to set empty string as a string value,
- (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
@@ -12,28 +12,32 @@
@interface FLEXArgumentInputStructView ()
@property (nonatomic, strong) NSArray *argumentInputViews;
@property (nonatomic) NSArray<FLEXArgumentInputView *> *argumentInputViews;
@end
@implementation FLEXArgumentInputStructView
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
{
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
self = [super initWithArgumentTypeEncoding:typeEncoding];
if (self) {
NSMutableArray *inputViews = [NSMutableArray array];
NSArray *customTitles = [[self class] customFieldTitlesForTypeEncoding:typeEncoding];
[FLEXRuntimeUtility enumerateTypesInStructEncoding:typeEncoding usingBlock:^(NSString *structName, const char *fieldTypeEncoding, NSString *prettyTypeEncoding, NSUInteger fieldIndex, NSUInteger fieldOffset) {
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) {
FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:fieldTypeEncoding];
inputView.backgroundColor = self.backgroundColor;
inputView.targetSize = FLEXArgumentInputViewSizeSmall;
if (fieldIndex < [customTitles count]) {
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,19 +51,17 @@
#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) {
if (strcmp(self.typeEncoding.UTF8String, structTypeEncoding) == 0) {
NSUInteger valueSize = 0;
@try {
// NSGetSizeAndAlignment barfs on type encoding for bitfields.
@@ -69,12 +71,16 @@
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];
if (fieldTypeEncoding[0] == @encode(id)[0] || fieldTypeEncoding[0] == @encode(Class)[0]) {
if (fieldTypeEncoding[0] == FLEXTypeEncodingObjcObject || fieldTypeEncoding[0] == FLEXTypeEncodingObjcClass) {
inputView.inputValue = (__bridge id)fieldPointer;
} else {
NSValue *boxedField = [FLEXRuntimeUtility valueForPrimitivePointer:fieldPointer objCType:fieldTypeEncoding];
@@ -87,10 +93,9 @@
}
}
- (id)inputValue
{
- (id)inputValue {
NSValue *boxedStruct = nil;
const char *structTypeEncoding = [self.typeEncoding UTF8String];
const char *structTypeEncoding = self.typeEncoding.UTF8String;
NSUInteger structSize = 0;
@try {
// NSGetSizeAndAlignment barfs on type encoding for bitfields.
@@ -99,12 +104,16 @@
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];
if (fieldTypeEncoding[0] == @encode(id)[0] || fieldTypeEncoding[0] == @encode(Class)[0]) {
if (fieldTypeEncoding[0] == FLEXTypeEncodingObjcObject || fieldTypeEncoding[0] == FLEXTypeEncodingObjcClass) {
// Object fields
memcpy(fieldPointer, (__bridge void *)inputView.inputValue, sizeof(id));
} else {
@@ -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,20 +179,33 @@
#pragma mark - Class Helpers
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
{
return type && type[0] == '{';
+ (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 *)customFieldTitlesForTypeEncoding:(const char *)typeEncoding
{
NSArray *customTitles = nil;
+ (NSArray<NSString *> *)customFieldTitlesForTypeEncoding:(const char *)typeEncoding {
NSArray<NSString *> *customTitles = nil;
if (strcmp(typeEncoding, @encode(CGRect)) == 0) {
customTitles = @[@"CGPoint origin", @"CGSize size"];
} else if (strcmp(typeEncoding, @encode(CGPoint)) == 0) {
customTitles = @[@"CGFloat x", @"CGFloat y"];
} else if (strcmp(typeEncoding, @encode(CGSize)) == 0) {
customTitles = @[@"CGFloat width", @"CGFloat height"];
} else if (strcmp(typeEncoding, @encode(CGVector)) == 0) {
customTitles = @[@"CGFloat dx", @"CGFloat dy"];
} else if (strcmp(typeEncoding, @encode(UIEdgeInsets)) == 0) {
customTitles = @[@"CGFloat top", @"CGFloat left", @"CGFloat bottom", @"CGFloat right"];
} else if (strcmp(typeEncoding, @encode(UIOffset)) == 0) {
@@ -203,6 +221,13 @@
customTitles = @[@"CGFloat a", @"CGFloat b",
@"CGFloat c", @"CGFloat d",
@"CGFloat tx", @"CGFloat ty"];
} else {
if (@available(iOS 11.0, *)) {
if (strcmp(typeEncoding, @encode(NSDirectionalEdgeInsets)) == 0) {
customTitles = @[@"CGFloat top", @"CGFloat leading",
@"CGFloat bottom", @"CGFloat trailing"];
}
}
}
return customTitles;
}
@@ -10,17 +10,16 @@
@interface FLEXArgumentInputSwitchView ()
@property (nonatomic, strong) UISwitch *inputSwitch;
@property (nonatomic) UISwitch *inputSwitch;
@end
@implementation FLEXArgumentInputSwitchView
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
{
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
self = [super initWithArgumentTypeEncoding:typeEncoding];
if (self) {
self.inputSwitch = [[UISwitch alloc] init];
self.inputSwitch = [UISwitch new];
[self.inputSwitch addTarget:self action:@selector(switchValueDidChange:) forControlEvents:UIControlEventValueChanged];
[self.inputSwitch sizeToFit];
[self addSubview:self.inputSwitch];
@@ -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
@@ -8,10 +8,11 @@
#import "FLEXArgumentInputView.h"
@interface FLEXArgumentInputTextView : FLEXArgumentInputView
@interface FLEXArgumentInputTextView : FLEXArgumentInputView <UITextViewDelegate>
// For subclass eyes only
@property (nonatomic, strong, readonly) UITextView *inputTextView;
@property (nonatomic, readonly) UITextView *inputTextView;
@property (nonatomic) NSString *inputPlaceholderText;
@end
@@ -6,105 +6,131 @@
//
//
#import "FLEXColor.h"
#import "FLEXArgumentInputTextView.h"
#import "FLEXUtility.h"
@interface FLEXArgumentInputTextView () <UITextViewDelegate>
@interface FLEXArgumentInputTextView ()
@property (nonatomic, strong) UITextView *inputTextView;
@property (nonatomic) UITextView *inputTextView;
@property (nonatomic) UILabel *placeholderLabel;
@property (nonatomic, readonly) NSUInteger numberOfInputLines;
@end
@implementation FLEXArgumentInputTextView
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
{
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
self = [super initWithArgumentTypeEncoding:typeEncoding];
if (self) {
self.inputTextView = [[UITextView alloc] init];
self.inputTextView = [UITextView new];
self.inputTextView.font = [[self class] inputFont];
self.inputTextView.backgroundColor = [UIColor whiteColor];
self.inputTextView.layer.borderColor = [[UIColor blackColor] CGColor];
self.inputTextView.layer.borderWidth = 1.0;
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];
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];
}
return self;
}
#pragma mark - private
#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 {
self.placeholderLabel.text = placeholder;
if (placeholder.length) {
if (!self.inputTextView.text.length) {
self.placeholderLabel.hidden = NO;
} else {
self.placeholderLabel.hidden = YES;
}
} else {
self.placeholderLabel.hidden = YES;
}
[self setNeedsLayout];
}
#pragma mark - Text View Changes
- (void)textViewDidChange:(UITextView *)textView
{
[self.delegate argumentInputViewValueDidChange:self];
- (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 then origin
// by the content inset then the text container inset
CGSize s = self.inputTextView.frame.size;
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 = 0;
- (NSUInteger)numberOfInputLines {
switch (self.targetSize) {
case FLEXArgumentInputViewSizeDefault:
numberOfInputLines = 2;
break;
return 2;
case FLEXArgumentInputViewSizeSmall:
numberOfInputLines = 1;
break;
return 1;
case FLEXArgumentInputViewSizeLarge:
numberOfInputLines = 8;
break;
return 8;
}
return numberOfInputLines;
}
- (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;
@@ -113,9 +139,16 @@
#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 {
[self.delegate argumentInputViewValueDidChange:self];
self.placeholderLabel.hidden = !(self.inputPlaceholderText.length && !textView.text.length);
}
@end
@@ -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
};
@@ -26,12 +29,13 @@ typedef NS_ENUM(NSUInteger, FLEXArgumentInputViewSize) {
/// To populate the filed with an initial value, set this property.
/// To reteive the value input by the user, access the property.
/// Primitive types and structs should/will be boxed in NSValue containers.
/// Concrete subclasses *must* override both the setter and getter for this property.
/// Concrete subclasses should override both the setter and getter for this property.
/// Subclasses can call super.inputValue to access a backing store for the value.
@property (nonatomic) id inputValue;
/// Setting this value to large will make some argument input views increase the size of their input field(s).
/// Useful to increase the use of space if there is only one input view on screen (i.e. for property and ivar editing).
@property (nonatomic, assign) FLEXArgumentInputViewSize targetSize;
@property (nonatomic) FLEXArgumentInputViewSize targetSize;
/// Users of the input view can get delegate callbacks for incremental changes in user input.
@property (nonatomic, weak) id <FLEXArgumentInputViewDelegate> delegate;
@@ -47,8 +51,8 @@ typedef NS_ENUM(NSUInteger, FLEXArgumentInputViewSize) {
// For subclass eyes only
@property (nonatomic, strong, readonly) UILabel *titleLabel;
@property (nonatomic, strong, readonly) NSString *typeEncoding;
@property (nonatomic, readonly) UILabel *titleLabel;
@property (nonatomic, readonly) NSString *typeEncoding;
@property (nonatomic, readonly) CGFloat topInputFieldVerticalLayoutGuide;
@end
@@ -8,27 +8,26 @@
#import "FLEXArgumentInputView.h"
#import "FLEXUtility.h"
#import "FLEXColor.h"
@interface FLEXArgumentInputView ()
@property (nonatomic, strong) UILabel *titleLabel;
@property (nonatomic, strong) NSString *typeEncoding;
@property (nonatomic) UILabel *titleLabel;
@property (nonatomic) NSString *typeEncoding;
@end
@implementation FLEXArgumentInputView
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding
{
- (instancetype)initWithArgumentTypeEncoding:(const char *)typeEncoding {
self = [super initWithFrame:CGRectZero];
if (self) {
self.typeEncoding = @(typeEncoding);
self.typeEncoding = typeEncoding != NULL ? @(typeEncoding) : nil;
}
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 alloc] init];
_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
{
return [self.title length] > 0;
- (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,48 +77,32 @@
#pragma mark - Subclasses Can Override
- (BOOL)inputViewIsFirstResponder
{
- (BOOL)inputViewIsFirstResponder {
return NO;
}
- (void)setInputValue:(id)inputValue
{
// Subclasses should override.
}
- (id)inputValue
{
// Subclasses should override.
return nil;
}
+ (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) {
if (self.title.length > 0) {
CGSize constrainSize = CGSizeMake(size.width, CGFLOAT_MAX);
height += ceil([self.titleLabel sizeThatFits:constrainSize].height);
height += [[self class] titleBottomPadding];
@@ -7,8 +7,7 @@
//
#import <Foundation/Foundation.h>
@class FLEXArgumentInputView;
#import "FLEXArgumentInputSwitchView.h"
@interface FLEXArgumentInputViewFactory : NSObject
@@ -8,7 +8,7 @@
#import "FLEXArgumentInputViewFactory.h"
#import "FLEXArgumentInputView.h"
#import "FLEXArgumentInputJSONObjectView.h"
#import "FLEXArgumentInputObjectView.h"
#import "FLEXArgumentInputNumberView.h"
#import "FLEXArgumentInputSwitchView.h"
#import "FLEXArgumentInputStructView.h"
@@ -17,55 +17,53 @@
#import "FLEXArgumentInputFontView.h"
#import "FLEXArgumentInputColorView.h"
#import "FLEXArgumentInputDateView.h"
#import "FLEXRuntimeUtility.h"
@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.
// The unsupported view shows "nil" and does not allow user input.
subclass = [FLEXArgumentInputNotSupportedView class];
}
return [[subclass alloc] initWithArgumentTypeEncoding:typeEncoding];
// Remove the field name if there is any (e.g. \"width\"d -> d)
const NSUInteger fieldNameOffset = [FLEXRuntimeUtility fieldNameOffsetForTypeEncoding:typeEncoding];
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;
NSArray<Class> *inputViewClasses = @[[FLEXArgumentInputColorView class],
[FLEXArgumentInputFontView class],
[FLEXArgumentInputStringView class],
[FLEXArgumentInputStructView class],
[FLEXArgumentInputSwitchView class],
[FLEXArgumentInputDateView class],
[FLEXArgumentInputNumberView class],
[FLEXArgumentInputObjectView class]];
// Note that order is important here since multiple subclasses may support the same type.
// An example is the number subclass and the bool subclass for the type @encode(BOOL).
// Both work, but we'd prefer to use the bool subclass.
if ([FLEXArgumentInputColorView supportsObjCType:typeEncoding withCurrentValue:currentValue]) {
argumentInputViewSubclass = [FLEXArgumentInputColorView class];
} else if ([FLEXArgumentInputFontView supportsObjCType:typeEncoding withCurrentValue:currentValue]) {
argumentInputViewSubclass = [FLEXArgumentInputFontView class];
} else if ([FLEXArgumentInputStringView supportsObjCType:typeEncoding withCurrentValue:currentValue]) {
argumentInputViewSubclass = [FLEXArgumentInputStringView class];
} else if ([FLEXArgumentInputStructView supportsObjCType:typeEncoding withCurrentValue:currentValue]) {
argumentInputViewSubclass = [FLEXArgumentInputStructView class];
} else if ([FLEXArgumentInputSwitchView supportsObjCType:typeEncoding withCurrentValue:currentValue]) {
argumentInputViewSubclass = [FLEXArgumentInputSwitchView class];
} else if ([FLEXArgumentInputDateView supportsObjCType:typeEncoding withCurrentValue:currentValue]) {
argumentInputViewSubclass = [FLEXArgumentInputDateView class];
} else if ([FLEXArgumentInputNumberView supportsObjCType:typeEncoding withCurrentValue:currentValue]) {
argumentInputViewSubclass = [FLEXArgumentInputNumberView class];
} else if ([FLEXArgumentInputJSONObjectView supportsObjCType:typeEncoding withCurrentValue:currentValue]) {
argumentInputViewSubclass = [FLEXArgumentInputJSONObjectView class];
for (Class inputViewClass in inputViewClasses) {
if ([inputViewClass supportsObjCType:typeEncoding + fieldNameOffset withCurrentValue:currentValue]) {
argumentInputViewSubclass = inputViewClass;
break;
}
}
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;
}
@@ -15,14 +15,13 @@
@interface FLEXDefaultEditorViewController ()
@property (nonatomic, readonly) NSUserDefaults *defaults;
@property (nonatomic, strong) NSString *key;
@property (nonatomic) NSString *key;
@end
@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,26 +30,26 @@
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;
id currentValue = [self.defaults objectForKey:self.key];
FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory argumentInputViewForTypeEncoding:@encode(id) currentValue:currentValue];
FLEXArgumentInputView *inputView = [FLEXArgumentInputViewFactory
argumentInputViewForTypeEncoding:FLEXEncodeObject(currentValue)
currentValue:currentValue
];
inputView.backgroundColor = self.view.backgroundColor;
inputView.inputValue = currentValue;
self.fieldEditorView.argumentInputViews = @[inputView];
}
- (void)actionButtonPressed:(id)sender
{
- (void)actionButtonPressed:(id)sender {
[super actionButtonPressed:sender];
id value = self.firstInputView.inputValue;
@@ -64,9 +63,17 @@
self.firstInputView.inputValue = [self.defaults objectForKey:self.key];
}
+ (BOOL)canEditDefaultWithValue:(id)currentValue
{
return [FLEXArgumentInputViewFactory canEditFieldWithTypeEncoding:@encode(id) currentValue:currentValue];
- (void)getterButtonPressed:(id)sender {
[super getterButtonPressed:sender];
id returnedObject = [self.defaults objectForKey:self.key];
[self exploreObjectOrPopViewController:returnedObject];
}
+ (BOOL)canEditDefaultWithValue:(id)currentValue {
return [FLEXArgumentInputViewFactory
canEditFieldWithTypeEncoding:FLEXEncodeObject(currentValue)
currentValue:currentValue
];
}
@end
+3 -1
View File
@@ -8,11 +8,13 @@
#import <UIKit/UIKit.h>
@class FLEXArgumentInputView;
@interface FLEXFieldEditorView : UIView
@property (nonatomic, copy) NSString *targetDescription;
@property (nonatomic, copy) NSString *fieldDescription;
@property (nonatomic, strong) NSArray *argumentInputViews;
@property (nonatomic, copy) NSArray<FLEXArgumentInputView *> *argumentInputViews;
@end
+22 -35
View File
@@ -12,20 +12,19 @@
@interface FLEXFieldEditorView ()
@property (nonatomic, strong) UILabel *targetDescriptionLabel;
@property (nonatomic, strong) UIView *targetDescriptionDivider;
@property (nonatomic, strong) UILabel *fieldDescriptionLabel;
@property (nonatomic, strong) UIView *fieldDescriptionDivider;
@property (nonatomic) UILabel *targetDescriptionLabel;
@property (nonatomic) UIView *targetDescriptionDivider;
@property (nonatomic) UILabel *fieldDescriptionLabel;
@property (nonatomic) UIView *fieldDescriptionDivider;
@end
@implementation FLEXFieldEditorView
- (id)initWithFrame:(CGRect)frame
{
- (id)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.targetDescriptionLabel = [[UILabel alloc] init];
self.targetDescriptionLabel = [UILabel new];
self.targetDescriptionLabel.numberOfLines = 0;
self.targetDescriptionLabel.font = [[self class] labelFont];
[self addSubview:self.targetDescriptionLabel];
@@ -33,7 +32,7 @@
self.targetDescriptionDivider = [[self class] dividerView];
[self addSubview:self.targetDescriptionDivider];
self.fieldDescriptionLabel = [[UILabel alloc] init];
self.fieldDescriptionLabel = [UILabel new];
self.fieldDescriptionLabel.numberOfLines = 0;
self.fieldDescriptionLabel.font = [[self class] labelFont];
[self addSubview:self.fieldDescriptionLabel];
@@ -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 *)argumentInputViews
{
- (void)setArgumentInputViews:(NSArray<FLEXArgumentInputView *> *)argumentInputViews {
if (![_argumentInputViews isEqual:argumentInputViews]) {
for (FLEXArgumentInputView *inputView in _argumentInputViews) {
@@ -121,40 +115,33 @@
}
}
+ (UIView *)dividerView
{
UIView *dividerView = [[UIView alloc] init];
+ (UIView *)dividerView {
UIView *dividerView = [UIView new];
dividerView.backgroundColor = [self dividerColor];
return dividerView;
}
+ (UIColor *)dividerColor
{
return [UIColor lightGrayColor];
+ (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 -16
View File
@@ -1,28 +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, strong, readonly) id target;
@property (nonatomic, strong, readonly) FLEXFieldEditorView *fieldEditorView;
@property (nonatomic, strong, readonly) UIBarButtonItem *setterButton;
- (void)actionButtonPressed:(id)sender;
- (NSString *)titleForActionButton;
- (void)getterButtonPressed:(id)sender;
@end
NS_ASSUME_NONNULL_END
+129 -84
View File
@@ -1,117 +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 "FLEXFieldEditorViewController.h"
#import "FLEXFieldEditorView.h"
#import "FLEXRuntimeUtility.h"
#import "FLEXUtility.h"
#import "FLEXArgumentInputView.h"
#import "FLEXArgumentInputViewFactory.h"
#import "FLEXPropertyAttributes.h"
#import "FLEXUtility.h"
#import "FLEXColor.h"
#import "UIBarButtonItem+FLEX.h"
@interface FLEXFieldEditorViewController () <UIScrollViewDelegate>
@interface FLEXFieldEditorViewController () <FLEXArgumentInputViewDelegate>
@property (nonatomic, strong) UIScrollView *scrollView;
@property (nonatomic) FLEXProperty *property;
@property (nonatomic) FLEXIvar *ivar;
@property (nonatomic, strong, readwrite) id target;
@property (nonatomic, strong, readwrite) FLEXFieldEditorView *fieldEditorView;
@property (nonatomic, strong, 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 = [FLEXUtility scrollViewGrayColor];
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 alloc] init];
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;
self.view.backgroundColor = [FLEXColor groupedBackgroundColor];
// Create getter button
_getterButton = [[UIBarButtonItem alloc]
initWithTitle:@"Get"
style:UIBarButtonItemStyleDone
target:self
action:@selector(getterButtonPressed:)
];
self.toolbarItems = @[
UIBarButtonItem.flex_flexibleSpace, self.getterButton, self.setterButton
];
// 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];
// 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)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;
- (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;
}
}
- (FLEXArgumentInputView *)firstInputView
{
return [[self.fieldEditorView argumentInputViews] firstObject];
}
- (void)actionButtonPressed:(id)sender
{
// Subclasses can override
- (void)getterButtonPressed:(id)sender {
[self.fieldEditorView endEditing:YES];
[self exploreObjectOrPopViewController:self.currentValue];
}
- (NSString *)titleForActionButton
{
// Subclasses can override.
return @"Set";
- (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 "FLEXFieldEditorViewController.h"
#import <objc/runtime.h>
@interface FLEXIvarEditorViewController : FLEXFieldEditorViewController
- (id)initWithTarget:(id)target ivar:(Ivar)ivar;
+ (BOOL)canEditIvar:(Ivar)ivar currentValue:(id)value;
@end
@@ -1,72 +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, assign) 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];
[FLEXRuntimeUtility setValue:self.firstInputView.inputValue forIvar:self.ivar onObject:self.target];
self.firstInputView.inputValue = [FLEXRuntimeUtility valueForIvar:self.ivar onObject:self.target];
}
- (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
@@ -13,87 +13,92 @@
#import "FLEXObjectExplorerViewController.h"
#import "FLEXArgumentInputView.h"
#import "FLEXArgumentInputViewFactory.h"
#import "FLEXUtility.h"
@interface FLEXMethodCallingViewController ()
@property (nonatomic, assign) Method method;
@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.title = [self isClassMethod] ? @"Class Method" : @"Method";
self.title = method.isInstanceMethod ? @"Method" : @"Class Method";
}
return self;
}
- (void)viewDidLoad
{
- (void)viewDidLoad {
[super viewDidLoad];
self.fieldEditorView.fieldDescription = [FLEXRuntimeUtility prettyNameForMethod:self.method isClassMethod:[self isClassMethod]];
NSArray *methodComponents = [FLEXRuntimeUtility prettyArgumentComponentsForMethod:self.method];
NSMutableArray *argumentInputViews = [NSMutableArray array];
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;
}
- (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) {
NSString *title = @"Method Call Failed";
NSString *message = [error localizedDescription];
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:title message:message delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
[alert show];
} 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
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 {
// 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];
[self exploreObjectOrPopViewController:returnValue];
}
}
@@ -1,18 +0,0 @@
//
// FLEXPropertyEditorViewController.h
// Flipboard
//
// Created by Ryan Olson on 5/20/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import "FLEXFieldEditorViewController.h"
#import <objc/runtime.h>
@interface FLEXPropertyEditorViewController : FLEXFieldEditorViewController
- (id)initWithTarget:(id)target property:(objc_property_t)property;
+ (BOOL)canEditProperty:(objc_property_t)property currentValue:(id)value;
@end
@@ -1,94 +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"
@interface FLEXPropertyEditorViewController () <FLEXArgumentInputViewDelegate>
@property (nonatomic, assign) 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 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) {
NSString *title = @"Property Setter Failed";
NSString *message = [error localizedDescription];
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:title message:message delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
[alert show];
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)argumentInputViewValueDidChange:(FLEXArgumentInputView *)argumentInputView
{
if ([argumentInputView isKindOfClass:[FLEXArgumentInputSwitchView class]]) {
[self actionButtonPressed:nil];
}
}
+ (BOOL)canEditProperty:(objc_property_t)property currentValue:(id)value
{
const char *typeEncoding = [[FLEXRuntimeUtility typeEncodingForProperty:property] UTF8String];
BOOL canEditType = [FLEXArgumentInputViewFactory canEditFieldWithTypeEncoding:typeEncoding currentValue:value];
BOOL isReadonly = [FLEXRuntimeUtility isReadonlyProperty:property];
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,14 +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:(UINavigationController *(^)(void))future completion:(void(^)(void))completion;
// Keyboard shortcut helpers
@@ -23,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
File diff suppressed because it is too large Load Diff
+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
+17 -12
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,22 +32,29 @@
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.
// Done at runtime with an obfuscated selector because it is private API. But you shoudn't ship this to the App Store anyways...
// Done at runtime with an obfuscated selector because it is private API. But you shouldn't ship this to the App Store anyways...
NSString *canAffectSelectorString = [@[@"_can", @"Affect", @"Status", @"Bar", @"Appearance"] componentsJoinedByString:@""];
SEL canAffectSelector = NSSelectorFromString(canAffectSelectorString);
Method shouldAffectMethod = class_getInstanceMethod(self, @selector(shouldAffectStatusBarAppearance));
@@ -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>
+15 -47
View File
@@ -6,12 +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
@interface FLEXManager : NSObject
+ (instancetype)sharedManager;
@property (nonatomic, readonly, class) FLEXManager *sharedManager;
@property (nonatomic, readonly) BOOL isHidden;
@@ -19,52 +22,17 @@
- (void)hideExplorer;
- (void)toggleExplorer;
#pragma mark - Network Debugging
/// Use this to present the explorer in a specific scene when the one
/// it chooses by default is not the one you wish to display it in.
- (void)showExplorerFromScene:(UIWindowScene *)scene API_AVAILABLE(ios(13.0));
/// 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, assign, getter=isNetworkDebuggingEnabled) BOOL networkDebuggingEnabled;
#pragma mark - Misc
/// Defaults to 25 MB if never set. Values set here are presisted across launches of the app.
/// The response cache uses an NSCache, so it may purge prior to hitting the limit when the app is under memory pressure.
@property (nonatomic, assign) NSUInteger networkResponseCacheByteLimit;
#pragma mark - Keyboard Shortcuts
/// Simulator keyboard shortcuts are enabled by default.
/// The shortcuts will not fire when there is an active text field, text view, or other responder accepting key input.
/// You can disable keyboard shortcuts if you have existing keyboard shortcuts that conflict with FLEX, or if you like doing things the hard way ;)
/// Keyboard shortcuts are always disabled (and support is compiled out) in non-simulator builds
@property (nonatomic, assign) BOOL simulatorShortcutsEnabled;
/// Adds an action to run when the specified key & modifier combination is pressed
/// @param key A single character string matching a key on the keyboard
/// @param modifiers Modifier keys such as shift, command, or alt/option
/// @param action The block to run on the main thread when the key & modifier combination is recognized.
/// @param description Shown the the keyboard shortcut help menu, which is accessed via the '?' key.
/// @note The action block will be retained for the duration of the application. You may want to use weak references.
/// @note FLEX registers several default keyboard shortcuts. Use the '?' key to see a list of shortcuts.
- (void)registerSimulatorShortcutWithKey:(NSString *)key modifiers:(UIKeyModifierFlags)modifiers action:(dispatch_block_t)action description:(NSString *)description;
#pragma mark - Extensions
/// Adds an entry at the bottom of the list of Global State items. Call this method before this view controller is displayed.
/// @param entryName The string to be displayed in the cell.
/// @param objectFutureBlock When you tap on the row, information about the object returned by this block will be displayed.
/// Passing a block that returns an object allows you to display information about an object whose actual pointer may change at runtime (e.g. +currentUser)
/// @note This method must be called from the main thread.
/// The objectFutureBlock will be invoked from the main thread and may return nil.
/// @note The passed block will be copied and retain for the duration of the application, you may want to use __weak references.
- (void)registerGlobalEntryWithName:(NSString *)entryName objectFutureBlock:(id (^)(void))objectFutureBlock;
/// Adds an entry at the bottom of the list of Global State items. Call this method before this view controller is displayed.
/// @param entryName The string to be displayed in the cell.
/// @param viewControllerFutureBlock When you tap on the row, view controller returned by this block will be pushed on the navigation controller stack.
/// @note This method must be called from the main thread.
/// The viewControllerFutureBlock will be invoked from the main thread and may not return nil.
/// @note The passed block will be copied and retain for the duration of the application, you may want to use __weak references.
- (void)registerGlobalEntryWithName:(NSString *)entryName
viewControllerFutureBlock:(UIViewController * (^)(void))viewControllerFutureBlock;
/// 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;
@end
typedef UIViewController *(^FLEXCustomContentViewerFuture)(NSData *data);
@@ -19,8 +19,8 @@
- (instancetype)initWithPath:(NSString*)path;
- (BOOL)open;
- (NSArray *)queryAllTables;
- (NSArray *)queryAllColumnsWithTableName:(NSString *)tableName;
- (NSArray *)queryAllDataWithTableName:(NSString *)tableName;
- (NSArray<NSDictionary<NSString *, id> *> *)queryAllTables;
- (NSArray<NSString *> *)queryAllColumnsWithTableName:(NSString *)tableName;
- (NSArray<NSDictionary<NSString *, id> *> *)queryAllDataWithTableName:(NSString *)tableName;
@end
@@ -13,14 +13,14 @@
@interface FLEXMultiColumnTableView ()
<UITableViewDataSource, UITableViewDelegate,UIScrollViewDelegate, FLEXTableContentCellDelegate>
@property (nonatomic, strong) UIScrollView *contentScrollView;
@property (nonatomic, strong) UIScrollView *headerScrollView;
@property (nonatomic, strong) UITableView *leftTableView;
@property (nonatomic, strong) UITableView *contentTableView;
@property (nonatomic, strong) UIView *leftHeader;
@property (nonatomic) UIScrollView *contentScrollView;
@property (nonatomic) UIScrollView *headerScrollView;
@property (nonatomic) UITableView *leftTableView;
@property (nonatomic) UITableView *contentTableView;
@property (nonatomic) UIView *leftHeader;
@property (nonatomic, strong) NSDictionary *sortStatusDict;
@property (nonatomic, strong) NSArray *rowData;
@property (nonatomic) NSDictionary<NSString *, NSNumber *> *sortStatusDict;
@property (nonatomic) NSArray *rowData;
@end
static const CGFloat kColumnMargin = 1;
@@ -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,20 +36,23 @@ 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;
CGFloat height = self.frame.size.height;
CGFloat topheaderHeight = [self topHeaderHeight];
CGFloat leftHeaderWidth = [self leftHeaderWidth];
CGFloat topInsets = 0.f;
if (@available (iOS 11.0, *)) {
topInsets = self.safeAreaInsets.top;
}
CGFloat contentWidth = 0.0;
NSInteger rowsCount = [self numberOfColumns];
@@ -58,25 +60,23 @@ static const CGFloat kColumnMargin = 1;
contentWidth += [self contentWidthForColumn:i];
}
self.leftTableView.frame = CGRectMake(0, topheaderHeight, leftHeaderWidth, height - topheaderHeight);
self.headerScrollView.frame = CGRectMake(leftHeaderWidth, 0, width - leftHeaderWidth, topheaderHeight);
self.leftTableView.frame = CGRectMake(0, topheaderHeight + topInsets, leftHeaderWidth, height - topheaderHeight - topInsets);
self.headerScrollView.frame = CGRectMake(leftHeaderWidth, topInsets, width - leftHeaderWidth, topheaderHeight);
self.headerScrollView.contentSize = CGSizeMake( self.contentTableView.frame.size.width, self.headerScrollView.frame.size.height);
self.contentTableView.frame = CGRectMake(0, 0, contentWidth + [self numberOfColumns] * [self columnMargin] , height - topheaderHeight);
self.contentScrollView.frame = CGRectMake(leftHeaderWidth, topheaderHeight, width - leftHeaderWidth, height - topheaderHeight);
self.contentTableView.frame = CGRectMake(0, 0, contentWidth + [self numberOfColumns] * [self columnMargin] , height - topheaderHeight - topInsets);
self.contentScrollView.frame = CGRectMake(leftHeaderWidth, topheaderHeight + topInsets, width - leftHeaderWidth, height - topheaderHeight - topInsets);
self.contentScrollView.contentSize = self.contentTableView.frame.size;
self.leftHeader.frame = CGRectMake(0, 0, [self leftHeaderWidth], [self topHeaderHeight]);
self.leftHeader.frame = CGRectMake(0, topInsets, [self leftHeaderWidth], [self topHeaderHeight]);
}
- (void)loadUI
{
- (void)loadUI {
[self loadHeaderScrollView];
[self loadContentScrollView];
[self loadLeftView];
}
- (void)reloadData
{
- (void)reloadData {
[self loadLeftViewData];
[self loadContentData];
[self loadHeaderData];
@@ -84,9 +84,8 @@ static const CGFloat kColumnMargin = 1;
#pragma mark - UI
- (void)loadHeaderScrollView
{
UIScrollView *headerScrollView = [[UIScrollView alloc] init];
- (void)loadHeaderScrollView {
UIScrollView *headerScrollView = [UIScrollView new];
headerScrollView.delegate = self;
self.headerScrollView = headerScrollView;
self.headerScrollView.backgroundColor = [UIColor colorWithWhite:0.803 alpha:0.850];
@@ -94,14 +93,13 @@ static const CGFloat kColumnMargin = 1;
[self addSubview:headerScrollView];
}
- (void)loadContentScrollView
{
- (void)loadContentScrollView {
UIScrollView *scrollView = [[UIScrollView alloc] init];
UIScrollView *scrollView = [UIScrollView new];
scrollView.bounces = NO;
scrollView.delegate = self;
UITableView *tableView = [[UITableView alloc] init];
UITableView *tableView = [UITableView new];
tableView.delegate = self;
tableView.dataSource = self;
tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
@@ -114,16 +112,15 @@ static const CGFloat kColumnMargin = 1;
}
- (void)loadLeftView
{
UITableView *leftTableView = [[UITableView alloc] init];
- (void)loadLeftView {
UITableView *leftTableView = [UITableView new];
leftTableView.delegate = self;
leftTableView.dataSource = self;
leftTableView.separatorStyle = UITableViewCellSeparatorStyleNone;
self.leftTableView = leftTableView;
[self addSubview:leftTableView];
UIView *leftHeader = [[UIView alloc] init];
UIView *leftHeader = [UIView new];
leftHeader.backgroundColor = [UIColor colorWithWhite:0.950 alpha:0.668];
self.leftHeader = leftHeader;
[self addSubview:leftHeader];
@@ -133,9 +130,8 @@ static const CGFloat kColumnMargin = 1;
#pragma mark - Data
- (void)loadHeaderData
{
NSArray *subviews = self.headerScrollView.subviews;
- (void)loadHeaderData {
NSArray<UIView *> *subviews = self.headerScrollView.subviews;
for (UIView *subview in subviews) {
[subview removeFromSuperview];
@@ -161,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];
@@ -186,20 +181,17 @@ 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
{
UIColor *backgroundColor = [UIColor whiteColor];
cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UIColor *backgroundColor = UIColor.whiteColor;
if (indexPath.row % 2 != 0) {
backgroundColor = [UIColor colorWithWhite:0.950 alpha:0.750];
}
@@ -214,11 +206,11 @@ static const CGFloat kColumnMargin = 1;
for (int i = 0 ; i < cell.labels.count; i++) {
UILabel *label = cell.labels[i];
label.textColor = [UIColor blackColor];
label.textColor = UIColor.blackColor;
NSString *content = [NSString stringWithFormat:@"%@",self.rowData[i]];
if ([content isEqualToString:@"<null>"]) {
label.textColor = [UIColor lightGrayColor];
label.textColor = UIColor.lightGrayColor;
content = @"NULL";
}
label.text = content;
@@ -234,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;
}
@@ -265,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
@@ -282,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];
}
@@ -18,7 +18,7 @@
@interface FLEXRealmDatabaseManager ()
@property (nonatomic, copy) NSString *path;
@property (nonatomic, strong) id realm;
@property (nonatomic) RLMRealm * realm;
@end
@@ -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");
@@ -51,15 +49,14 @@
}
NSError *error = nil;
id configuration = [[configurationClass alloc] init];
id configuration = [configurationClass new];
[(RLMRealmConfiguration *)configuration setFileURL:[NSURL fileURLWithPath:self.path]];
self.realm = [realmClass realmWithConfiguration:configuration error:&error];
return (error == nil);
}
- (NSArray *)queryAllTables
{
NSMutableArray *allTables = [NSMutableArray array];
- (NSArray<NSDictionary<NSString *, id> *> *)queryAllTables {
NSMutableArray<NSDictionary<NSString *, id> *> *allTables = [NSMutableArray array];
RLMSchema *schema = [self.realm schema];
for (RLMObjectSchema *objectSchema in schema.objectSchema) {
@@ -67,21 +64,20 @@
continue;
}
NSDictionary *dictionary = @{@"name":objectSchema.className};
NSDictionary<NSString *, id> *dictionary = @{@"name":objectSchema.className};
[allTables addObject:dictionary];
}
return allTables;
}
- (NSArray *)queryAllColumnsWithTableName:(NSString *)tableName
{
- (NSArray<NSString *> *)queryAllColumnsWithTableName:(NSString *)tableName {
RLMObjectSchema *objectSchema = [[self.realm schema] schemaForClassName:tableName];
if (objectSchema == nil) {
return nil;
}
NSMutableArray *columnNames = [NSMutableArray array];
NSMutableArray<NSString *> *columnNames = [NSMutableArray array];
for (RLMProperty *property in objectSchema.properties) {
[columnNames addObject:property.name];
}
@@ -89,17 +85,16 @@
return columnNames;
}
- (NSArray *)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) {
return nil;
}
NSMutableArray *allDataEntries = [NSMutableArray array];
NSMutableArray<NSDictionary<NSString *, id> *> *allDataEntries = [NSMutableArray array];
for (RLMObject *result in results) {
NSMutableDictionary *entry = [NSMutableDictionary dictionary];
NSMutableDictionary<NSString *, id> *entry = [NSMutableDictionary dictionary];
for (RLMProperty *property in objectSchema.properties) {
id value = [result valueForKey:property.name];
entry[property.name] = (value) ? (value) : [NSNull null];
@@ -22,13 +22,13 @@
@end
@interface RLMSchema : NSObject
@property (nonatomic, readonly) NSArray *objectSchema;
@property (nonatomic, readonly) NSArray<RLMObjectSchema *> *objectSchema;
- (RLMObjectSchema *)schemaForClassName:(NSString *)className;
@end
@interface RLMObjectSchema : NSObject
@property (nonatomic, readonly) NSString *className;
@property (nonatomic, readonly) NSArray *properties;
@property (nonatomic, readonly) NSArray<RLMProperty *> *properties;
@end
@interface RLMProperty : NSString
@@ -43,4 +43,4 @@
@end
#endif
#endif
@@ -7,19 +7,18 @@
//
#import "FLEXSQLiteDatabaseManager.h"
#import "FLEXManager.h"
#import <sqlite3.h>
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) {
@@ -32,7 +31,18 @@ static NSString *const QUERY_TABLENAMES_SQL = @"SELECT name FROM sqlite_master W
if (_db) {
return YES;
}
int err = sqlite3_open([_databasePath UTF8String], &_db);
int err = sqlite3_open(_databasePath.UTF8String, &_db);
#if SQLITE_HAS_CODEC
NSString *defaultSqliteDatabasePassword = [FLEXManager sharedManager].defaultSqliteDatabasePassword;
if (defaultSqliteDatabasePassword) {
const char *key = defaultSqliteDatabasePassword.UTF8String;
sqlite3_key(_db, key, (int)strlen(key));
}
#endif
if(err != SQLITE_OK) {
NSLog(@"error opening!: %d", err);
return NO;
@@ -74,24 +84,22 @@ static NSString *const QUERY_TABLENAMES_SQL = @"SELECT name FROM sqlite_master W
}
- (NSArray *)queryAllTables
{
- (NSArray<NSDictionary<NSString *, id> *> *)queryAllTables {
return [self executeQuery:QUERY_TABLENAMES_SQL];
}
- (NSArray *)queryAllColumnsWithTableName:(NSString *)tableName
{
- (NSArray<NSString *> *)queryAllColumnsWithTableName:(NSString *)tableName {
NSString *sql = [NSString stringWithFormat:@"PRAGMA table_info('%@')",tableName];
NSArray *resultArray = [self executeQuery:sql];
NSMutableArray *array = [NSMutableArray array];
for (NSDictionary *dict in resultArray) {
[array addObject:dict[@"name"]];
NSArray<NSDictionary<NSString *, id> *> *resultArray = [self executeQuery:sql];
NSMutableArray<NSString *> *array = [NSMutableArray array];
for (NSDictionary<NSString *, id> *dict in resultArray) {
NSString *columnName = (NSString *)dict[@"name"] ?: @"";
[array addObject:columnName];
}
return array;
}
- (NSArray *)queryAllDataWithTableName:(NSString *)tableName
{
- (NSArray<NSDictionary<NSString *, id> *> *)queryAllDataWithTableName:(NSString *)tableName {
NSString *sql = [NSString stringWithFormat:@"SELECT * FROM %@",tableName];
return [self executeQuery:sql];
}
@@ -99,16 +107,15 @@ static NSString *const QUERY_TABLENAMES_SQL = @"SELECT name FROM sqlite_master W
#pragma mark -
#pragma mark - Private
- (NSArray *)executeQuery:(NSString *)sql
{
- (NSArray<NSDictionary<NSString *, id> *> *)executeQuery:(NSString *)sql {
[self open];
NSMutableArray *resultArray = [NSMutableArray array];
NSMutableArray<NSDictionary<NSString *, id> *> *resultArray = [NSMutableArray array];
sqlite3_stmt *pstmt;
if (sqlite3_prepare_v2(_db, [sql UTF8String], -1, &pstmt, 0) == SQLITE_OK) {
if (sqlite3_prepare_v2(_db, sql.UTF8String, -1, &pstmt, 0) == SQLITE_OK) {
while (sqlite3_step(pstmt) == SQLITE_ROW) {
NSUInteger num_cols = (NSUInteger)sqlite3_data_count(pstmt);
if (num_cols > 0) {
NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:num_cols];
NSMutableDictionary<NSString *, id> *dict = [NSMutableDictionary dictionaryWithCapacity:num_cols];
int columnCount = sqlite3_column_count(pstmt);
@@ -1,6 +1,6 @@
//
// FLEXTableContentHeaderCell.h
// UICatalog
// FLEX
//
// Created by Peng Tao on 15/11/26.
// Copyright © 2015年 f. All rights reserved.
@@ -16,7 +16,7 @@ typedef NS_ENUM(NSUInteger, FLEXTableColumnHeaderSortType) {
@interface FLEXTableColumnHeader : UIView
@property (nonatomic, strong) UILabel *label;
@property (nonatomic) UILabel *label;
- (void)changeSortStatusWithType:(FLEXTableColumnHeaderSortType)type;
@@ -1,6 +1,6 @@
//
// FLEXTableContentHeaderCell.m
// UICatalog
// FLEX
//
// Created by Peng Tao on 15/11/26.
// Copyright © 2015 f. All rights reserved.
@@ -8,17 +8,15 @@
#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];
self.backgroundColor = UIColor.whiteColor;
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(5, 0, frame.size.width - 25, frame.size.height)];
label.font = [UIFont systemFontOfSize:13.0];
@@ -38,8 +36,7 @@
return self;
}
- (void)changeSortStatusWithType:(FLEXTableColumnHeaderSortType)type
{
- (void)changeSortStatusWithType:(FLEXTableColumnHeaderSortType)type {
switch (type) {
case FLEXTableColumnHeaderSortTypeNone:
_arrowLabel.text = @"";
@@ -1,6 +1,6 @@
//
// FLEXTableContentCell.h
// UICatalog
// FLEX
//
// Created by Peng Tao on 15/11/24.
// Copyright © 2015年 f. All rights reserved.
@@ -18,9 +18,9 @@
@interface FLEXTableContentCell : UITableViewCell
@property (nonatomic, strong)NSArray *labels;
@property (nonatomic) NSArray<UILabel *> *labels;
@property (nonatomic, weak) id<FLEXTableContentCellDelegate>delegate;
@property (nonatomic, weak) id<FLEXTableContentCellDelegate> delegate;
+ (instancetype)cellWithTableView:(UITableView *)tableView columnNumber:(NSInteger)number;
@@ -1,6 +1,6 @@
//
// FLEXTableContentCell.m
// UICatalog
// FLEX
//
// Created by Peng Tao on 15/11/24.
// Copyright © 2015 f. All rights reserved.
@@ -15,19 +15,18 @@
@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) {
cell = [[FLEXTableContentCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
NSMutableArray *labels = [NSMutableArray array];
NSMutableArray<UILabel *> *labels = [NSMutableArray array];
for (int i = 0; i < number ; i++) {
UILabel *label = [[UILabel alloc] initWithFrame:CGRectZero];
label.backgroundColor = [UIColor whiteColor];
label.backgroundColor = UIColor.whiteColor;
label.font = [UIFont systemFontOfSize:13.0];
label.textAlignment = NSTextAlignmentLeft;
label.backgroundColor = [UIColor greenColor];
label.backgroundColor = UIColor.greenColor;
[labels addObject:label];
UITapGestureRecognizer *gesture = [[UITapGestureRecognizer alloc] initWithTarget:cell
@@ -36,15 +35,14 @@
label.userInteractionEnabled = YES;
[cell.contentView addSubview:label];
cell.contentView.backgroundColor = [UIColor whiteColor];
cell.contentView.backgroundColor = UIColor.whiteColor;
}
cell.labels = labels;
}
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];
@@ -10,7 +10,7 @@
@interface FLEXTableContentViewController : UIViewController
@property (nonatomic, strong) NSArray *columnsArray;
@property (nonatomic, strong) NSArray *contentsArray;
@property (nonatomic) NSArray<NSString *> *columnsArray;
@property (nonatomic) NSArray<NSDictionary<NSString *, id> *> *contentsArray;
@end
+49 -64
View File
@@ -13,72 +13,60 @@
@interface FLEXTableContentViewController ()<FLEXMultiColumnTableViewDataSource, FLEXMultiColumnTableViewDelegate>
@property (nonatomic, strong)FLEXMultiColumnTableView *multiColumView;
@property (nonatomic) FLEXMultiColumnTableView *multiColumnView;
@end
@implementation FLEXTableContentViewController
- (instancetype)init
{
self = [super init];
if (self) {
CGRect rectStatus = [UIApplication sharedApplication].statusBarFrame;
CGFloat y = 64;
if (rectStatus.size.height == 0) {
y = 32;
}
_multiColumView = [[FLEXMultiColumnTableView alloc] initWithFrame:
CGRectMake(0, y, self.view.frame.size.width, self.view.frame.size.height - y)];
_multiColumView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
_multiColumView.backgroundColor = [UIColor whiteColor];
_multiColumView.dataSource = self;
_multiColumView.delegate = self;
self.automaticallyAdjustsScrollViewInsets = NO;
[self.view addSubview:_multiColumView];
}
return self;
- (void)viewDidLoad {
[super viewDidLoad];
self.edgesForExtendedLayout = UIRectEdgeNone;
[self.view addSubview:self.multiColumnView];
}
- (void)viewWillAppear:(BOOL)animated
{
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
[self.multiColumView reloadData];
[self.multiColumnView reloadData];
}
#pragma mark -
#pragma mark init SubView
- (FLEXMultiColumnTableView *)multiColumnView {
if (!_multiColumnView) {
_multiColumnView = [[FLEXMultiColumnTableView alloc] initWithFrame:
CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height)];
_multiColumnView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleTopMargin;
_multiColumnView.backgroundColor = UIColor.whiteColor;
_multiColumnView.dataSource = self;
_multiColumnView.delegate = self;
}
return _multiColumnView;
}
#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 *dic = self.contentsArray[row];
NSDictionary<NSString *, id> *dic = self.contentsArray[row];
if (self.contentsArray.count > column) {
return [NSString stringWithFormat:@"%@",[dic objectForKey:self.columnsArray[column]]];
}
@@ -86,40 +74,35 @@
return @"";
}
- (NSArray *)contentAtRow:(NSInteger)row
{
- (NSArray *)contentAtRow:(NSInteger)row {
NSMutableArray *result = [NSMutableArray array];
if (self.contentsArray.count > row) {
NSDictionary *dic = self.contentsArray[row];
NSDictionary<NSString *, id> *dic = self.contentsArray[row];
for (int i = 0; i < self.columnsArray.count; i ++) {
[result addObject:dic[self.columnsArray[i]]];
}
return result;
return result;
}
return nil;
}
- (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 *attrs = @{@"NSFontAttributeName":[UIFont systemFontOfSize:17.0]};
NSDictionary<NSString *, id> *attrs = @{@"NSFontAttributeName":[UIFont systemFontOfSize:17.0]};
CGSize size = [str boundingRectWithSize:CGSizeMake(CGFLOAT_MAX, 14)
options:NSStringDrawingUsesLineFragmentOrigin
attributes:attrs context:nil].size;
@@ -130,16 +113,14 @@
#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 *sortContentData = [self.contentsArray sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2) {
NSArray<NSDictionary<NSString *, id> *> *sortContentData = [self.contentsArray sortedArrayUsingComparator:^NSComparisonResult(NSDictionary<NSString *, id> * obj1, NSDictionary<NSString *, id> * obj2) {
if ([obj1 objectForKey:text] == [NSNull null]) {
return NSOrderedAscending;
@@ -147,34 +128,38 @@
if ([obj2 objectForKey:text] == [NSNull null]) {
return NSOrderedDescending;
}
if (![[obj1 objectForKey:text] respondsToSelector:@selector(compare:)] && ![[obj2 objectForKey:text] respondsToSelector:@selector(compare:)]) {
return NSOrderedSame;
}
NSComparisonResult result = [[obj1 objectForKey:text] compare:[obj2 objectForKey:text]];
return result;
}];
if (sortType == FLEXTableColumnHeaderSortTypeDesc) {
NSEnumerator *contentReverseEvumerator = [sortContentData reverseObjectEnumerator];
sortContentData = [NSArray arrayWithArray:[contentReverseEvumerator allObjects]];
NSEnumerator *contentReverseEnumerator = sortContentData.reverseObjectEnumerator;
sortContentData = [NSArray arrayWithArray:contentReverseEnumerator.allObjects];
}
self.contentsArray = sortContentData;
[self.multiColumView reloadData];
[self.multiColumnView reloadData];
}
#pragma mark -
#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) {
if (newCollection.verticalSizeClass == UIUserInterfaceSizeClassCompact) {
_multiColumView.frame = CGRectMake(0, 32, self.view.frame.size.width, self.view.frame.size.height - 32);
self->_multiColumnView.frame = CGRectMake(0, 32, self.view.frame.size.width, self.view.frame.size.height - 32);
}
else {
_multiColumView.frame = CGRectMake(0, 64, self.view.frame.size.width, self.view.frame.size.height - 64);
self->_multiColumnView.frame = CGRectMake(0, 64, self.view.frame.size.width, self.view.frame.size.height - 64);
}
[self.view setNeedsLayout];
} completion:nil];
@@ -1,6 +1,6 @@
//
// FLEXTableLeftCell.h
// UICatalog
// FLEX
//
// Created by Peng Tao on 15/11/24.
// Copyright © 2015年 f. All rights reserved.
@@ -10,7 +10,7 @@
@interface FLEXTableLeftCell : UITableViewCell
@property (nonatomic, strong) UILabel *titlelabel;
@property (nonatomic) UILabel *titlelabel;
+ (instancetype)cellWithTableView:(UITableView *)tableView;
@@ -1,6 +1,6 @@
//
// FLEXTableLeftCell.m
// UICatalog
// FLEX
//
// Created by Peng Tao on 15/11/24.
// Copyright © 2015 f. All rights reserved.
@@ -10,8 +10,7 @@
@implementation FLEXTableLeftCell
+ (instancetype)cellWithTableView:(UITableView *)tableView
{
+ (instancetype)cellWithTableView:(UITableView *)tableView {
static NSString *identifier = @"FLEXTableLeftCell";
FLEXTableLeftCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
@@ -20,15 +19,14 @@
UILabel *textLabel = [[UILabel alloc] initWithFrame:CGRectZero];
textLabel.textAlignment = NSTextAlignmentCenter;
textLabel.font = [UIFont systemFontOfSize:13.0];
textLabel.backgroundColor = [UIColor clearColor];
textLabel.backgroundColor = UIColor.clearColor;
[cell.contentView addSubview:textLabel];
cell.titlelabel = textLabel;
}
return cell;
}
- (void)layoutSubviews
{
- (void)layoutSubviews {
[super layoutSubviews];
self.titlelabel.frame = self.contentView.frame;
}
@@ -6,9 +6,9 @@
// Copyright © 2015年 Peng Tao. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "FLEXTableViewController.h"
@interface FLEXTableListViewController : UITableViewController
@interface FLEXTableListViewController : FLEXTableViewController
+ (BOOL)supportsExtension:(NSString *)extension;
- (instancetype)initWithPath:(NSString *)path;
@@ -14,23 +14,22 @@
#import "FLEXTableContentViewController.h"
@interface FLEXTableListViewController ()
{
@interface FLEXTableListViewController () {
id<FLEXDatabaseManager> _dbm;
NSString *_databasePath;
}
@property (nonatomic, strong) NSArray *tables;
@property (nonatomic) NSArray<NSString *> *tables;
@property (nonatomic) NSArray<NSString *> *filteredTables;
+ (NSArray *)supportedSQLiteExtensions;
+ (NSArray *)supportedRealmExtensions;
+ (NSArray<NSString *> *)supportedSQLiteExtensions;
+ (NSArray<NSString *> *)supportedRealmExtensions;
@end
@implementation FLEXTableListViewController
- (instancetype)initWithPath:(NSString *)path
{
- (instancetype)initWithPath:(NSString *)path {
self = [super initWithStyle:UITableViewStyleGrouped];
if (self) {
_databasePath = [path copy];
@@ -41,16 +40,15 @@
return self;
}
- (id<FLEXDatabaseManager>)databaseManagerForFileAtPath:(NSString *)path
{
- (id<FLEXDatabaseManager>)databaseManagerForFileAtPath:(NSString *)path {
NSString *pathExtension = path.pathExtension.lowercaseString;
NSArray *sqliteExtensions = [FLEXTableListViewController supportedSQLiteExtensions];
NSArray<NSString *> *sqliteExtensions = [FLEXTableListViewController supportedSQLiteExtensions];
if ([sqliteExtensions indexOfObject:pathExtension] != NSNotFound) {
return [[FLEXSQLiteDatabaseManager alloc] initWithPath:path];
}
NSArray *realmExtensions = [FLEXTableListViewController supportedRealmExtensions];
NSArray<NSString *> *realmExtensions = [FLEXTableListViewController supportedRealmExtensions];
if (realmExtensions != nil && [realmExtensions indexOfObject:pathExtension] != NSNotFound) {
return [[FLEXRealmDatabaseManager alloc] initWithPath:path];
}
@@ -58,60 +56,78 @@
return nil;
}
- (void)getAllTables
{
NSArray *resultArray = [_dbm queryAllTables];
NSMutableArray *array = [NSMutableArray array];
for (NSDictionary *dict in resultArray) {
[array addObject:dict[@"name"]];
- (void)getAllTables {
NSArray<NSDictionary<NSString *, id> *> *resultArray = [_dbm queryAllTables];
NSMutableArray<NSString *> *array = [NSMutableArray array];
for (NSDictionary<NSString *, id> *dict in resultArray) {
NSString *columnName = (NSString *)dict[@"name"] ?: @"";
[array addObject:columnName];
}
self.tables = array;
self.filteredTables = array;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.tables.count;
- (void)viewDidLoad {
[super viewDidLoad];
self.showsSearchBar = YES;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
#pragma mark - Search bar
- (void)updateSearchResults:(NSString *)searchText {
if (searchText.length > 0) {
NSPredicate *searchPredicate = [NSPredicate predicateWithFormat:@"SELF CONTAINS[cd] %@", searchText];
self.filteredTables = [self.tables filteredArrayUsingPredicate:searchPredicate];
} else {
self.filteredTables = self.tables;
}
[self.tableView reloadData];
}
#pragma mark - Table View Data Source
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.filteredTables.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"FLEXTableListViewControllerCell"];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault
reuseIdentifier:@"FLEXTableListViewControllerCell"];
}
cell.textLabel.text = self.tables[indexPath.row];
cell.textLabel.text = self.filteredTables[indexPath.row];
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
FLEXTableContentViewController *contentViewController = [[FLEXTableContentViewController alloc] init];
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
FLEXTableContentViewController *contentViewController = [FLEXTableContentViewController new];
contentViewController.contentsArray = [_dbm queryAllDataWithTableName:self.tables[indexPath.row]];
contentViewController.columnsArray = [_dbm queryAllColumnsWithTableName:self.tables[indexPath.row]];
contentViewController.contentsArray = [_dbm queryAllDataWithTableName:self.filteredTables[indexPath.row]];
contentViewController.columnsArray = [_dbm queryAllColumnsWithTableName:self.filteredTables[indexPath.row]];
contentViewController.title = self.tables[indexPath.row];
contentViewController.title = self.filteredTables[indexPath.row];
[self.navigationController pushViewController:contentViewController animated:YES];
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
return [NSString stringWithFormat:@"%lu tables", (unsigned long)self.tables.count];
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
return [NSString stringWithFormat:@"Tables (%lu)", (unsigned long)self.filteredTables.count];
}
+ (BOOL)supportsExtension:(NSString *)extension
{
#pragma mark - FLEXTableListViewController
+ (BOOL)supportsExtension:(NSString *)extension {
extension = extension.lowercaseString;
NSArray *sqliteExtensions = [FLEXTableListViewController supportedSQLiteExtensions];
NSArray<NSString *> *sqliteExtensions = [FLEXTableListViewController supportedSQLiteExtensions];
if (sqliteExtensions.count > 0 && [sqliteExtensions indexOfObject:extension] != NSNotFound) {
return YES;
}
NSArray *realmExtensions = [FLEXTableListViewController supportedRealmExtensions];
NSArray<NSString *> *realmExtensions = [FLEXTableListViewController supportedRealmExtensions];
if (realmExtensions.count > 0 && [realmExtensions indexOfObject:extension] != NSNotFound) {
return YES;
}
@@ -119,13 +135,11 @@
return NO;
}
+ (NSArray *)supportedSQLiteExtensions
{
+ (NSArray<NSString *> *)supportedSQLiteExtensions {
return @[@"db", @"sqlite", @"sqlite3"];
}
+ (NSArray *)supportedRealmExtensions
{
+ (NSArray<NSString *> *)supportedRealmExtensions {
if (NSClassFromString(@"RLMRealm") == nil) {
return nil;
}
@@ -0,0 +1,13 @@
//
// FLEXAddressExplorerCoordinator.h
// FLEX
//
// Created by Tanner Bennett on 7/10/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import "FLEXGlobalsEntry.h"
@interface FLEXAddressExplorerCoordinator : NSObject <FLEXGlobalsEntry>
@end
@@ -0,0 +1,95 @@
//
// FLEXAddressExplorerCoordinator.m
// FLEX
//
// Created by Tanner Bennett on 7/10/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import "FLEXAddressExplorerCoordinator.h"
#import "FLEXGlobalsViewController.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXObjectExplorerViewController.h"
#import "FLEXRuntimeUtility.h"
#import "FLEXUtility.h"
@interface FLEXGlobalsViewController (FLEXAddressExploration)
- (void)deselectSelectedRow;
- (void)tryExploreAddress:(NSString *)addressString safely:(BOOL)safely;
@end
@implementation FLEXAddressExplorerCoordinator
#pragma mark - FLEXGlobalsEntry
+ (NSString *)globalsEntryTitle:(FLEXGlobalsRow)row {
return @"🔎 Address Explorer";
}
+ (FLEXGlobalsTableViewControllerRowAction)globalsEntryRowAction:(FLEXGlobalsRow)row {
return ^(FLEXGlobalsViewController *host) {
NSString *title = @"Explore Object at Address";
NSString *message = @"Paste a hexadecimal address below, starting with '0x'. "
"Use the unsafe option if you need to bypass pointer validation, "
"but know that it may crash the app if the address is invalid.";
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(title).message(message);
make.configuredTextField(^(UITextField *textField) {
NSString *copied = UIPasteboard.generalPasteboard.string;
textField.placeholder = @"0x00000070deadbeef";
// Go ahead and paste our clipboard if we have an address copied
if ([copied hasPrefix:@"0x"]) {
textField.text = copied;
[textField selectAll:nil];
}
});
make.button(@"Explore").handler(^(NSArray<NSString *> *strings) {
[host tryExploreAddress:strings.firstObject safely:YES];
});
make.button(@"Unsafe Explore").destructiveStyle().handler(^(NSArray *strings) {
[host tryExploreAddress:strings.firstObject safely:NO];
});
make.button(@"Cancel").cancelStyle();
} showFrom:host];
};
}
@end
@implementation FLEXGlobalsViewController (FLEXAddressExploration)
- (void)deselectSelectedRow {
NSIndexPath *selected = self.tableView.indexPathForSelectedRow;
[self.tableView deselectRowAtIndexPath:selected animated:YES];
}
- (void)tryExploreAddress:(NSString *)addressString safely:(BOOL)safely {
NSScanner *scanner = [NSScanner scannerWithString:addressString];
unsigned long long hexValue = 0;
BOOL didParseAddress = [scanner scanHexLongLong:&hexValue];
const void *pointerValue = (void *)hexValue;
NSString *error = nil;
if (didParseAddress) {
if (safely && ![FLEXRuntimeUtility pointerIsValidObjcObject:pointerValue]) {
error = @"The given address is unlikely to be a valid object.";
}
} else {
error = @"Malformed address. Make sure it's not too long and starts with '0x'.";
}
if (!error) {
id object = (__bridge id)pointerValue;
FLEXObjectExplorerViewController *explorer = [FLEXObjectExplorerFactory explorerViewControllerForObject:object];
[self.navigationController pushViewController:explorer animated:YES];
} else {
[FLEXAlert showAlert:@"Uh-oh" message:error from:self];
[self deselectSelectedRow];
}
}
@end
@@ -1,15 +0,0 @@
//
// FLEXClassesTableViewController.h
// Flipboard
//
// Created by Ryan Olson on 2014-05-03.
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface FLEXClassesTableViewController : UITableViewController
@property (nonatomic, copy) NSString *binaryImageName;
@end
@@ -1,140 +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 () <UISearchBarDelegate>
@property (nonatomic, strong) NSArray *classNames;
@property (nonatomic, strong) NSArray *filteredClassNames;
@property (nonatomic, strong) UISearchBar *searchBar;
@end
@implementation FLEXClassesTableViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.searchBar = [[UISearchBar alloc] init];
self.searchBar.placeholder = [FLEXUtility searchBarPlaceholderText];
self.searchBar.delegate = self;
[self.searchBar sizeToFit];
self.tableView.tableHeaderView = self.searchBar;
}
- (void)setBinaryImageName:(NSString *)binaryImageName
{
if (![_binaryImageName isEqual:binaryImageName]) {
_binaryImageName = binaryImageName;
[self loadClassNames];
[self updateTitle];
}
}
- (void)setClassNames:(NSArray *)classNames
{
_classNames = classNames;
self.filteredClassNames = classNames;
}
- (void)loadClassNames
{
unsigned int classNamesCount = 0;
const char **classNames = objc_copyClassNamesForImage([self.binaryImageName UTF8String], &classNamesCount);
if (classNames) {
NSMutableArray *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);
}
}
- (void)updateTitle
{
NSString *shortImageName = self.binaryImageName.lastPathComponent;
self.title = [NSString stringWithFormat:@"%@ Classes (%lu)", shortImageName, (unsigned long)[self.filteredClassNames count]];
}
#pragma mark - Search
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
{
if ([searchText length] > 0) {
NSPredicate *searchPreidcate = [NSPredicate predicateWithFormat:@"SELF CONTAINS[cd] %@", searchText];
self.filteredClassNames = [self.classNames filteredArrayUsingPredicate:searchPreidcate];
} else {
self.filteredClassNames = self.classNames;
}
[self updateTitle];
[self.tableView reloadData];
}
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar
{
[searchBar resignFirstResponder];
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
// Dismiss the keyboard when interacting with filtered results.
[self.searchBar endEditing:YES];
}
#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
@@ -6,8 +6,9 @@
// Copyright © 2015 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "FLEXGlobalsEntry.h"
#import "FLEXTableViewController.h"
@interface FLEXCookiesTableViewController : UITableViewController
@interface FLEXCookiesTableViewController : FLEXTableViewController <FLEXGlobalsEntry>
@end
@@ -11,33 +11,37 @@
#import "FLEXUtility.h"
@interface FLEXCookiesTableViewController ()
@property (nonatomic, strong) NSArray *cookies;
@property (nonatomic, readonly) NSArray<NSHTTPCookie *> *cookies;
@property (nonatomic) NSString *headerTitle;
@end
@implementation FLEXCookiesTableViewController
- (id)initWithStyle:(UITableViewStyle)style {
self = [super initWithStyle:style];
if (self) {
self.title = @"Cookies";
- (void)viewDidLoad {
[super viewDidLoad];
NSSortDescriptor *nameSortDescriptor = [[NSSortDescriptor alloc] initWithKey:@"name" ascending:YES selector:@selector(caseInsensitiveCompare:)];
_cookies =[[NSHTTPCookieStorage sharedHTTPCookieStorage].cookies sortedArrayUsingDescriptors:@[nameSortDescriptor]];
}
return self;
NSSortDescriptor *nameSortDescriptor = [[NSSortDescriptor alloc]
initWithKey:@"name" ascending:YES selector:@selector(caseInsensitiveCompare:)
];
_cookies = [NSHTTPCookieStorage.sharedHTTPCookieStorage.cookies
sortedArrayUsingDescriptors:@[nameSortDescriptor]
];
self.title = @"Cookies";
[self updateHeaderTitle];
}
- (void)updateHeaderTitle {
self.headerTitle = [NSString stringWithFormat:@"%@ cookies", @(self.cookies.count)];
// TODO update header title here when we can search cookies
}
- (NSHTTPCookie *)cookieForRowAtIndexPath:(NSIndexPath *)indexPath {
return self.cookies[indexPath.row];
}
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView {
return 1;
}
#pragma mark - Table View Data Source
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return self.cookies.count;
@@ -48,9 +52,9 @@
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];
cell.textLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
cell.detailTextLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
cell.detailTextLabel.textColor = [UIColor grayColor];
cell.textLabel.font = UIFont.flex_defaultTableCellFont;
cell.detailTextLabel.font = UIFont.flex_defaultTableCellFont;
cell.detailTextLabel.textColor = UIColor.grayColor;
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
}
@@ -61,6 +65,13 @@
return cell;
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
return self.headerTitle;
}
#pragma mark - Table View Delegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NSHTTPCookie *cookie = [self cookieForRowAtIndexPath:indexPath];
UIViewController *cookieViewController = (UIViewController *)[FLEXObjectExplorerFactory explorerViewControllerForObject:cookie];
@@ -68,4 +79,15 @@
[self.navigationController pushViewController:cookieViewController animated:YES];
}
#pragma mark - FLEXGlobalsEntry
+ (NSString *)globalsEntryTitle:(FLEXGlobalsRow)row {
return @"🍪 Cookies";
}
+ (UIViewController *)globalsEntryViewController:(FLEXGlobalsRow)row {
return [self new];
}
@end
@@ -1,33 +0,0 @@
//
// FLEXFileBrowserFileOperationController.h
// Flipboard
//
// Created by Daniel Rodriguez Troitino on 2/13/15.
// Copyright (c) 2015 Flipboard. All rights reserved.
//
#import <Foundation/Foundation.h>
@protocol FLEXFileBrowserFileOperationController;
@protocol FLEXFileBrowserFileOperationControllerDelegate <NSObject>
- (void)fileOperationControllerDidDismiss:(id<FLEXFileBrowserFileOperationController>)controller;
@end
@protocol FLEXFileBrowserFileOperationController <NSObject>
@property (nonatomic, weak) id<FLEXFileBrowserFileOperationControllerDelegate> delegate;
- (instancetype)initWithPath:(NSString *)path;
- (void)show;
@end
@interface FLEXFileBrowserFileDeleteOperationController : NSObject <FLEXFileBrowserFileOperationController>
@end
@interface FLEXFileBrowserFileRenameOperationController : NSObject <FLEXFileBrowserFileOperationController>
@end
@@ -1,142 +0,0 @@
//
// FLEXFileBrowserFileOperationController.m
// Flipboard
//
// Created by Daniel Rodriguez Troitino on 2/13/15.
// Copyright (c) 2015 Flipboard. All rights reserved.
//
#import "FLEXFileBrowserFileOperationController.h"
#import <UIKit/UIKit.h>
@interface FLEXFileBrowserFileDeleteOperationController () <UIAlertViewDelegate>
@property (nonatomic, copy, readonly) NSString *path;
- (instancetype)initWithPath:(NSString *)path NS_DESIGNATED_INITIALIZER;
@end
@implementation FLEXFileBrowserFileDeleteOperationController
@synthesize delegate = _delegate;
- (instancetype)init
{
return [self initWithPath:nil];
}
- (instancetype)initWithPath:(NSString *)path
{
self = [super init];
if (self) {
_path = path;
}
return self;
}
- (void)show
{
BOOL isDirectory = NO;
BOOL stillExists = [[NSFileManager defaultManager] fileExistsAtPath:self.path isDirectory:&isDirectory];
if (stillExists) {
UIAlertView *deleteWarning = [[UIAlertView alloc]
initWithTitle:[NSString stringWithFormat:@"Delete %@?", self.path.lastPathComponent]
message:[NSString stringWithFormat:@"The %@ will be deleted. This operation cannot be undone", isDirectory ? @"directory" : @"file"]
delegate:self
cancelButtonTitle:@"Cancel"
otherButtonTitles:@"Delete", nil];
[deleteWarning show];
} else {
[[[UIAlertView alloc] initWithTitle:@"File Removed" message:@"The file at the specified path no longer exists." delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show];
}
}
#pragma mark - UIAlertViewDelegate
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
if (buttonIndex == alertView.cancelButtonIndex) {
// Nothing, just cancel
} else if (buttonIndex == alertView.firstOtherButtonIndex) {
[[NSFileManager defaultManager] removeItemAtPath:self.path error:NULL];
}
}
- (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex
{
[self.delegate fileOperationControllerDidDismiss:self];
}
@end
@interface FLEXFileBrowserFileRenameOperationController () <UIAlertViewDelegate>
@property (nonatomic, copy, readonly) NSString *path;
- (instancetype)initWithPath:(NSString *)path NS_DESIGNATED_INITIALIZER;
@end
@implementation FLEXFileBrowserFileRenameOperationController
@synthesize delegate = _delegate;
- (instancetype)init
{
return [self initWithPath:nil];
}
- (instancetype)initWithPath:(NSString *)path
{
self = [super init];
if (self) {
_path = path;
}
return self;
}
- (void)show
{
BOOL isDirectory = NO;
BOOL stillExists = [[NSFileManager defaultManager] fileExistsAtPath:self.path isDirectory:&isDirectory];
if (stillExists) {
UIAlertView *renameDialog = [[UIAlertView alloc]
initWithTitle:[NSString stringWithFormat:@"Rename %@?", self.path.lastPathComponent]
message:nil
delegate:self
cancelButtonTitle:@"Cancel"
otherButtonTitles:@"Rename", nil];
renameDialog.alertViewStyle = UIAlertViewStylePlainTextInput;
UITextField *textField = [renameDialog textFieldAtIndex:0];
textField.placeholder = @"New file name";
textField.text = self.path.lastPathComponent;
[renameDialog show];
} else {
[[[UIAlertView alloc] initWithTitle:@"File Removed" message:@"The file at the specified path no longer exists." delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show];
}
}
#pragma mark - UIAlertViewDelegate
- (void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex
{
if (buttonIndex == alertView.cancelButtonIndex) {
// Nothing, just cancel
} else if (buttonIndex == alertView.firstOtherButtonIndex) {
NSString *newFileName = [alertView textFieldAtIndex:0].text;
NSString *newPath = [[self.path stringByDeletingLastPathComponent] stringByAppendingPathComponent:newFileName];
[[NSFileManager defaultManager] moveItemAtPath:self.path toPath:newPath error:NULL];
}
}
- (void)alertView:(UIAlertView *)alertView didDismissWithButtonIndex:(NSInteger)buttonIndex
{
[self.delegate fileOperationControllerDidDismiss:self];
}
@end
@@ -1,351 +0,0 @@
//
// FLEXFileBrowserTableViewController.m
// Flipboard
//
// Created by Ryan Olson on 6/9/14.
//
//
#import "FLEXFileBrowserTableViewController.h"
#import "FLEXFileBrowserFileOperationController.h"
#import "FLEXUtility.h"
#import "FLEXWebViewController.h"
#import "FLEXImagePreviewViewController.h"
#import "FLEXTableListViewController.h"
@interface FLEXFileBrowserTableViewCell : UITableViewCell
@end
@interface FLEXFileBrowserTableViewController () <FLEXFileBrowserFileOperationControllerDelegate, FLEXFileBrowserSearchOperationDelegate, UISearchResultsUpdating, UISearchControllerDelegate>
@property (nonatomic, copy) NSString *path;
@property (nonatomic, copy) NSArray *childPaths;
@property (nonatomic, strong) NSArray *searchPaths;
@property (nonatomic, strong) NSNumber *recursiveSize;
@property (nonatomic, strong) NSNumber *searchPathsSize;
@property (nonatomic, strong) UISearchController *searchController;
@property (nonatomic) NSOperationQueue *operationQueue;
@property (nonatomic, strong) UIDocumentInteractionController *documentController;
@property (nonatomic, strong) id<FLEXFileBrowserFileOperationController> fileOperationController;
@end
@implementation FLEXFileBrowserTableViewController
- (id)initWithStyle:(UITableViewStyle)style
{
return [self initWithPath:NSHomeDirectory()];
}
- (id)initWithPath:(NSString *)path
{
self = [super initWithStyle:UITableViewStyleGrouped];
if (self) {
self.path = path;
self.title = [path lastPathComponent];
self.operationQueue = [NSOperationQueue new];
self.searchController = [[UISearchController alloc] initWithSearchResultsController:nil];
self.searchController.searchResultsUpdater = self;
self.searchController.delegate = self;
self.searchController.dimsBackgroundDuringPresentation = NO;
self.tableView.tableHeaderView = self.searchController.searchBar;
//computing path size
FLEXFileBrowserTableViewController *__weak weakSelf = self;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSFileManager *fileManager = [NSFileManager defaultManager];
NSDictionary *attributes = [fileManager attributesOfItemAtPath:path error:NULL];
uint64_t totalSize = [attributes fileSize];
for (NSString *fileName in [fileManager enumeratorAtPath:path]) {
attributes = [fileManager attributesOfItemAtPath:[path stringByAppendingPathComponent:fileName] error:NULL];
totalSize += [attributes fileSize];
// Bail if the interested view controller has gone away.
if (!weakSelf) {
return;
}
}
dispatch_async(dispatch_get_main_queue(), ^{
FLEXFileBrowserTableViewController *__strong strongSelf = weakSelf;
strongSelf.recursiveSize = @(totalSize);
[strongSelf.tableView reloadData];
});
});
[self reloadChildPaths];
}
return self;
}
#pragma mark - UIViewController
- (void)viewDidLoad
{
[super viewDidLoad];
UIMenuItem *renameMenuItem = [[UIMenuItem alloc] initWithTitle:@"Rename" action:@selector(fileBrowserRename:)];
UIMenuItem *deleteMenuItem = [[UIMenuItem alloc] initWithTitle:@"Delete" action:@selector(fileBrowserDelete:)];
[UIMenuController sharedMenuController].menuItems = @[renameMenuItem, deleteMenuItem];
}
#pragma mark - FLEXFileBrowserSearchOperationDelegate
- (void)fileBrowserSearchOperationResult:(NSArray *)searchResult size:(uint64_t)size
{
self.searchPaths = searchResult;
self.searchPathsSize = @(size);
[self.tableView reloadData];
}
#pragma mark - UISearchResultsUpdating
- (void)updateSearchResultsForSearchController:(UISearchController *)searchController
{
[self reloadDisplayedPaths];
}
#pragma mark - UISearchControllerDelegate
- (void)willDismissSearchController:(UISearchController *)searchController
{
[self.operationQueue cancelAllOperations];
[self reloadChildPaths];
[self.tableView reloadData];
}
#pragma mark - Table view data source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.searchController.isActive ? [self.searchPaths count] : [self.childPaths count];
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
BOOL isSearchActive = self.searchController.isActive;
NSNumber *currentSize = isSearchActive ? self.searchPathsSize : self.recursiveSize;
NSArray *currentPaths = isSearchActive ? self.searchPaths : self.childPaths;
NSString *sizeString = nil;
if (!currentSize) {
sizeString = @"Computing size…";
} else {
sizeString = [NSByteCountFormatter stringFromByteCount:[currentSize longLongValue] countStyle:NSByteCountFormatterCountStyleFile];
}
return [NSString stringWithFormat:@"%lu files (%@)", (unsigned long)[currentPaths count], sizeString];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *fullPath = [self filePathAtIndexPath:indexPath];
NSDictionary *attributes = [[NSFileManager defaultManager] attributesOfItemAtPath:fullPath error:NULL];
BOOL isDirectory = [[attributes fileType] isEqual:NSFileTypeDirectory];
NSString *subtitle = nil;
if (isDirectory) {
NSUInteger count = [[[NSFileManager defaultManager] contentsOfDirectoryAtPath:fullPath error:NULL] count];
subtitle = [NSString stringWithFormat:@"%lu file%@", (unsigned long)count, (count == 1 ? @"" : @"s")];
} else {
NSString *sizeString = [NSByteCountFormatter stringFromByteCount:[attributes fileSize] countStyle:NSByteCountFormatterCountStyleFile];
subtitle = [NSString stringWithFormat:@"%@ - %@", sizeString, [attributes fileModificationDate]];
}
static NSString *textCellIdentifier = @"textCell";
static NSString *imageCellIdentifier = @"imageCell";
UITableViewCell *cell = nil;
// Separate image and text only cells because otherwise the separator lines get out-of-whack on image cells reused with text only.
BOOL showImagePreview = [FLEXUtility isImagePathExtension:[fullPath pathExtension]];
NSString *cellIdentifier = showImagePreview ? imageCellIdentifier : textCellIdentifier;
if (!cell) {
cell = [[FLEXFileBrowserTableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:cellIdentifier];
cell.textLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
cell.detailTextLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
cell.detailTextLabel.textColor = [UIColor grayColor];
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
}
NSString *cellTitle = [fullPath lastPathComponent];
cell.textLabel.text = cellTitle;
cell.detailTextLabel.text = subtitle;
if (showImagePreview) {
cell.imageView.contentMode = UIViewContentModeScaleAspectFit;
cell.imageView.image = [UIImage imageWithContentsOfFile:fullPath];
}
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *fullPath = [self filePathAtIndexPath:indexPath];
NSString *subpath = [fullPath lastPathComponent];
NSString *pathExtension = [subpath pathExtension];
BOOL isDirectory = NO;
BOOL stillExists = [[NSFileManager defaultManager] fileExistsAtPath:fullPath isDirectory:&isDirectory];
if (stillExists) {
UIViewController *drillInViewController = nil;
if (isDirectory) {
drillInViewController = [[[self class] alloc] initWithPath:fullPath];
} else if ([FLEXUtility isImagePathExtension:pathExtension]) {
UIImage *image = [UIImage imageWithContentsOfFile:fullPath];
drillInViewController = [[FLEXImagePreviewViewController alloc] initWithImage:image];
} else {
// Special case keyed archives, json, and plists to get more readable data.
NSString *prettyString = nil;
if ([pathExtension isEqual:@"archive"] || [pathExtension isEqual:@"coded"]) {
prettyString = [[NSKeyedUnarchiver unarchiveObjectWithFile:fullPath] description];
} else if ([pathExtension isEqualToString:@"json"]) {
prettyString = [FLEXUtility prettyJSONStringFromData:[NSData dataWithContentsOfFile:fullPath]];
} else if ([pathExtension isEqualToString:@"plist"]) {
NSData *fileData = [NSData dataWithContentsOfFile:fullPath];
prettyString = [[NSPropertyListSerialization propertyListWithData:fileData options:0 format:NULL error:NULL] description];
}
if ([prettyString length] > 0) {
drillInViewController = [[FLEXWebViewController alloc] initWithText:prettyString];
} else if ([FLEXWebViewController supportsPathExtension:pathExtension]) {
drillInViewController = [[FLEXWebViewController alloc] initWithURL:[NSURL fileURLWithPath:fullPath]];
} else if ([FLEXTableListViewController supportsExtension:subpath.pathExtension]) {
drillInViewController = [[FLEXTableListViewController alloc] initWithPath:fullPath];
}
else {
NSString *fileString = [NSString stringWithContentsOfFile:fullPath encoding:NSUTF8StringEncoding error:NULL];
if ([fileString length] > 0) {
drillInViewController = [[FLEXWebViewController alloc] initWithText:fileString];
}
}
}
if (drillInViewController) {
drillInViewController.title = [subpath lastPathComponent];
[self.navigationController pushViewController:drillInViewController animated:YES];
} else {
[self openFileController:fullPath];
[self.tableView deselectRowAtIndexPath:indexPath animated:YES];
}
} else {
[[[UIAlertView alloc] initWithTitle:@"File Removed" message:@"The file at the specified path no longer exists." delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil] show];
[self reloadDisplayedPaths];
}
}
- (BOOL)tableView:(UITableView *)tableView shouldShowMenuForRowAtIndexPath:(NSIndexPath *)indexPath
{
return YES;
}
- (BOOL)tableView:(UITableView *)tableView canPerformAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender
{
return action == @selector(fileBrowserDelete:) || action == @selector(fileBrowserRename:);
}
- (void)tableView:(UITableView *)tableView performAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender
{
// Empty, but has to exist for the menu to show
// The table view only calls this method for actions in the UIResponderStandardEditActions informal protocol.
// Since our actions are outside of that protocol, we need to manually handle the action forwarding from the cells.
}
#pragma mark - FLEXFileBrowserFileOperationControllerDelegate
- (void)fileOperationControllerDidDismiss:(id<FLEXFileBrowserFileOperationController>)controller
{
[self reloadDisplayedPaths];
}
- (void)openFileController:(NSString *)fullPath
{
UIDocumentInteractionController *controller = [UIDocumentInteractionController new];
controller.URL = [[NSURL alloc] initFileURLWithPath:fullPath];
[controller presentOptionsMenuFromRect:self.view.bounds inView:self.view animated:YES];
self.documentController = controller;
}
- (void)fileBrowserRename:(UITableViewCell *)sender
{
NSIndexPath *indexPath = [self.tableView indexPathForCell:sender];
NSString *fullPath = [self filePathAtIndexPath:indexPath];
self.fileOperationController = [[FLEXFileBrowserFileRenameOperationController alloc] initWithPath:fullPath];
self.fileOperationController.delegate = self;
[self.fileOperationController show];
}
- (void)fileBrowserDelete:(UITableViewCell *)sender
{
NSIndexPath *indexPath = [self.tableView indexPathForCell:sender];
NSString *fullPath = [self filePathAtIndexPath:indexPath];
self.fileOperationController = [[FLEXFileBrowserFileDeleteOperationController alloc] initWithPath:fullPath];
self.fileOperationController.delegate = self;
[self.fileOperationController show];
}
- (void)reloadDisplayedPaths
{
if (self.searchController.isActive) {
[self reloadSearchPaths];
} else {
[self reloadChildPaths];
}
[self.tableView reloadData];
}
- (void)reloadChildPaths
{
NSMutableArray *childPaths = [NSMutableArray array];
NSArray *subpaths = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:self.path error:NULL];
for (NSString *subpath in subpaths) {
[childPaths addObject:[self.path stringByAppendingPathComponent:subpath]];
}
self.childPaths = childPaths;
}
- (void)reloadSearchPaths
{
self.searchPaths = nil;
self.searchPathsSize = nil;
//clear pre search request and start a new one
[self.operationQueue cancelAllOperations];
FLEXFileBrowserSearchOperation *newOperation = [[FLEXFileBrowserSearchOperation alloc] initWithPath:self.path searchString:self.searchController.searchBar.text];
newOperation.delegate = self;
[self.operationQueue addOperation:newOperation];
}
- (NSString *)filePathAtIndexPath:(NSIndexPath *)indexPath
{
return self.searchController.isActive ? self.searchPaths[indexPath.row] : self.childPaths[indexPath.row];
}
@end
@implementation FLEXFileBrowserTableViewCell
- (void)fileBrowserRename:(UIMenuController *)sender
{
id target = [self.nextResponder targetForAction:_cmd withSender:sender];
[[UIApplication sharedApplication] sendAction:_cmd to:target from:self forEvent:nil];
}
- (void)fileBrowserDelete:(UIMenuController *)sender
{
id target = [self.nextResponder targetForAction:_cmd withSender:sender];
[[UIApplication sharedApplication] sendAction:_cmd to:target from:self forEvent:nil];
}
@end
@@ -1,27 +0,0 @@
//
// FLEXGlobalsTableViewController.h
// Flipboard
//
// Created by Ryan Olson on 2014-05-03.
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
@protocol FLEXGlobalsTableViewControllerDelegate;
@interface FLEXGlobalsTableViewController : UITableViewController
@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;
@end
@@ -1,306 +0,0 @@
//
// FLEXGlobalsTableViewController.m
// Flipboard
//
// Created by Ryan Olson on 2014-05-03.
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import "FLEXGlobalsTableViewController.h"
#import "FLEXUtility.h"
#import "FLEXLibrariesTableViewController.h"
#import "FLEXClassesTableViewController.h"
#import "FLEXObjectExplorerViewController.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXLiveObjectsTableViewController.h"
#import "FLEXFileBrowserTableViewController.h"
#import "FLEXCookiesTableViewController.h"
#import "FLEXGlobalsTableViewControllerEntry.h"
#import "FLEXManager+Private.h"
#import "FLEXSystemLogTableViewController.h"
#import "FLEXNetworkHistoryTableViewController.h"
static __weak UIWindow *s_applicationWindow = nil;
typedef NS_ENUM(NSUInteger, FLEXGlobalsRow) {
FLEXGlobalsRowNetworkHistory,
FLEXGlobalsRowSystemLog,
FLEXGlobalsRowLiveObjects,
FLEXGlobalsRowFileBrowser,
FLEXGlobalsCookies,
FLEXGlobalsRowSystemLibraries,
FLEXGlobalsRowAppClasses,
FLEXGlobalsRowAppDelegate,
FLEXGlobalsRowRootViewController,
FLEXGlobalsRowUserDefaults,
FLEXGlobalsRowApplication,
FLEXGlobalsRowKeyWindow,
FLEXGlobalsRowMainScreen,
FLEXGlobalsRowCurrentDevice,
FLEXGlobalsRowCount
};
@interface FLEXGlobalsTableViewController ()
/// [FLEXGlobalsTableViewControllerEntry *]
@property (nonatomic, readonly, copy) NSArray *entries;
@end
@implementation FLEXGlobalsTableViewController
/// [FLEXGlobalsTableViewControllerEntry *]
+ (NSArray *)defaultGlobalEntries
{
NSMutableArray *defaultGlobalEntries = [NSMutableArray array];
for (FLEXGlobalsRow defaultRowIndex = 0; defaultRowIndex < FLEXGlobalsRowCount; defaultRowIndex++) {
FLEXGlobalsTableViewControllerEntryNameFuture titleFuture = nil;
FLEXGlobalsTableViewControllerViewControllerFuture viewControllerFuture = nil;
switch (defaultRowIndex) {
case FLEXGlobalsRowAppClasses:
titleFuture = ^NSString *{
return [NSString stringWithFormat:@"📕 %@ Classes", [FLEXUtility applicationName]];
};
viewControllerFuture = ^UIViewController *{
FLEXClassesTableViewController *classesViewController = [[FLEXClassesTableViewController alloc] init];
classesViewController.binaryImageName = [FLEXUtility applicationImageName];
return classesViewController;
};
break;
case FLEXGlobalsRowSystemLibraries: {
NSString *titleString = @"📚 System Libraries";
titleFuture = ^NSString *{
return titleString;
};
viewControllerFuture = ^UIViewController *{
FLEXLibrariesTableViewController *librariesViewController = [[FLEXLibrariesTableViewController alloc] init];
librariesViewController.title = titleString;
return librariesViewController;
};
break;
}
case FLEXGlobalsRowLiveObjects:
titleFuture = ^NSString *{
return @"💩 Heap Objects";
};
viewControllerFuture = ^UIViewController *{
return [[FLEXLiveObjectsTableViewController alloc] init];
};
break;
case FLEXGlobalsRowAppDelegate:
titleFuture = ^NSString *{
return [NSString stringWithFormat:@"👉 %@", [[[UIApplication sharedApplication] delegate] class]];
};
viewControllerFuture = ^UIViewController *{
id <UIApplicationDelegate> appDelegate = [[UIApplication sharedApplication] delegate];
return [FLEXObjectExplorerFactory explorerViewControllerForObject:appDelegate];
};
break;
case FLEXGlobalsRowRootViewController:
titleFuture = ^NSString *{
return [NSString stringWithFormat:@"🌴 %@", [[s_applicationWindow rootViewController] class]];
};
viewControllerFuture = ^UIViewController *{
UIViewController *rootViewController = [s_applicationWindow rootViewController];
return [FLEXObjectExplorerFactory explorerViewControllerForObject:rootViewController];
};
break;
case FLEXGlobalsRowUserDefaults:
titleFuture = ^NSString *{
return @"🚶 +[NSUserDefaults standardUserDefaults]";
};
viewControllerFuture = ^UIViewController *{
NSUserDefaults *standardUserDefaults = [NSUserDefaults standardUserDefaults];
return [FLEXObjectExplorerFactory explorerViewControllerForObject:standardUserDefaults];
};
break;
case FLEXGlobalsRowApplication:
titleFuture = ^NSString *{
return @"💾 +[UIApplication sharedApplication]";
};
viewControllerFuture = ^UIViewController *{
UIApplication *sharedApplication = [UIApplication sharedApplication];
return [FLEXObjectExplorerFactory explorerViewControllerForObject:sharedApplication];
};
break;
case FLEXGlobalsRowKeyWindow:
titleFuture = ^NSString *{
return @"🔑 -[UIApplication keyWindow]";
};
viewControllerFuture = ^UIViewController *{
return [FLEXObjectExplorerFactory explorerViewControllerForObject:s_applicationWindow];
};
break;
case FLEXGlobalsRowMainScreen:
titleFuture = ^NSString *{
return @"💻 +[UIScreen mainScreen]";
};
viewControllerFuture = ^UIViewController *{
UIScreen *mainScreen = [UIScreen mainScreen];
return [FLEXObjectExplorerFactory explorerViewControllerForObject:mainScreen];
};
break;
case FLEXGlobalsRowCurrentDevice:
titleFuture = ^NSString *{
return @"📱 +[UIDevice currentDevice]";
};
viewControllerFuture = ^UIViewController *{
UIDevice *currentDevice = [UIDevice currentDevice];
return [FLEXObjectExplorerFactory explorerViewControllerForObject:currentDevice];
};
break;
case FLEXGlobalsCookies:
titleFuture = ^NSString *{
return @"🍪 Cookies";
};
viewControllerFuture = ^UIViewController *{
return [[FLEXCookiesTableViewController alloc] init];
};
break;
case FLEXGlobalsRowFileBrowser:
titleFuture = ^NSString *{
return @"📁 File Browser";
};
viewControllerFuture = ^UIViewController *{
return [[FLEXFileBrowserTableViewController alloc] init];
};
break;
case FLEXGlobalsRowSystemLog:
titleFuture = ^{
return @"⚠️ System Log";
};
viewControllerFuture = ^{
return [[FLEXSystemLogTableViewController alloc] init];
};
break;
case FLEXGlobalsRowNetworkHistory:
titleFuture = ^{
return @"📡 Network History";
};
viewControllerFuture = ^{
return [[FLEXNetworkHistoryTableViewController alloc] init];
};
break;
case FLEXGlobalsRowCount:
break;
}
NSParameterAssert(titleFuture);
NSParameterAssert(viewControllerFuture);
[defaultGlobalEntries addObject:[FLEXGlobalsTableViewControllerEntry entryWithNameFuture:titleFuture viewControllerFuture:viewControllerFuture]];
}
return defaultGlobalEntries;
}
- (id)initWithStyle:(UITableViewStyle)style
{
self = [super initWithStyle:style];
if (self) {
self.title = @"💪 FLEX";
_entries = [[[self class] defaultGlobalEntries] arrayByAddingObjectsFromArray:[FLEXManager sharedManager].userGlobalEntries];
}
return self;
}
#pragma mark - Public
+ (void)setApplicationWindow:(UIWindow *)applicationWindow
{
s_applicationWindow = applicationWindow;
}
#pragma mark - UIViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemDone target:self action:@selector(donePressed:)];
}
#pragma mark -
- (void)donePressed:(id)sender
{
[self.delegate globalsViewControllerDidFinish:self];
}
#pragma mark - Table Data Helpers
- (FLEXGlobalsTableViewControllerEntry *)globalEntryAtIndexPath:(NSIndexPath *)indexPath
{
return self.entries[indexPath.row];
}
- (NSString *)titleForRowAtIndexPath:(NSIndexPath *)indexPath
{
FLEXGlobalsTableViewControllerEntry *entry = [self globalEntryAtIndexPath:indexPath];
return entry.entryNameFuture();
}
- (UIViewController *)viewControllerToPushForRowAtIndexPath:(NSIndexPath *)indexPath
{
FLEXGlobalsTableViewControllerEntry *entry = [self globalEntryAtIndexPath:indexPath];
return entry.viewControllerFuture();
}
#pragma mark - Table View Data Source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [self.entries 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 defaultFontOfSize:14.0];
}
cell.textLabel.text = [self titleForRowAtIndexPath:indexPath];
return cell;
}
#pragma mark - Table View Delegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
UIViewController *viewControllerToPush = [self viewControllerToPushForRowAtIndexPath:indexPath];
[self.navigationController pushViewController:viewControllerToPush animated:YES];
}
@end
@@ -1,130 +0,0 @@
//
// FLEXInstancesTableViewController.m
// Flipboard
//
// Created by Ryan Olson on 5/28/14.
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import "FLEXInstancesTableViewController.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXObjectExplorerViewController.h"
#import "FLEXRuntimeUtility.h"
#import "FLEXUtility.h"
#import "FLEXHeapEnumerator.h"
#import <malloc/malloc.h>
@interface FLEXInstancesTableViewController ()
@property (nonatomic, strong) NSArray *instances;
@property (nonatomic, strong) NSArray *fieldNames;
@end
@implementation FLEXInstancesTableViewController
+ (instancetype)instancesTableViewControllerForClassName:(NSString *)className
{
const char *classNameCString = [className UTF8String];
NSMutableArray *instances = [NSMutableArray array];
[FLEXHeapEnumerator enumerateLiveObjectsUsingBlock:^(__unsafe_unretained id object, __unsafe_unretained Class actualClass) {
if (strcmp(classNameCString, class_getName(actualClass)) == 0) {
// Note: objects of certain classes crash when retain is called. It is up to the user to avoid tapping into instance lists for these classes.
// Ex. OS_dispatch_queue_specific_queue
// In the future, we could provide some kind of warning for classes that are known to be problematic.
if (malloc_size((__bridge const void *)(object)) > 0) {
[instances addObject:object];
}
}
}];
FLEXInstancesTableViewController *instancesViewController = [[self alloc] init];
instancesViewController.instances = instances;
instancesViewController.title = [NSString stringWithFormat:@"%@ (%lu)", className, (unsigned long)[instances count]];
return instancesViewController;
}
+ (instancetype)instancesTableViewControllerForInstancesReferencingObject:(id)object
{
NSMutableArray *instances = [NSMutableArray array];
NSMutableArray *fieldNames = [NSMutableArray array];
[FLEXHeapEnumerator enumerateLiveObjectsUsingBlock:^(__unsafe_unretained id tryObject, __unsafe_unretained Class actualClass) {
// Get all the ivars on the object. Start with the class and and travel up the inheritance chain.
// Once we find a match, record it and move on to the next object. There's no reason to find multiple matches within the same object.
Class tryClass = actualClass;
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] == @encode(id)[0] || typeEncoding[0] == @encode(Class)[0]) {
ptrdiff_t offset = ivar_getOffset(ivar);
uintptr_t *fieldPointer = (__bridge void *)tryObject + offset;
if (*fieldPointer == (uintptr_t)(__bridge void *)object) {
[instances addObject:tryObject];
[fieldNames addObject:@(ivar_getName(ivar))];
return;
}
}
}
tryClass = class_getSuperclass(tryClass);
}
}];
FLEXInstancesTableViewController *instancesViewController = [[self alloc] init];
instancesViewController.instances = instances;
instancesViewController.fieldNames = fieldNames;
instancesViewController.title = [NSString stringWithFormat:@"Referencing %@ %p", NSStringFromClass(object_getClass(object)), object];
return instancesViewController;
}
#pragma mark - Table View Data Source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [self.instances count];
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = @"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
UIFont *cellFont = [FLEXUtility defaultTableViewCellLabelFont];
cell.textLabel.font = cellFont;
cell.detailTextLabel.font = cellFont;
cell.detailTextLabel.textColor = [UIColor grayColor];
}
id instance = self.instances[indexPath.row];
NSString *title = nil;
if ((NSInteger)[self.fieldNames count] > indexPath.row) {
title = [NSString stringWithFormat:@"%@ %@", NSStringFromClass(object_getClass(instance)), self.fieldNames[indexPath.row]];
} else {
title = [NSString stringWithFormat:@"%@ %p", NSStringFromClass(object_getClass(instance)), instance];
}
cell.textLabel.text = title;
cell.detailTextLabel.text = [FLEXRuntimeUtility descriptionForIvarOrPropertyValue:instance];
return cell;
}
#pragma mark - Table View Delegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
id instance = self.instances[indexPath.row];
FLEXObjectExplorerViewController *drillInViewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:instance];
[self.navigationController pushViewController:drillInViewController animated:YES];
}
@end

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