Compare commits

...

317 Commits

Author SHA1 Message Date
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
Ryan Olson e40054ba1a Merge pull request #142 from shepting/patch-1
Fix typo pruged -> pruned
2016-09-30 16:42:58 -07:00
Ryan Olson 1761734447 Merge pull request #149 from c0diq/xcode8
Add Xcode8 support
2016-09-30 16:41:54 -07:00
Ryan Olson f23ee3cd95 Merge pull request #151 from Blankdlh/master
Fix method swizzling for NSURLSessionDelegate
2016-09-30 16:38:55 -07:00
kunka e455ac0c7d Fix swizzling for URLSession:task:willPerformHTTPRedirection:newRequest:completionHandler: and URLSession:dataTask:didReceiveResponse:completionHandler: 2016-09-29 18:10:31 +08:00
Sylvain Rebaud 94f68c6dfe Add Xcode8 support 2016-09-26 15:15:36 -07:00
Steven Hepting a22f022014 Fix typo. 2016-08-15 08:17:07 -07:00
Ryan Olson 22e7edb698 Bring back the memory reader for heap enumeration
Some malloc replacements don't provide a default reader
2016-08-09 08:55:54 -07:00
Ryan Olson 52fcda53c5 Merge pull request #138 from iampgzp/master
Added curl logger for debugging request
2016-08-01 10:15:44 -07:00
Ji Pei be6c5d0e43 Removed curl log in recordRequestWillBeSentWithRequestID to avoid too much logging spew 2016-08-01 12:49:32 -04:00
Ryan Olson 6ed0037f50 Merge pull request #134 from ThePantsThief/master
Search for a specific class
2016-07-29 15:35:49 -07:00
Ryan Olson 81e3a5ff47 Merge pull request #132 from revolter/patch-1
Update FLEXRuntimeUtility.m
2016-07-29 15:27:32 -07:00
Ji Pei 2a6e28c9d0 Added curl logger for debugging request 2016-07-27 16:52:47 -04:00
ThePantsThief 9e928b0b09 Fix incorrect strcmp check 2016-07-22 12:49:17 -05:00
Tanner Bennett d5deaad628 Search for a specific class
Added a way to jump to a specific class from the System Libraries tab,
since it is sometimes difficult to find a class if you don’t know which
bundle it’s in.

Also fixed a few other things that bugged me, like manually getting the
last path component of a string instead of using the lastPathComponent
property…
2016-07-06 15:04:39 -05:00
Iulian Onofrei 232ae8a6fd Update FLEXRuntimeUtility.m
Fix typos
2016-06-21 10:46:57 +03:00
Ryan Olson c766e5d94a Merge pull request #128 from Flipboard/timoliver-realm-path-fix
Replaced deprecated 'path' property in Realm configuration
2016-05-20 06:15:30 -07:00
Tim Oliver 5f27a2304b Replaced 'path' property with 'fileURL' in Realm configuration 2016-05-20 15:21:24 +08:00
Ryan Olson 0cb0f44f18 Update version in .podspec 2016-02-28 22:18:38 -08:00
Ryan Olson d1c1aa0a26 Fix xcodebuild error from incorrect method resolution 2016-02-28 22:15:07 -08:00
Ryan Olson 832957f621 Merge pull request #114 from SoXeon/master
fix crash when object malloc_size no greater than zero
2016-02-28 21:52:30 -08:00
Ryan Olson 24985ac984 Fix crash on calling _keyCode on events from the simulator keyboard 2016-02-28 21:43:39 -08:00
dazi.dp 0b652c2f2a add malloc.h 2016-02-25 22:31:47 +08:00
dazi.dp 98d83bb438 fix crash when object malloc_size no greater than zero 2016-02-25 22:20:22 +08:00
Tim Oliver e13717b056 Merge pull request #112 from TimOliver/timoliver-realm-null-fix
Added handling for nil Realm property values.
2016-02-23 22:12:57 +08:00
Tim Oliver 552e687b9c Added handling for nil Realm property values 2016-02-23 22:08:51 +08:00
Ryan Olson fb29421644 Merge pull request #110 from TimOliver/patch-1
Updated database features in README.
2016-02-22 22:08:27 -08:00
Tim Oliver f5d930bd58 Updated database features in README. 2016-02-22 02:10:15 +08:00
Ryan Olson 26af4ef476 Merge pull request #105 from TimOliver/timoliver-realm-integration
Added Realm Database File Introspection Alongside SQLite
2016-02-21 09:39:55 -08:00
Ryan Olson ac8940da26 Merge pull request #107 from nin9tyfour/master
*Added the ability to determine a UIView's view controller without selection the view controller's view delegate.
2016-02-16 20:24:36 -08:00
Tim Oliver f597152a62 Cleaned up example project. 2016-02-17 11:35:39 +08:00
Tim Oliver 58f94f108c Made protocol required. 2016-02-17 11:34:54 +08:00
Tim Oliver a5e0bbd50e Added better extension-checking logic. 2016-02-17 11:34:36 +08:00
Tim Oliver ac273fbfc9 Added test Realm file and classes to the sample app. 2016-02-17 11:20:17 +08:00
Tim Oliver 548fd03bd5 Made Realm integration a dynamic runtime check. 2016-02-17 11:19:17 +08:00
nin9tyfour b564c25d2a *Added the ability to determine a UIView's view controller without selection the view controller's view delegate. 2016-02-16 15:29:52 +11:00
Ryan Olson bd821dc553 Restrict simulator force touch support to iOS 9+ 2016-02-13 13:44:00 -08:00
Ryan Olson e46a33417b Merge pull request #106 from ReadmeCritic/master
Correct the spelling of CocoaPods in README
2016-02-13 13:36:37 -08:00
ReadmeCritic a3419a841f Correct the spelling of CocoaPods in README 2016-02-11 15:46:43 -08:00
Tim Oliver cafc1ba0bd Reset UICatalog project. 2016-02-09 14:16:54 +08:00
Tim Oliver 0112c097d9 Updated Realm optional macros and cleaned project changes. 2016-02-09 14:10:52 +08:00
Tim Oliver 507d03fd90 Cleaned up project file, and added test Realm file. 2016-02-07 16:47:31 +08:00
Tim Oliver b6453ac360 Made references to Realm build optional. 2016-02-06 15:36:49 +08:00
Tim Oliver b693ceb20e Cleaned up protocol logic. 2016-02-06 14:42:19 +08:00
Ryan Olson 31e81a616d Merge pull request #104 from Flipboard/dzc-add-contributing
Add contributing stub
2016-01-30 15:13:42 -08:00
David Creemer 928f60b56f Add contributing stub 2016-01-28 13:19:10 -08:00
Tim Oliver 12a1900d75 Abstracted database parser logic and added framework for Realm parser. 2016-01-28 12:50:39 +08:00
Ryan Olson c72b6f7e5b Merge pull request #102 from pra85/patch-1
Update license year range to 2016
2016-01-18 11:43:12 -08:00
Prayag Verma 3cf9f72dcb Update license year range to 2016 2016-01-18 14:12:03 +05:30
Ryan Olson 9b1318e975 Project structure cleanup 2016-01-17 20:52:34 -08:00
Ryan Olson d3d0f04c23 Heap scan cleanup 2016-01-17 19:54:38 -08:00
Ryan Olson b606d04944 Fix keyboard shortcuts firing when typing into an alert view 2016-01-11 13:27:15 -08:00
Ryan Olson 7afd50d241 Remove asl hacks needed for iOS 7 2016-01-08 15:01:42 -08:00
Ryan Olson 94f06d5ff8 Fix heap search/enumeration crashes! 2016-01-06 17:43:01 -08:00
Ryan Olson 85424fd15e Support viewing AFNetworking request bodies 2016-01-05 15:10:10 -08:00
Ryan Olson cff391f78c Use NSKeyedArchiver for files with .coded extensions 2015-12-18 19:38:32 -05:00
Ryan Olson 3e420bb747 Bump version in .podspec 2015-12-14 08:56:10 -08:00
Ryan Olson 8aece0a266 Update README 2015-12-14 08:31:42 -08:00
Ryan Olson 81b27b6918 Modify approach to toggling views and menu modals.
This approach seems to eliminate some of the status bar issues we were seeing.
2015-12-13 23:21:43 -08:00
Ryan Olson 727943c4b3 Merge pull request #92 from WangHengHeng/master
- (void)toggle***Tool - It is not necessary to 'dismiss' when 'present'
2015-12-13 23:12:41 -08:00
Ryan Olson 9f2c032157 List FLEX.h in the public headers in the podspec 2015-12-13 23:06:40 -08:00
Ryan Olson d6a5b1af8d Bump podspec iOS version to 8.0 2015-12-13 23:03:09 -08:00
Ryan Olson dda9dd5beb Move to UISearchController in FLEXSystemLogTableViewController 2015-12-13 22:55:57 -08:00
Ryan Olson 888887f09a Move to UISearchController in FLEXNetworkHistoryTableViewController 2015-12-13 22:46:46 -08:00
Ryan Olson b70a1a2f48 Move to UISearchController in FLEXFileBrowserTableViewController 2015-12-13 22:15:45 -08:00
Ryan Olson 54730c368c Use FLEX.framework in example project
Bump example project deployment target to iOS 8 so we can link to the dynamic framework.
2015-12-13 21:09:20 -08:00
Ryan Olson 21672e6f8d Merge pull request #93 from tttpeng/master
Add a sqlite database browser
2015-12-12 19:11:12 -08:00
Taavo 4ffc992872 Fix some problems about database browser 2015-12-06 04:32:26 +08:00
tttpeng 8eea2ec652 Remove useless methods, About sqlite browser 2015-12-02 12:00:40 +08:00
王 原闯 3df01ee7bb dismiss previous present viewController before present a new one 2015-12-02 10:53:55 +08:00
Ryan Olson d0ad6e4319 Merge pull request #94 from untouchable741/master
Support searching for view pointer address in FLEXHierarchyTableViewC…
2015-12-01 08:26:43 -08:00
Tai Vuong 37aec6dacc Support searching for view pointer address in FLEXHierarchyTableViewController 2015-12-01 22:32:24 +07:00
王 原闯 cdc5aae4b7 - (void)toggle***Tool - It is not necessary to 'dismiss' when 'present' 2015-11-30 18:01:19 +08:00
tttpeng fd2b89fd24 Add sqlite database browser 2015-11-30 17:36:41 +08:00
Ryan Olson f1683e54c3 Add support for force touch in the simulator 2015-11-16 11:37:57 -08:00
Ryan Olson c66dd2e7d3 Merge pull request #85 from dlo/master
Use Objective-C 2.0 subscripting
2015-11-09 06:29:55 -08:00
Ryan Olson 5a5b921bbf Merge pull request #89 from revolter/patch-1
Update FLEXManager.m
2015-11-09 06:27:26 -08:00
Iulian Onofrei 30cc65bd9d Update FLEXManager.m
Fix help screen typo
2015-11-09 11:18:29 +02:00
Dan Loewenherz 29a45aa02d use Objective-C 2.0 subscripting for dictionaries 2015-10-31 17:37:45 -05:00
Dan Loewenherz 08b25ea8d3 use Objective-C 2.0 subscripting for arrays 2015-10-31 17:35:47 -05:00
Ryan Olson 7ffcb83563 Bump version in FLEX.podspec 2015-10-28 20:05:54 -07:00
285 changed files with 13110 additions and 5302 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
+3
View File
@@ -0,0 +1,3 @@
# Contributing to FLEX #
We welcome contributions! Please open a pull request with your changes.
+15
View File
@@ -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
+91
View File
@@ -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
+20
View File
@@ -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
+207
View File
@@ -0,0 +1,207 @@
//
// 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) NSLayoutConstraint *heightConstraint;
@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;
_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.translatesAutoresizingMaskIntoConstraints = NO;
self.collectionView.translatesAutoresizingMaskIntoConstraints = NO;
[self.centerXAnchor constraintEqualToAnchor:self.superview.centerXAnchor].active = YES;
[self.widthAnchor constraintEqualToAnchor:self.superview.widthAnchor].active = YES;
[self.topAnchor constraintEqualToAnchor:self.superview.topAnchor].active = YES;
[self.collectionView pinEdgesToSuperview];
_heightConstraint = [self.heightAnchor constraintEqualToConstant:100];
self.heightConstraint.active = YES;
self.constraintsInstalled = YES;
}
self.heightConstraint.constant = [self.sizingCell systemLayoutSizeFittingSize:UILayoutFittingCompressedSize].height;
[super updateConstraints];
}
#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
+96
View File
@@ -0,0 +1,96 @@
//
// FLEXTableViewController.h
// FLEX
//
// Created by Tanner on 7/5/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
@class FLEXScopeCarousel;
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;
@interface FLEXTableViewController : UITableViewController <UISearchResultsUpdating, UISearchControllerDelegate, UISearchBarDelegate>
/// 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.
/// Make sure your subclass conforms to UISearchControllerDelegate.
/// The search bar will not dim the background or hide the navigation bar by default.
/// On iOS 11 and up, the search bar will appear in the navigation bar below the title.
@property (nonatomic) UISearchController *searchController;
/// Used to initialize the search controller. Defaults to nil.
@property (nonatomic) UIViewController *searchResultsController;
/// Defaults to "Fast"
///
/// Determines how often search bar results will be "debounced."
/// Empty query events are always sent instantly. Query events will
/// be sent when the user has not changed the query for this interval.
@property (nonatomic) FLEXDebounceInterval searchBarDebounceInterval;
/// Whether the search bar stays at the top of the view while scrolling.
///
/// Calls into self.navigationItem.hidesSearchBarWhenScrolling.
/// Do not change self.navigationItem.hidesSearchBarWhenScrolling directly,
/// or it will not be respsected. Use this instead.
/// 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, readonly) NSInteger selectedScope;
/// self.searchController.searchBar.text
@property (nonatomic, readonly) NSString *searchText;
/// 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;
@end
+285
View File
@@ -0,0 +1,285 @@
//
// FLEXTableViewController.m
// FLEX
//
// Created by Tanner on 7/5/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import "FLEXTableViewController.h"
#import "FLEXScopeCarousel.h"
#import "FLEXTableView.h"
#import "FLEXUtility.h"
#import <objc/runtime.h>
@interface Block : NSObject
- (void)invoke;
@end
CGFloat const kFLEXDebounceInstant = 0.f;
CGFloat const kFLEXDebounceFast = 0.05;
CGFloat const kFLEXDebounceForAsyncSearch = 0.15;
CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
@interface FLEXTableViewController ()
@property (nonatomic) NSTimer *debounceTimer;
@property (nonatomic) BOOL didInitiallyRevealSearchBar;
@end
@implementation FLEXTableViewController
@synthesize automaticallyShowsSearchBarCancelButton = _automaticallyShowsSearchBarCancelButton;
#pragma mark - Public
- (id)init {
#if FLEX_AT_LEAST_IOS13_SDK
if (@available(iOS 13.0, *)) {
self = [self initWithStyle:UITableViewStyleInsetGrouped];
} else {
self = [self initWithStyle:UITableViewStyleGrouped];
}
#else
self = [self initWithStyle:UITableViewStyleGrouped];
#endif
return self;
}
- (id)initWithStyle:(UITableViewStyle)style {
self = [super initWithStyle:style];
if (self) {
_searchBarDebounceInterval = kFLEXDebounceFast;
_showSearchBarInitially = YES;
}
return self;
}
- (void)setShowsSearchBar:(BOOL)showsSearchBar {
if (_showsSearchBar == showsSearchBar) return;
_showsSearchBar = showsSearchBar;
UIViewController *results = self.searchResultsController;
self.searchController = [[UISearchController alloc] initWithSearchResultsController:results];
self.searchController.searchBar.placeholder = @"Filter";
self.searchController.searchResultsUpdater = (id)self;
self.searchController.delegate = (id)self;
self.searchController.dimsBackgroundDuringPresentation = NO;
self.searchController.hidesNavigationBarDuringPresentation = NO;
/// Not necessary in iOS 13; remove this when iOS 13 is the minimum deployment target
self.searchController.searchBar.delegate = self;
self.automaticallyShowsSearchBarCancelButton = YES;
#if FLEX_AT_LEAST_IOS13_SDK
if (@available(iOS 13, *)) {
self.searchController.automaticallyShowsScopeBar = NO;
}
#endif
if (@available(iOS 11.0, *)) {
self.navigationItem.searchController = self.searchController;
} else {
self.tableView.tableHeaderView = self.searchController.searchBar;
}
}
- (void)setShowsCarousel:(BOOL)showsCarousel {
if (_showsCarousel == showsCarousel) return;
_showsCarousel = showsCarousel;
_carousel = ({
__weak __typeof(self) weakSelf = self;
FLEXScopeCarousel *carousel = [FLEXScopeCarousel new];
carousel.selectedIndexChangedAction = ^(NSInteger idx) {
__typeof(self) self = weakSelf;
[self updateSearchResults:self.searchText];
};
self.tableView.tableHeaderView = carousel;
[self.tableView layoutIfNeeded];
// UITableView won't update the header size unless you reset the header view
[carousel registerBlockForDynamicTypeChanges:^(FLEXScopeCarousel *carousel) {
__typeof(self) self = weakSelf;
self.tableView.tableHeaderView = carousel;
[self.tableView layoutIfNeeded];
}];
carousel;
});
}
- (NSInteger)selectedScope {
if (self.searchController.searchBar.showsScopeBar) {
return self.searchController.searchBar.selectedScopeButtonIndex;
} else if (self.showsCarousel) {
return self.carousel.selectedIndex;
} else {
return NSNotFound;
}
}
- (NSString *)searchText {
return self.searchController.searchBar.text;
}
- (BOOL)automaticallyShowsSearchBarCancelButton {
#if FLEX_AT_LEAST_IOS13_SDK
if (@available(iOS 13, *)) {
return self.searchController.automaticallyShowsCancelButton;
}
#endif
return _automaticallyShowsSearchBarCancelButton;
}
- (void)setAutomaticallyShowsSearchBarCancelButton:(BOOL)value {
#if FLEX_AT_LEAST_IOS13_SDK
if (@available(iOS 13, *)) {
self.searchController.automaticallyShowsCancelButton = value;
}
#endif
_automaticallyShowsSearchBarCancelButton = value;
}
- (void)updateSearchResults:(NSString *)newText { }
- (void)onBackgroundQueue:(NSArray *(^)(void))backgroundBlock thenOnMainQueue:(void(^)(NSArray *))mainBlock {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSArray *items = backgroundBlock();
dispatch_async(dispatch_get_main_queue(), ^{
mainBlock(items);
});
});
}
#pragma mark - View Controller Lifecycle
- (void)viewDidLoad {
[super viewDidLoad];
self.tableView.keyboardDismissMode = UIScrollViewKeyboardDismissModeOnDrag;
// On iOS 13, the root view controller shows it's search bar no matter what.
// Turning this off avoids some weird flash the navigation bar does when we
// toggle navigationItem.hidesSearchBarWhenScrolling on and off. The flash
// will still happen on subsequent view controllers, but we can at least
// avoid it for the root view controller
if (@available(iOS 13, *)) {
if (self.navigationController.viewControllers.firstObject == self) {
_showSearchBarInitially = NO;
}
}
}
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
// When going back, make the search bar reappear instead of hiding
if (@available(iOS 11.0, *)) {
if ((self.pinSearchBar || self.showSearchBarInitially) && !self.didInitiallyRevealSearchBar) {
self.navigationItem.hidesSearchBarWhenScrolling = NO;
}
}
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
// Allow scrolling to collapse the search bar, only if we don't want it pinned
if (@available(iOS 11.0, *)) {
if (self.showSearchBarInitially && !self.pinSearchBar && !self.didInitiallyRevealSearchBar) {
// All this mumbo jumbo is necessary to work around a bug in iOS 13 up to 13.2
// wherein quickly toggling navigationItem.hidesSearchBarWhenScrolling to make
// the search bar appear initially results in a bugged search bar that
// becomes transparent and floats over the screen as you scroll
[UIView animateWithDuration:0.2 animations:^{
self.navigationItem.hidesSearchBarWhenScrolling = YES;
[self.navigationController.view setNeedsLayout];
[self.navigationController.view layoutIfNeeded];
}];
}
}
// We only want to reveal the search bar when the view controller first appears.
self.didInitiallyRevealSearchBar = YES;
}
- (void)willMoveToParentViewController:(UIViewController *)parent {
[super willMoveToParentViewController:parent];
// Reset this since we are re-appearing under a new
// parent view controller and need to show it again
self.didInitiallyRevealSearchBar = NO;
}
#pragma mark - Private
- (void)debounce:(void(^)(void))block {
[self.debounceTimer invalidate];
self.debounceTimer = [NSTimer
scheduledTimerWithTimeInterval:self.searchBarDebounceInterval
target:block
selector:@selector(invoke)
userInfo:nil
repeats:NO
];
}
#pragma mark - Search Bar
#pragma mark UISearchResultsUpdating
- (void)updateSearchResultsForSearchController:(UISearchController *)searchController
{
[self.debounceTimer invalidate];
NSString *text = searchController.searchBar.text;
// Only debounce if we want to, and if we have a non-empty string
// Empty string events are sent instantly
if (text.length && self.searchBarDebounceInterval > kFLEXDebounceInstant) {
[self debounce:^{
[self updateSearchResults:text];
}];
} else {
[self updateSearchResults:text];
}
}
#pragma mark UISearchControllerDelegate
- (void)willPresentSearchController:(UISearchController *)searchController {
// Manually show cancel button for < iOS 13
if (!@available(iOS 13, *) && self.automaticallyShowsSearchBarCancelButton) {
[searchController.searchBar setShowsCancelButton:YES animated:YES];
}
}
- (void)willDismissSearchController:(UISearchController *)searchController {
// Manually hide cancel button for < iOS 13
if (!@available(iOS 13, *) && self.automaticallyShowsSearchBarCancelButton) {
[searchController.searchBar setShowsCancelButton:NO animated:YES];
}
}
#pragma mark UISearchBarDelegate
/// Not necessary in iOS 13; remove this when iOS 13 is the deployment target
- (void)searchBar:(UISearchBar *)searchBar selectedScopeButtonIndexDidChange:(NSInteger)selectedScope {
[self updateSearchResultsForSearchController:self.searchController];
}
#pragma mark Table view
/// Not having a title in the first section looks weird with a rounded-corner table view style
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section {
if (@available(iOS 13, *)) {
return @" "; // For inset grouped style
}
return nil; // For plain/gropued style
}
@end
+39
View File
@@ -0,0 +1,39 @@
//
// FLEXTableViewSection.h
// FLEX
//
// Created by Tanner Bennett on 7/11/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
/// A protocol for arbitrary case-insensitive pattern matching
@protocol FLEXPatternMatching <NSObject>
/// @return YES if the receiver matches the query, case-insensitive
- (BOOL)matches:(NSString *)query;
@end
@interface FLEXTableViewSection<__covariant ObjectType> : NSObject
+ (instancetype)section:(NSInteger)section title:(NSString *)title rows:(NSArray<ObjectType<FLEXPatternMatching>> *)rows;
@property (nonatomic, readonly) NSInteger section;
@property (nonatomic, readonly) NSString *title;
@property (nonatomic, readonly) NSArray<ObjectType<FLEXPatternMatching>> *rows;
@property (nonatomic, readonly) NSInteger count;
/// @return A new section containing only rows that match the string,
/// or nil if the section was empty and no rows matched the string.
- (nullable instancetype)newSectionWithRowsMatchingQuery:(NSString *)query;
@end
@interface FLEXTableViewSection<__covariant ObjectType> (Subscripting)
- (ObjectType)objectAtIndexedSubscript:(NSUInteger)idx;
@end
NS_ASSUME_NONNULL_END
+49
View File
@@ -0,0 +1,49 @@
//
// FLEXTableViewSection.m
// FLEX
//
// Created by Tanner Bennett on 7/11/19.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import "FLEXTableViewSection.h"
@implementation FLEXTableViewSection
+ (instancetype)section:(NSInteger)section title:(NSString *)title rows:(NSArray *)rows {
FLEXTableViewSection *s = [self new];
s->_section = section;
s->_title = title;
s->_rows = rows.copy;
return s;
}
- (instancetype)newSectionWithRowsMatchingQuery:(NSString *)query {
// Find rows containing the search string
NSPredicate *containsString = [NSPredicate predicateWithBlock:^BOOL(id<FLEXPatternMatching> obj, NSDictionary *bindings) {
return [obj matches:query];
}];
NSArray *filteredRows = [self.rows filteredArrayUsingPredicate:containsString];
// Only return new section if not empty
if (filteredRows.count) {
return [[self class] section:self.section title:self.title rows:filteredRows];
}
return nil;
}
- (NSInteger)count {
return self.rows.count;
}
@end
@implementation FLEXTableViewSection (Subscripting)
- (id)objectAtIndexedSubscript:(NSUInteger)idx {
return self.rows[idx];
}
@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;
@@ -34,12 +34,12 @@
{
self = [super initWithFrame:frame];
if (self) {
self.slider = [[UISlider alloc] init];
self.slider = [UISlider new];
self.slider.backgroundColor = self.backgroundColor;
[self.slider addTarget:self action:@selector(sliderChanged:) forControlEvents:UIControlEventValueChanged];
[self addSubview:self.slider];
self.valueLabel = [[UILabel alloc] init];
self.valueLabel = [UILabel new];
self.valueLabel.backgroundColor = self.backgroundColor;
self.valueLabel.font = [FLEXUtility defaultFontOfSize:14.0];
self.valueLabel.textAlignment = NSTextAlignmentRight;
@@ -94,9 +94,9 @@
@interface FLEXColorPreviewBox : UIView
@property (nonatomic, strong) UIColor *color;
@property (nonatomic) UIColor *color;
@property (nonatomic, strong) UIView *colorOverlayView;
@property (nonatomic) UIView *colorOverlayView;
@end
@@ -107,12 +107,12 @@
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;
@@ -134,12 +134,12 @@
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,12 +153,12 @@
@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
@@ -168,32 +168,32 @@
{
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 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];
}
@@ -221,14 +221,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);
@@ -11,7 +11,7 @@
@interface FLEXArgumentInputDateView ()
@property (nonatomic, strong) UIDatePicker *datePicker;
@property (nonatomic) UIDatePicker *datePicker;
@end
@@ -21,7 +21,7 @@
{
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];
@@ -13,8 +13,8 @@
@interface FLEXArgumentInputFontView ()
@property (nonatomic, strong) FLEXArgumentInputView *fontNameInput;
@property (nonatomic, strong) FLEXArgumentInputView *pointSizeInput;
@property (nonatomic) FLEXArgumentInputView *fontNameInput;
@property (nonatomic) FLEXArgumentInputView *pointSizeInput;
@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,7 +11,7 @@
@interface FLEXArgumentInputFontsPickerView ()
@property (nonatomic, strong) NSMutableArray *availableFonts;
@property (nonatomic) NSMutableArray<NSString *> *availableFonts;
@end
@@ -35,12 +35,12 @@
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;
return self.inputTextView.text.length > 0 ? [self.inputTextView.text copy] : nil;
}
#pragma mark - private
@@ -56,7 +56,7 @@
- (void)createAvailableFonts
{
NSMutableArray *unsortedFontsArray = [NSMutableArray array];
NSMutableArray<NSString *> *unsortedFontsArray = [NSMutableArray array];
for (NSString *eachFontFamily in [UIFont familyNames]) {
for (NSString *eachFontName in [UIFont fontNamesForFamilyName:eachFontFamily]) {
[unsortedFontsArray addObject:eachFontName];
@@ -74,7 +74,7 @@
- (NSInteger)pickerView:(UIPickerView *)pickerView numberOfRowsInComponent:(NSInteger)component
{
return [self.availableFonts count];
return self.availableFonts.count;
}
#pragma mark - UIPickerViewDelegate
@@ -84,13 +84,13 @@
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];
@@ -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
@@ -30,12 +30,12 @@
- (id)inputValue
{
return [FLEXRuntimeUtility valueForNumberWithObjCType:[self.typeEncoding UTF8String] fromInputString:self.inputTextView.text];
return [FLEXRuntimeUtility valueForNumberWithObjCType:self.typeEncoding.UTF8String fromInputString:self.inputTextView.text];
}
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
{
static NSArray *primitiveTypes = nil;
static NSArray<NSString *> *primitiveTypes = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
primitiveTypes = @[@(@encode(char)),
@@ -49,7 +49,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,243 @@
//
// 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] == '@';
}
+ (FLEXArgInputObjectType)preferredDefaultTypeForObjCType:(const char *)type withCurrentValue:(id)value
{
NSParameterAssert(type[0] == '@');
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
@@ -27,11 +27,12 @@
- (id)inputValue
{
// Interpret empty string as nil. We loose the ablitiy to set empty string as a string value,
// 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;
return self.inputTextView.text.length > 0 ? [self.inputTextView.text copy] : nil;
}
// TODO: Support using object address for strings, as in the object arg view.
#pragma mark -
@@ -12,7 +12,7 @@
@interface FLEXArgumentInputStructView ()
@property (nonatomic, strong) NSArray *argumentInputViews;
@property (nonatomic) NSArray<FLEXArgumentInputView *> *argumentInputViews;
@end
@@ -22,16 +22,16 @@
{
self = [super initWithArgumentTypeEncoding:typeEncoding];
if (self) {
NSMutableArray *inputViews = [NSMutableArray array];
NSArray *customTitles = [[self class] customFieldTitlesForTypeEncoding:typeEncoding];
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]) {
inputView.title = [customTitles objectAtIndex:fieldIndex];
if (fieldIndex < customTitles.count) {
inputView.title = customTitles[fieldIndex];
} else {
inputView.title = [NSString stringWithFormat:@"%@ field %lu (%@)", structName, (unsigned long)fieldIndex, prettyTypeEncoding];
}
@@ -59,7 +59,7 @@
{
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.
@@ -72,9 +72,9 @@
[FLEXRuntimeUtility enumerateTypesInStructEncoding:structTypeEncoding usingBlock:^(NSString *structName, const char *fieldTypeEncoding, NSString *prettyTypeEncoding, NSUInteger fieldIndex, NSUInteger fieldOffset) {
void *fieldPointer = unboxedValue + fieldOffset;
FLEXArgumentInputView *inputView = [self.argumentInputViews objectAtIndex:fieldIndex];
FLEXArgumentInputView *inputView = self.argumentInputViews[fieldIndex];
if (fieldTypeEncoding[0] == @encode(id)[0] || fieldTypeEncoding[0] == @encode(Class)[0]) {
if (fieldTypeEncoding[0] == FLEXTypeEncodingObjcObject || fieldTypeEncoding[0] == FLEXTypeEncodingObjcClass) {
inputView.inputValue = (__bridge id)fieldPointer;
} else {
NSValue *boxedField = [FLEXRuntimeUtility valueForPrimitivePointer:fieldPointer objCType:fieldTypeEncoding];
@@ -90,7 +90,7 @@
- (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.
@@ -102,9 +102,9 @@
[FLEXRuntimeUtility enumerateTypesInStructEncoding:structTypeEncoding usingBlock:^(NSString *structName, const char *fieldTypeEncoding, NSString *prettyTypeEncoding, NSUInteger fieldIndex, NSUInteger fieldOffset) {
void *fieldPointer = unboxedStruct + fieldOffset;
FLEXArgumentInputView *inputView = [self.argumentInputViews objectAtIndex:fieldIndex];
FLEXArgumentInputView *inputView = self.argumentInputViews[fieldIndex];
if (fieldTypeEncoding[0] == @encode(id)[0] || fieldTypeEncoding[0] == @encode(Class)[0]) {
if (fieldTypeEncoding[0] == FLEXTypeEncodingObjcObject || fieldTypeEncoding[0] == FLEXTypeEncodingObjcClass) {
// Object fields
memcpy(fieldPointer, (__bridge void *)inputView.inputValue, sizeof(id));
} else {
@@ -176,18 +176,20 @@
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
{
return type && type[0] == '{';
return type && type[0] == FLEXTypeEncodingStructBegin;
}
+ (NSArray *)customFieldTitlesForTypeEncoding:(const char *)typeEncoding
+ (NSArray<NSString *> *)customFieldTitlesForTypeEncoding:(const char *)typeEncoding
{
NSArray *customTitles = nil;
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 +205,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,7 +10,7 @@
@interface FLEXArgumentInputSwitchView ()
@property (nonatomic, strong) UISwitch *inputSwitch;
@property (nonatomic) UISwitch *inputSwitch;
@end
@@ -20,7 +20,7 @@
{
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];
@@ -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,12 +6,14 @@
//
//
#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
@@ -22,23 +24,31 @@
{
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 primaryBackgroundColor];
self.inputTextView.layer.borderColor = [FLEXColor borderColor].CGColor;
self.inputTextView.layer.borderWidth = 1.f;
self.inputTextView.layer.cornerRadius = 5.f;
self.inputTextView.autocapitalizationType = UITextAutocapitalizationTypeNone;
self.inputTextView.autocorrectionType = UITextAutocorrectionTypeNo;
self.inputTextView.delegate = self;
self.inputTextView.inputAccessoryView = [self createToolBar];
[self addSubview:self.inputTextView];
self.placeholderLabel = [UILabel new];
self.placeholderLabel.font = self.inputTextView.font;
self.placeholderLabel.textColor = [FLEXColor deemphasizedTextColor];
self.placeholderLabel.numberOfLines = 0;
[self.inputTextView addSubview:self.placeholderLabel];
}
return self;
}
#pragma mark - private
#pragma mark - Private
- (UIToolbar*)createToolBar
- (UIToolbar *)createToolBar
{
UIToolbar *toolBar = [UIToolbar new];
[toolBar sizeToFit];
@@ -53,12 +63,25 @@
[self.inputTextView resignFirstResponder];
}
#pragma mark - Text View Changes
- (void)textViewDidChange:(UITextView *)textView
- (void)setInputPlaceholderText:(NSString *)placeholder
{
[self.delegate argumentInputViewValueDidChange:self];
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];
}
- (NSString *)inputPlaceholderText
{
return self.placeholderLabel.text;
}
@@ -77,25 +100,28 @@
[super layoutSubviews];
self.inputTextView.frame = CGRectMake(0, self.topInputFieldVerticalLayoutGuide, self.bounds.size.width, [self inputTextViewHeight]);
// Placeholder label is positioned by insetting origin,
// which is the line fragment padding for X and 0 for Y,
// by the content inset then the text container inset
CGFloat leading = self.inputTextView.textContainer.lineFragmentPadding;
CGSize s = self.inputTextView.frame.size;
self.placeholderLabel.frame = CGRectMake(leading, 0, s.width, s.height);
self.placeholderLabel.frame = UIEdgeInsetsInsetRect(
UIEdgeInsetsInsetRect(self.placeholderLabel.frame, self.inputTextView.contentInset),
self.inputTextView.textContainerInset
);
}
- (NSUInteger)numberOfInputLines
{
NSUInteger numberOfInputLines = 0;
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
@@ -111,6 +137,20 @@
}
#pragma mark - Trait collection changes
- (void)traitCollectionDidChange:(UITraitCollection *)previousTraitCollection
{
#if FLEX_AT_LEAST_IOS13_SDK
if (@available(iOS 13.0, *)) {
if (previousTraitCollection.userInterfaceStyle != self.traitCollection.userInterfaceStyle) {
self.inputTextView.layer.borderColor = [FLEXColor borderColor].CGColor;
}
}
#endif
}
#pragma mark - Class Helpers
+ (UIFont *)inputFont
@@ -118,4 +158,13 @@
return [FLEXUtility defaultFontOfSize:14.0];
}
#pragma mark - UITextViewDelegate
- (void)textViewDidChange:(UITextView *)textView
{
[self.delegate argumentInputViewValueDidChange:self];
self.placeholderLabel.hidden = !(self.inputPlaceholderText.length && !textView.text.length);
}
@end
@@ -26,12 +26,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 +48,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
@@ -11,8 +11,8 @@
@interface FLEXArgumentInputView ()
@property (nonatomic, strong) UILabel *titleLabel;
@property (nonatomic, strong) NSString *typeEncoding;
@property (nonatomic) UILabel *titleLabel;
@property (nonatomic) NSString *typeEncoding;
@end
@@ -22,7 +22,7 @@
{
self = [super initWithFrame:CGRectZero];
if (self) {
self.typeEncoding = @(typeEncoding);
self.typeEncoding = typeEncoding != NULL ? @(typeEncoding) : nil;
}
return self;
}
@@ -56,7 +56,7 @@
- (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];
@@ -68,7 +68,7 @@
- (BOOL)showsTitle
{
return [self.title length] > 0;
return self.title.length > 0;
}
- (CGFloat)topInputFieldVerticalLayoutGuide
@@ -89,17 +89,6 @@
return NO;
}
- (void)setInputValue:(id)inputValue
{
// Subclasses should override.
}
- (id)inputValue
{
// Subclasses should override.
return nil;
}
+ (BOOL)supportsObjCType:(const char *)type withCurrentValue:(id)value
{
return NO;
@@ -125,7 +114,7 @@
{
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];
@@ -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,6 +17,7 @@
#import "FLEXArgumentInputFontView.h"
#import "FLEXArgumentInputColorView.h"
#import "FLEXArgumentInputDateView.h"
#import "FLEXRuntimeUtility.h"
@implementation FLEXArgumentInputViewFactory
@@ -33,34 +34,35 @@
// 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
{
// 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;
}
@@ -6,9 +6,9 @@
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import "FLEXFieldEditorViewController.h"
#import "FLEXMutableFieldEditorViewController.h"
@interface FLEXDefaultEditorViewController : FLEXFieldEditorViewController
@interface FLEXDefaultEditorViewController : FLEXMutableFieldEditorViewController
- (id)initWithDefaults:(NSUserDefaults *)defaults key:(NSString *)key;
@@ -15,7 +15,7 @@
@interface FLEXDefaultEditorViewController ()
@property (nonatomic, readonly) NSUserDefaults *defaults;
@property (nonatomic, strong) NSString *key;
@property (nonatomic) NSString *key;
@end
@@ -43,7 +43,10 @@
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];
@@ -64,9 +67,19 @@
self.firstInputView.inputValue = [self.defaults objectForKey:self.key];
}
- (void)getterButtonPressed:(id)sender
{
[super getterButtonPressed:sender];
id returnedObject = [self.defaults objectForKey:self.key];
[self exploreObjectOrPopViewController:returnedObject];
}
+ (BOOL)canEditDefaultWithValue:(id)currentValue
{
return [FLEXArgumentInputViewFactory canEditFieldWithTypeEncoding:@encode(id) currentValue: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) NSArray<FLEXArgumentInputView *> *argumentInputViews;
@end
+9 -9
View File
@@ -12,10 +12,10 @@
@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
@@ -25,7 +25,7 @@
{
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 +33,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];
@@ -103,7 +103,7 @@
}
}
- (void)setArgumentInputViews:(NSArray *)argumentInputViews
- (void)setArgumentInputViews:(NSArray<FLEXArgumentInputView *> *)argumentInputViews
{
if (![_argumentInputViews isEqual:argumentInputViews]) {
@@ -123,14 +123,14 @@
+ (UIView *)dividerView
{
UIView *dividerView = [[UIView alloc] init];
UIView *dividerView = [UIView new];
dividerView.backgroundColor = [self dividerColor];
return dividerView;
}
+ (UIColor *)dividerColor
{
return [UIColor lightGrayColor];
return UIColor.lightGrayColor;
}
+ (CGFloat)horizontalPadding
@@ -19,10 +19,14 @@
@property (nonatomic, readonly) FLEXArgumentInputView *firstInputView;
// For subclass use only.
@property (nonatomic, strong, readonly) id target;
@property (nonatomic, strong, readonly) FLEXFieldEditorView *fieldEditorView;
@property (nonatomic, strong, readonly) UIBarButtonItem *setterButton;
@property (nonatomic, readonly) id target;
@property (nonatomic, readonly) FLEXFieldEditorView *fieldEditorView;
@property (nonatomic, readonly) UIBarButtonItem *setterButton;
- (void)actionButtonPressed:(id)sender;
- (NSString *)titleForActionButton;
/// Pushes an explorer view controller for the given object
/// or pops the current view controller.
- (void)exploreObjectOrPopViewController:(id)objectOrNil;
@end
+25 -10
View File
@@ -6,20 +6,23 @@
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import "FLEXColor.h"
#import "FLEXFieldEditorViewController.h"
#import "FLEXFieldEditorView.h"
#import "FLEXRuntimeUtility.h"
#import "FLEXUtility.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXArgumentInputView.h"
#import "FLEXArgumentInputViewFactory.h"
#import "FLEXObjectExplorerViewController.h"
@interface FLEXFieldEditorViewController () <UIScrollViewDelegate>
@property (nonatomic, strong) UIScrollView *scrollView;
@property (nonatomic) UIScrollView *scrollView;
@property (nonatomic, strong, readwrite) id target;
@property (nonatomic, strong, readwrite) FLEXFieldEditorView *fieldEditorView;
@property (nonatomic, strong, readwrite) UIBarButtonItem *setterButton;
@property (nonatomic, readwrite) id target;
@property (nonatomic, readwrite) FLEXFieldEditorView *fieldEditorView;
@property (nonatomic, readwrite) UIBarButtonItem *setterButton;
@end
@@ -30,15 +33,15 @@
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];
[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];
[NSNotificationCenter.defaultCenter removeObserver:self];
}
- (void)keyboardDidShow:(NSNotification *)notification
@@ -72,7 +75,7 @@
{
[super viewDidLoad];
self.view.backgroundColor = [FLEXUtility scrollViewGrayColor];
self.view.backgroundColor = [FLEXColor scrollViewBackgroundColor];
self.scrollView = [[UIScrollView alloc] initWithFrame:self.view.bounds];
self.scrollView.backgroundColor = self.view.backgroundColor;
@@ -80,7 +83,7 @@
self.scrollView.delegate = self;
[self.view addSubview:self.scrollView];
self.fieldEditorView = [[FLEXFieldEditorView alloc] init];
self.fieldEditorView = [FLEXFieldEditorView new];
self.fieldEditorView.backgroundColor = self.view.backgroundColor;
self.fieldEditorView.targetDescription = [NSString stringWithFormat:@"%@ %p", [self.target class], self.target];
[self.scrollView addSubview:self.fieldEditorView];
@@ -99,7 +102,7 @@
- (FLEXArgumentInputView *)firstInputView
{
return [[self.fieldEditorView argumentInputViews] firstObject];
return [self.fieldEditorView argumentInputViews].firstObject;
}
- (void)actionButtonPressed:(id)sender
@@ -114,4 +117,16 @@
return @"Set";
}
- (void)exploreObjectOrPopViewController:(id)objectOrNil {
if (objectOrNil) {
// For non-nil (or void) return types, push an explorer view controller to display the object
FLEXObjectExplorerViewController *explorerViewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:objectOrNil];
[self.navigationController pushViewController:explorerViewController animated:YES];
} else {
// If we didn't get a returned object but the method call succeeded,
// pop this view controller off the stack to indicate that the call went through.
[self.navigationController popViewControllerAnimated:YES];
}
}
@end
@@ -6,10 +6,10 @@
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import "FLEXFieldEditorViewController.h"
#import "FLEXMutableFieldEditorViewController.h"
#import <objc/runtime.h>
@interface FLEXIvarEditorViewController : FLEXFieldEditorViewController
@interface FLEXIvarEditorViewController : FLEXMutableFieldEditorViewController
- (id)initWithTarget:(id)target ivar:(Ivar)ivar;
+15 -1
View File
@@ -15,7 +15,7 @@
@interface FLEXIvarEditorViewController () <FLEXArgumentInputViewDelegate>
@property (nonatomic, assign) Ivar ivar;
@property (nonatomic) Ivar ivar;
@end
@@ -52,9 +52,23 @@
- (void)actionButtonPressed:(id)sender
{
[super actionButtonPressed:sender];
// TODO: check mutability and use mutableCopy if necessary;
// this currently could and would assign NSArray to NSMutableArray
[FLEXRuntimeUtility setValue:self.firstInputView.inputValue forIvar:self.ivar onObject:self.target];
self.firstInputView.inputValue = [FLEXRuntimeUtility valueForIvar:self.ivar onObject:self.target];
// Pop view controller for consistency;
// property setters and method calls also pop on success.
[self.navigationController popViewControllerAnimated:YES];
}
- (void)getterButtonPressed:(id)sender
{
[super getterButtonPressed:sender];
id returnedObject = [FLEXRuntimeUtility valueForIvar:self.ivar onObject:self.target];
[self exploreObjectOrPopViewController:returnedObject];
}
- (void)argumentInputViewValueDidChange:(FLEXArgumentInputView *)argumentInputView
@@ -13,10 +13,12 @@
#import "FLEXObjectExplorerViewController.h"
#import "FLEXArgumentInputView.h"
#import "FLEXArgumentInputViewFactory.h"
#import "FLEXUtility.h"
@interface FLEXMethodCallingViewController ()
@property (nonatomic, assign) Method method;
@property (nonatomic) Method method;
@property (nonatomic) FLEXTypeEncoding *returnType;
@end
@@ -27,6 +29,7 @@
self = [super initWithTarget:target];
if (self) {
self.method = method;
self.returnType = [FLEXRuntimeUtility returnTypeForMethod:method];
self.title = [self isClassMethod] ? @"Class Method" : @"Method";
}
return self;
@@ -36,10 +39,14 @@
{
[super viewDidLoad];
self.fieldEditorView.fieldDescription = [FLEXRuntimeUtility prettyNameForMethod:self.method isClassMethod:[self isClassMethod]];
NSString *returnType = @((const char *)self.returnType);
NSString *methodDescription = [FLEXRuntimeUtility prettyNameForMethod:self.method isClassMethod:[self isClassMethod]];
NSString *format = @"Signature:\n%@\n\nReturn Type:\n%@";
NSString *info = [NSString stringWithFormat:format, methodDescription, returnType];
self.fieldEditorView.fieldDescription = info;
NSArray *methodComponents = [FLEXRuntimeUtility prettyArgumentComponentsForMethod:self.method];
NSMutableArray *argumentInputViews = [NSMutableArray array];
NSArray<NSString *> *methodComponents = [FLEXRuntimeUtility prettyArgumentComponentsForMethod:self.method];
NSMutableArray<FLEXArgumentInputView *> *argumentInputViews = [NSMutableArray array];
unsigned int argumentIndex = kFLEXNumberOfImplicitArgs;
for (NSString *methodComponent in methodComponents) {
char *argumentTypeEncoding = method_copyArgumentType(self.method, argumentIndex);
@@ -54,6 +61,12 @@
self.fieldEditorView.argumentInputViews = argumentInputViews;
}
- (void)dealloc
{
free(self.returnType);
self.returnType = NULL;
}
- (BOOL)isClassMethod
{
return self.target && self.target == [self.target class];
@@ -82,18 +95,14 @@
id returnedObject = [FLEXRuntimeUtility performSelector:method_getName(self.method) onObject:self.target withArguments:arguments error:&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];
[FLEXAlert showAlert:@"Method Call Failed" message:[error localizedDescription] from:self];
} else if (returnedObject) {
// For non-nil (or void) return types, push an explorer view controller to display the returned object
returnedObject = [FLEXRuntimeUtility potentiallyUnwrapBoxedPointer:returnedObject type:self.returnType];
FLEXObjectExplorerViewController *explorerViewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:returnedObject];
[self.navigationController pushViewController:explorerViewController animated:YES];
} 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:returnedObject];
}
}
@@ -0,0 +1,18 @@
//
// FLEXMutableFieldEditorViewController.h
// FLEX
//
// Created by Tanner on 11/22/18.
// Copyright © 2018 Flipboard. All rights reserved.
//
#import "FLEXFieldEditorViewController.h"
@interface FLEXMutableFieldEditorViewController : FLEXFieldEditorViewController
@property (nonatomic, readonly) UIBarButtonItem *getterButton;
- (void)getterButtonPressed:(id)sender;
- (NSString *)titleForGetterButton;
@end
@@ -0,0 +1,36 @@
//
// FLEXMutableFieldEditorViewController.m
// FLEX
//
// Created by Tanner on 11/22/18.
// Copyright © 2018 Flipboard. All rights reserved.
//
#import "FLEXMutableFieldEditorViewController.h"
#import "FLEXFieldEditorView.h"
@interface FLEXMutableFieldEditorViewController ()
@property (nonatomic, readwrite) UIBarButtonItem *getterButton;
@end
@implementation FLEXMutableFieldEditorViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.getterButton = [[UIBarButtonItem alloc] initWithTitle:[self titleForGetterButton] style:UIBarButtonItemStyleDone target:self action:@selector(getterButtonPressed:)];
self.navigationItem.rightBarButtonItems = @[self.setterButton, self.getterButton];
}
- (void)getterButtonPressed:(id)sender {
// Subclasses can override
[self.fieldEditorView endEditing:YES];
}
- (NSString *)titleForGetterButton {
return @"Get";
}
@end
@@ -6,13 +6,13 @@
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import "FLEXFieldEditorViewController.h"
#import "FLEXMutableFieldEditorViewController.h"
#import <objc/runtime.h>
@interface FLEXPropertyEditorViewController : FLEXFieldEditorViewController
@interface FLEXPropertyEditorViewController : FLEXMutableFieldEditorViewController
- (id)initWithTarget:(id)target property:(objc_property_t)property;
+ (BOOL)canEditProperty:(objc_property_t)property currentValue:(id)value;
+ (BOOL)canEditProperty:(objc_property_t)property onObject:(id)object currentValue:(id)value;
@end
@@ -12,10 +12,11 @@
#import "FLEXArgumentInputView.h"
#import "FLEXArgumentInputViewFactory.h"
#import "FLEXArgumentInputSwitchView.h"
#import "FLEXUtility.h"
@interface FLEXPropertyEditorViewController () <FLEXArgumentInputViewDelegate>
@property (nonatomic, assign) objc_property_t property;
@property (nonatomic) objc_property_t property;
@end
@@ -37,9 +38,9 @@
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];
self.setterButton.enabled = [[self class] canEditProperty:self.property onObject:self.target currentValue:currentValue];
const char *typeEncoding = [[FLEXRuntimeUtility typeEncodingForProperty:self.property] UTF8String];
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];
@@ -62,10 +63,7 @@
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];
[FLEXAlert showAlert:@"Property Setter Failed" message:[error localizedDescription] from:self];
self.firstInputView.inputValue = [FLEXRuntimeUtility valueForProperty:self.property onObject:self.target];
} else {
// If the setter was called without error, pop the view controller to indicate that and make the user's life easier.
@@ -76,6 +74,13 @@
}
}
- (void)getterButtonPressed:(id)sender
{
[super getterButtonPressed:sender];
id returnedObject = [FLEXRuntimeUtility valueForProperty:self.property onObject:self.target];
[self exploreObjectOrPopViewController:returnedObject];
}
- (void)argumentInputViewValueDidChange:(FLEXArgumentInputView *)argumentInputView
{
if ([argumentInputView isKindOfClass:[FLEXArgumentInputSwitchView class]]) {
@@ -83,11 +88,12 @@
}
}
+ (BOOL)canEditProperty:(objc_property_t)property currentValue:(id)value
+ (BOOL)canEditProperty:(objc_property_t)property onObject:(id)object currentValue:(id)value
{
const char *typeEncoding = [[FLEXRuntimeUtility typeEncodingForProperty:property] UTF8String];
const char *typeEncoding = [FLEXRuntimeUtility typeEncodingForProperty:property].UTF8String;
BOOL canEditType = [FLEXArgumentInputViewFactory canEditFieldWithTypeEncoding:typeEncoding currentValue:value];
BOOL isReadonly = [FLEXRuntimeUtility isReadonlyProperty:property];
SEL setterSelector = [FLEXRuntimeUtility setterSelectorForProperty:property];
BOOL isReadonly = [FLEXRuntimeUtility isReadonlyProperty:property] && (!setterSelector || ![object respondsToSelector:setterSelector]);
return canEditType && !isReadonly;
}
@@ -10,6 +10,7 @@
@protocol FLEXExplorerViewControllerDelegate;
/// A view controller that manages the FLEX toolbar.
@interface FLEXExplorerViewController : UIViewController
@property (nonatomic, weak) id <FLEXExplorerViewControllerDelegate> delegate;
@@ -17,6 +18,12 @@
- (BOOL)shouldReceiveTouchAtWindowPoint:(CGPoint)pointInWindowCoordinates;
- (BOOL)wantsWindowToBecomeKey;
/// @brief Used to present (or dismiss) a modal view controller ("tool"), typically triggered by pressing a button in the toolbar.
///
/// If a tool is already presented, this method simply dismisses it and calls the completion block.
/// If no tool is presented, @code future() @endcode is presented and the completion block is called.
- (void)toggleToolWithViewControllerProvider:(UIViewController *(^)(void))future completion:(void(^)(void))completion;
// Keyboard shortcut helpers
- (void)toggleSelectTool;
@@ -16,6 +16,8 @@
#import "FLEXObjectExplorerFactory.h"
#import "FLEXNetworkHistoryTableViewController.h"
static NSString *const kFLEXToolbarTopMarginDefaultsKey = @"com.flex.FLEXToolbar.topMargin";
typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
FLEXExplorerModeDefault,
FLEXExplorerModeSelect,
@@ -24,49 +26,43 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
@interface FLEXExplorerViewController () <FLEXHierarchyTableViewControllerDelegate, FLEXGlobalsTableViewControllerDelegate>
@property (nonatomic, strong) FLEXExplorerToolbar *explorerToolbar;
@property (nonatomic) FLEXExplorerToolbar *explorerToolbar;
/// Tracks the currently active tool/mode
@property (nonatomic, assign) FLEXExplorerMode currentMode;
@property (nonatomic) FLEXExplorerMode currentMode;
/// Gesture recognizer for dragging a view in move mode
@property (nonatomic, strong) UIPanGestureRecognizer *movePanGR;
@property (nonatomic) UIPanGestureRecognizer *movePanGR;
/// Gesture recognizer for showing additional details on the selected view
@property (nonatomic, strong) UITapGestureRecognizer *detailsTapGR;
@property (nonatomic) UITapGestureRecognizer *detailsTapGR;
/// Only valid while a move pan gesture is in progress.
@property (nonatomic, assign) CGRect selectedViewFrameBeforeDragging;
@property (nonatomic) CGRect selectedViewFrameBeforeDragging;
/// Only valid while a toolbar drag pan gesture is in progress.
@property (nonatomic, assign) CGRect toolbarFrameBeforeDragging;
@property (nonatomic) CGRect toolbarFrameBeforeDragging;
/// Borders of all the visible views in the hierarchy at the selection point.
/// The keys are NSValues with the correponding view (nonretained).
@property (nonatomic, strong) NSDictionary *outlineViewsForVisibleViews;
/// The keys are NSValues with the corresponding view (nonretained).
@property (nonatomic) NSDictionary<NSValue *, UIView *> *outlineViewsForVisibleViews;
/// The actual views at the selection point with the deepest view last.
@property (nonatomic, strong) NSArray *viewsAtTapPoint;
@property (nonatomic) NSArray<UIView *> *viewsAtTapPoint;
/// The view that we're currently highlighting with an overlay and displaying details for.
@property (nonatomic, strong) UIView *selectedView;
@property (nonatomic) UIView *selectedView;
/// A colored transparent overlay to indicate that the view is selected.
@property (nonatomic, strong) UIView *selectedViewOverlay;
@property (nonatomic) UIView *selectedViewOverlay;
/// Tracked so we can restore the key window after dismissing a modal.
/// We need to become key after modal presentation so we can correctly capture intput.
/// 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, strong) UIWindow *previousKeyWindow;
/// Similar to the previousKeyWindow property above, we need to track status bar styling if
/// the app doesn't use view controller based status bar management. When we present a modal,
/// we want to change the status bar style to UIStausBarStyleDefault. Before changing, we stash
/// the current style. On dismissal, we return the staus bar to the style that the app was using previously.
@property (nonatomic, assign) UIStatusBarStyle previousStatusBarStyle;
@property (nonatomic) UIWindow *previousKeyWindow;
/// All views that we're KVOing. Used to help us clean up properly.
@property (nonatomic, strong) NSMutableSet *observedViews;
@property (nonatomic) NSMutableSet<UIView *> *observedViews;
@end
@@ -91,13 +87,17 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
- (void)viewDidLoad
{
[super viewDidLoad];
// Toolbar
self.explorerToolbar = [[FLEXExplorerToolbar alloc] init];
CGSize toolbarSize = [self.explorerToolbar sizeThatFits:self.view.bounds.size];
self.explorerToolbar = [FLEXExplorerToolbar new];
// Start the toolbar off below any bars that may be at the top of the view.
CGFloat toolbarOriginY = 100.0;
self.explorerToolbar.frame = CGRectMake(0.0, toolbarOriginY, toolbarSize.width, toolbarSize.height);
id toolbarOriginYDefault = [[NSUserDefaults standardUserDefaults] objectForKey:kFLEXToolbarTopMarginDefaultsKey];
CGFloat toolbarOriginY = toolbarOriginYDefault ? [toolbarOriginYDefault doubleValue] : 100;
CGRect safeArea = [self viewSafeArea];
CGSize toolbarSize = [self.explorerToolbar sizeThatFits:CGSizeMake(CGRectGetWidth(self.view.bounds), CGRectGetHeight(safeArea))];
[self updateToolbarPositionWithUnconstrainedFrame:CGRectMake(CGRectGetMinX(safeArea), toolbarOriginY, toolbarSize.width, toolbarSize.height)];
self.explorerToolbar.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleBottomMargin | UIViewAutoresizingFlexibleTopMargin;
[self.view addSubview:self.explorerToolbar];
[self setupToolbarActions];
@@ -125,16 +125,15 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
- (UIViewController *)viewControllerForRotationAndOrientation
{
UIWindow *window = self.previousKeyWindow ?: [[UIApplication sharedApplication] keyWindow];
UIWindow *window = self.previousKeyWindow ?: [UIApplication.sharedApplication keyWindow];
UIViewController *viewController = window.rootViewController;
// Obfuscating selector _viewControllerForSupportedInterfaceOrientations
NSString *viewControllerSelectorString = [@[@"_vie", @"wContro", @"llerFor", @"Supported", @"Interface", @"Orientations"] componentsJoinedByString:@""];
SEL viewControllerSelector = NSSelectorFromString(viewControllerSelectorString);
if ([viewController respondsToSelector:viewControllerSelector]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
viewController = [viewController performSelector:viewControllerSelector];
#pragma clang diagnostic pop
viewController = [viewController valueForKey:viewControllerSelectorString];
}
return viewController;
}
@@ -147,7 +146,8 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
}
// The UIViewController docs state that this method must not return zero.
// If we weren't able to get a valid value for the supported interface orientations, default to all supported.
// If we weren't able to get a valid value for the supported interface
// orientations, default to all supported.
if (supportedOrientations == 0) {
supportedOrientations = UIInterfaceOrientationMaskAll;
}
@@ -165,31 +165,29 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
return shouldAutorotate;
}
- (void)willRotateToInterfaceOrientation:(UIInterfaceOrientation)toInterfaceOrientation duration:(NSTimeInterval)duration
- (void)viewWillTransitionToSize:(CGSize)size withTransitionCoordinator:(id<UIViewControllerTransitionCoordinator>)coordinator
{
for (UIView *outlineView in [self.outlineViewsForVisibleViews allValues]) {
outlineView.hidden = YES;
}
self.selectedViewOverlay.hidden = YES;
}
- (void)didRotateFromInterfaceOrientation:(UIInterfaceOrientation)fromInterfaceOrientation
{
for (UIView *view in self.viewsAtTapPoint) {
NSValue *key = [NSValue valueWithNonretainedObject:view];
UIView *outlineView = self.outlineViewsForVisibleViews[key];
outlineView.frame = [self frameInLocalCoordinatesForView:view];
if (self.currentMode == FLEXExplorerModeSelect) {
outlineView.hidden = NO;
[coordinator animateAlongsideTransition:^(id<UIViewControllerTransitionCoordinatorContext> context) {
for (UIView *outlineView in self.outlineViewsForVisibleViews.allValues) {
outlineView.hidden = YES;
}
self.selectedViewOverlay.hidden = YES;
} completion:^(id<UIViewControllerTransitionCoordinatorContext> context) {
for (UIView *view in self.viewsAtTapPoint) {
NSValue *key = [NSValue valueWithNonretainedObject:view];
UIView *outlineView = self.outlineViewsForVisibleViews[key];
outlineView.frame = [self frameInLocalCoordinatesForView:view];
if (self.currentMode == FLEXExplorerModeSelect) {
outlineView.hidden = NO;
}
}
}
if (self.selectedView) {
self.selectedViewOverlay.frame = [self frameInLocalCoordinatesForView:self.selectedView];
self.selectedViewOverlay.hidden = NO;
}
}
if (self.selectedView) {
self.selectedViewOverlay.frame = [self frameInLocalCoordinatesForView:self.selectedView];
self.selectedViewOverlay.hidden = NO;
}
}];
}
#pragma mark - Setter Overrides
@@ -206,17 +204,17 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
// Update the toolbar and selected overlay
self.explorerToolbar.selectedViewDescription = [FLEXUtility descriptionForView:selectedView includingFrame:YES];
self.explorerToolbar.selectedViewOverlayColor = [FLEXUtility consistentRandomColorForObject:selectedView];;
self.explorerToolbar.selectedViewOverlayColor = [FLEXUtility consistentRandomColorForObject:selectedView];
if (selectedView) {
if (!self.selectedViewOverlay) {
self.selectedViewOverlay = [[UIView alloc] init];
self.selectedViewOverlay = [UIView new];
[self.view addSubview:self.selectedViewOverlay];
self.selectedViewOverlay.layer.borderWidth = 1.0;
}
UIColor *outlineColor = [FLEXUtility consistentRandomColorForObject:selectedView];
self.selectedViewOverlay.backgroundColor = [outlineColor colorWithAlphaComponent:0.2];
self.selectedViewOverlay.layer.borderColor = [outlineColor CGColor];
self.selectedViewOverlay.layer.borderColor = outlineColor.CGColor;
self.selectedViewOverlay.frame = [self.view convertRect:selectedView.bounds fromView:selectedView];
// Make sure the selected overlay is in front of all the other subviews except the toolbar, which should always stay on top.
@@ -232,7 +230,7 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
}
}
- (void)setViewsAtTapPoint:(NSArray *)viewsAtTapPoint
- (void)setViewsAtTapPoint:(NSArray<UIView *> *)viewsAtTapPoint
{
if (![_viewsAtTapPoint isEqual:viewsAtTapPoint]) {
for (UIView *view in _viewsAtTapPoint) {
@@ -262,7 +260,7 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
case FLEXExplorerModeSelect:
// Make sure the outline views are unhidden in case we came from the move mode.
for (id key in self.outlineViewsForVisibleViews) {
for (NSValue *key in self.outlineViewsForVisibleViews) {
UIView *outlineView = self.outlineViewsForVisibleViews[key];
outlineView.hidden = NO;
}
@@ -270,7 +268,7 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
case FLEXExplorerModeMove:
// Hide all the outline views to focus on the selected view, which is the only one that will move.
for (id key in self.outlineViewsForVisibleViews) {
for (NSValue *key in self.outlineViewsForVisibleViews) {
UIView *outlineView = self.outlineViewsForVisibleViews[key];
outlineView.hidden = YES;
}
@@ -311,9 +309,9 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
[self.observedViews removeObject:view];
}
+ (NSArray *)viewKeyPathsToTrack
+ (NSArray<NSString *> *)viewKeyPathsToTrack
{
static NSArray *trackedViewKeyPaths = nil;
static NSArray<NSString *> *trackedViewKeyPaths = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSString *frameKeyPath = NSStringFromSelector(@selector(frame));
@@ -322,7 +320,7 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
return trackedViewKeyPaths;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *, id> *)change context:(void *)context
{
[self updateOverlayAndDescriptionForObjectIfNeeded:object];
}
@@ -331,9 +329,9 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
{
NSUInteger indexOfView = [self.viewsAtTapPoint indexOfObject:object];
if (indexOfView != NSNotFound) {
UIView *view = [self.viewsAtTapPoint objectAtIndex:indexOfView];
UIView *view = self.viewsAtTapPoint[indexOfView];
NSValue *key = [NSValue valueWithNonretainedObject:view];
UIView *outline = [self.outlineViewsForVisibleViews objectForKey:key];
UIView *outline = self.outlineViewsForVisibleViews[key];
if (outline) {
outline.frame = [self frameInLocalCoordinatesForView:view];
}
@@ -376,10 +374,10 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
[self toggleViewsTool];
}
- (NSArray *)allViewsInHierarchy
- (NSArray<UIView *> *)allViewsInHierarchy
{
NSMutableArray *allViews = [NSMutableArray array];
NSArray *windows = [self allWindows];
NSMutableArray<UIView *> *allViews = [NSMutableArray array];
NSArray<UIWindow *> *windows = [FLEXUtility allWindows];
for (UIWindow *window in windows) {
if (window != self.view.window) {
[allViews addObject:window];
@@ -389,32 +387,10 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
return allViews;
}
- (NSArray *)allWindows
{
BOOL includeInternalWindows = YES;
BOOL onlyVisibleWindows = NO;
NSArray *allWindowsComponents = @[@"al", @"lWindo", @"wsIncl", @"udingInt", @"ernalWin", @"dows:o", @"nlyVisi", @"bleWin", @"dows:"];
SEL allWindowsSelector = NSSelectorFromString([allWindowsComponents componentsJoinedByString:@""]);
NSMethodSignature *methodSignature = [[UIWindow class] methodSignatureForSelector:allWindowsSelector];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
invocation.target = [UIWindow class];
invocation.selector = allWindowsSelector;
[invocation setArgument:&includeInternalWindows atIndex:2];
[invocation setArgument:&onlyVisibleWindows atIndex:3];
[invocation invoke];
__unsafe_unretained NSArray *windows = nil;
[invocation getReturnValue:&windows];
return windows;
}
- (UIWindow *)statusWindow
{
NSString *statusBarString = [NSString stringWithFormat:@"%@arWindow", @"_statusB"];
return [[UIApplication sharedApplication] valueForKey:statusBarString];
return [UIApplication.sharedApplication valueForKey:statusBarString];
}
- (void)moveButtonTapped:(FLEXToolbarItem *)sender
@@ -465,12 +441,12 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
switch (panGR.state) {
case UIGestureRecognizerStateBegan:
self.toolbarFrameBeforeDragging = self.explorerToolbar.frame;
[self updateToolbarPostionWithDragGesture:panGR];
[self updateToolbarPositionWithDragGesture:panGR];
break;
case UIGestureRecognizerStateChanged:
case UIGestureRecognizerStateEnded:
[self updateToolbarPostionWithDragGesture:panGR];
[self updateToolbarPositionWithDragGesture:panGR];
break;
default:
@@ -478,20 +454,30 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
}
}
- (void)updateToolbarPostionWithDragGesture:(UIPanGestureRecognizer *)panGR
- (void)updateToolbarPositionWithDragGesture:(UIPanGestureRecognizer *)panGR
{
CGPoint translation = [panGR translationInView:self.view];
CGRect newToolbarFrame = self.toolbarFrameBeforeDragging;
newToolbarFrame.origin.y += translation.y;
CGFloat maxY = CGRectGetMaxY(self.view.bounds) - newToolbarFrame.size.height;
if (newToolbarFrame.origin.y < 0.0) {
newToolbarFrame.origin.y = 0.0;
} else if (newToolbarFrame.origin.y > maxY) {
newToolbarFrame.origin.y = maxY;
[self updateToolbarPositionWithUnconstrainedFrame:newToolbarFrame];
}
- (void)updateToolbarPositionWithUnconstrainedFrame:(CGRect)unconstrainedFrame
{
CGRect safeArea = [self viewSafeArea];
// We only constrain the Y-axis because We want the toolbar to handle the X-axis safeArea layout by itself
CGFloat minY = CGRectGetMinY(safeArea);
CGFloat maxY = CGRectGetMaxY(safeArea) - unconstrainedFrame.size.height;
if (unconstrainedFrame.origin.y < minY) {
unconstrainedFrame.origin.y = minY;
} else if (unconstrainedFrame.origin.y > maxY) {
unconstrainedFrame.origin.y = maxY;
}
self.explorerToolbar.frame = newToolbarFrame;
self.explorerToolbar.frame = unconstrainedFrame;
[[NSUserDefaults standardUserDefaults] setDouble:unconstrainedFrame.origin.y forKey:kFLEXToolbarTopMarginDefaultsKey];
}
- (void)handleToolbarHintTapGesture:(UITapGestureRecognizer *)tapGR
@@ -548,8 +534,8 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
// For outlined views and the selected view, only use visible views.
// Outlining hidden views adds clutter and makes the selection behavior confusing.
NSArray *visibleViewsAtTapPoint = [self viewsAtPoint:selectionPointInWindow skipHiddenViews:YES];
NSMutableDictionary *newOutlineViewsForVisibleViews = [NSMutableDictionary dictionary];
NSArray<UIView *> *visibleViewsAtTapPoint = [self viewsAtPoint:selectionPointInWindow skipHiddenViews:YES];
NSMutableDictionary<NSValue *, UIView *> *newOutlineViewsForVisibleViews = [NSMutableDictionary dictionary];
for (UIView *view in visibleViewsAtTapPoint) {
UIView *outlineView = [self outlineViewForView:view];
[self.view addSubview:outlineView];
@@ -569,25 +555,25 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
{
CGRect outlineFrame = [self frameInLocalCoordinatesForView:view];
UIView *outlineView = [[UIView alloc] initWithFrame:outlineFrame];
outlineView.backgroundColor = [UIColor clearColor];
outlineView.layer.borderColor = [[FLEXUtility consistentRandomColorForObject:view] CGColor];
outlineView.backgroundColor = UIColor.clearColor;
outlineView.layer.borderColor = [FLEXUtility consistentRandomColorForObject:view].CGColor;
outlineView.layer.borderWidth = 1.0;
return outlineView;
}
- (void)removeAndClearOutlineViews
{
for (id key in self.outlineViewsForVisibleViews) {
for (NSValue *key in self.outlineViewsForVisibleViews) {
UIView *outlineView = self.outlineViewsForVisibleViews[key];
[outlineView removeFromSuperview];
}
self.outlineViewsForVisibleViews = nil;
}
- (NSArray *)viewsAtPoint:(CGPoint)tapPointInWindow skipHiddenViews:(BOOL)skipHidden
- (NSArray<UIView *> *)viewsAtPoint:(CGPoint)tapPointInWindow skipHiddenViews:(BOOL)skipHidden
{
NSMutableArray *views = [NSMutableArray array];
for (UIWindow *window in [self allWindows]) {
NSMutableArray<UIView *> *views = [NSMutableArray array];
for (UIWindow *window in [FLEXUtility allWindows]) {
// Don't include the explorer's own window or subviews.
if (window != self.view.window && [window pointInside:tapPointInWindow withEvent:nil]) {
[views addObject:window];
@@ -601,8 +587,8 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
{
// Select in the window that would handle the touch, but don't just use the result of hitTest:withEvent: so we can still select views with interaction disabled.
// Default to the the application's key window if none of the windows want the touch.
UIWindow *windowForSelection = [[UIApplication sharedApplication] keyWindow];
for (UIWindow *window in [[self allWindows] reverseObjectEnumerator]) {
UIWindow *windowForSelection = [UIApplication.sharedApplication keyWindow];
for (UIWindow *window in [FLEXUtility allWindows].reverseObjectEnumerator) {
// Ignore the explorer's own window.
if (window != self.view.window) {
if ([window hitTest:tapPointInWindow withEvent:nil]) {
@@ -613,12 +599,12 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
}
// Select the deepest visible view at the tap point. This generally corresponds to what the user wants to select.
return [[self recursiveSubviewsAtPoint:tapPointInWindow inView:windowForSelection skipHiddenViews:YES] lastObject];
return [self recursiveSubviewsAtPoint:tapPointInWindow inView:windowForSelection skipHiddenViews:YES].lastObject;
}
- (NSArray *)recursiveSubviewsAtPoint:(CGPoint)pointInView inView:(UIView *)view skipHiddenViews:(BOOL)skipHidden
- (NSArray<UIView *> *)recursiveSubviewsAtPoint:(CGPoint)pointInView inView:(UIView *)view skipHiddenViews:(BOOL)skipHidden
{
NSMutableArray *subviewsAtPoint = [NSMutableArray array];
NSMutableArray<UIView *> *subviewsAtPoint = [NSMutableArray array];
for (UIView *subview in view.subviews) {
BOOL isHidden = subview.hidden || subview.alpha < 0.01;
if (skipHidden && isHidden) {
@@ -640,9 +626,9 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
return subviewsAtPoint;
}
- (NSArray *)allRecursiveSubviewsInView:(UIView *)view
- (NSArray<UIView *> *)allRecursiveSubviewsInView:(UIView *)view
{
NSMutableArray *subviews = [NSMutableArray array];
NSMutableArray<UIView *> *subviews = [NSMutableArray array];
for (UIView *subview in view.subviews) {
[subviews addObject:subview];
[subviews addObjectsFromArray:[self allRecursiveSubviewsInView:subview]];
@@ -650,9 +636,9 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
return subviews;
}
- (NSDictionary *)hierarchyDepthsForViews:(NSArray *)views
- (NSDictionary<NSValue *, NSNumber *> *)hierarchyDepthsForViews:(NSArray<UIView *> *)views
{
NSMutableDictionary *hierarchyDepths = [NSMutableDictionary dictionary];
NSMutableDictionary<NSValue *, NSNumber *> *hierarchyDepths = [NSMutableDictionary dictionary];
for (UIView *view in views) {
NSInteger depth = 0;
UIView *tryView = view;
@@ -696,6 +682,30 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
}
#pragma mark - Safe Area Handling
- (CGRect)viewSafeArea
{
CGRect safeArea = self.view.bounds;
if (@available(iOS 11.0, *)) {
safeArea = UIEdgeInsetsInsetRect(self.view.bounds, self.view.safeAreaInsets);
}
return safeArea;
}
- (void)viewSafeAreaInsetsDidChange
{
if (@available(iOS 11.0, *)) {
[super viewSafeAreaInsetsDidChange];
CGRect safeArea = [self viewSafeArea];
CGSize toolbarSize = [self.explorerToolbar sizeThatFits:CGSizeMake(CGRectGetWidth(self.view.bounds), CGRectGetHeight(safeArea))];
[self updateToolbarPositionWithUnconstrainedFrame:CGRectMake(CGRectGetMinX(self.explorerToolbar.frame), CGRectGetMinY(self.explorerToolbar.frame), toolbarSize.width, toolbarSize.height)];
}
}
#pragma mark - Touch Handling
- (BOOL)shouldReceiveTouchAtWindowPoint:(CGPoint)pointInWindowCoordinates
@@ -734,7 +744,7 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
{
// Note that we need to wait until the view controller is dismissed to calculated the frame of the outline view.
// Otherwise the coordinate conversion doesn't give the correct result.
[self resignKeyAndDismissViewControllerAnimated:YES completion:^{
[self toggleViewsToolWithCompletion:^{
// If the selected view is outside of the tap point array (selected from "Full Hierarchy"),
// then clear out the tap point array and remove all the outline views.
if (![self.viewsAtTapPoint containsObject:selectedView]) {
@@ -774,20 +784,28 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
- (void)makeKeyAndPresentViewController:(UIViewController *)viewController animated:(BOOL)animated completion:(void (^)(void))completion
{
// Save the current key window so we can restore it following dismissal.
self.previousKeyWindow = [[UIApplication sharedApplication] keyWindow];
self.previousKeyWindow = UIApplication.sharedApplication.keyWindow;
// Make our window key to correctly handle input.
[self.view.window makeKeyWindow];
// Fix for iOS 13, regarding custom UIMenu callouts not appearing because
// the UITextEffectsWindow has a lower level than the FLEX window by default
// until a text field is activated, bringing it above the FLEX window.
if (@available(iOS 13, *)) {
for (UIWindow *window in UIApplication.sharedApplication.windows) {
if ([window isKindOfClass:NSClassFromString(@"UITextEffectsWindow")]) {
if (window.windowLevel <= self.view.window.windowLevel) {
window.windowLevel = self.view.window.windowLevel + 1;
break;
}
}
}
}
// Move the status bar on top of FLEX so we can get scroll to top behavior for taps.
[[self statusWindow] setWindowLevel:self.view.window.windowLevel + 1.0];
// If this app doesn't use view controller based status bar management and we're on iOS 7+,
// make sure the status bar style is UIStatusBarStyleDefault. We don't actully have to check
// for view controller based management because the global methods no-op if that is turned on.
self.previousStatusBarStyle = [[UIApplication sharedApplication] statusBarStyle];
[[UIApplication sharedApplication] setStatusBarStyle:UIStatusBarStyleDefault];
// Show the view controller.
[self presentViewController:viewController animated:animated completion:completion];
}
@@ -803,9 +821,6 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
// We want it above FLEX while a modal is presented for scroll to top, but below FLEX otherwise for exploration.
[[self statusWindow] setWindowLevel:UIWindowLevelStatusBar];
// Restore the stauts bar style if the app is using global status bar management.
[[UIApplication sharedApplication] setStatusBarStyle:self.previousStatusBarStyle];
[self dismissViewControllerAnimated:animated completion:completion];
}
@@ -814,6 +829,15 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
return self.previousKeyWindow != nil;
}
- (void)toggleToolWithViewControllerProvider:(UIViewController *(^)(void))future completion:(void(^)(void))completion
{
if (self.presentedViewController) {
[self resignKeyAndDismissViewControllerAnimated:YES completion:completion];
} else if (future) {
[self makeKeyAndPresentViewController:future() animated:YES completion:completion];
}
}
#pragma mark - Keyboard Shortcut Helpers
- (void)toggleSelectTool
@@ -836,44 +860,41 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
- (void)toggleViewsTool
{
BOOL viewsModalShown = [[self presentedViewController] isKindOfClass:[UINavigationController class]];
viewsModalShown = viewsModalShown && [[[(UINavigationController *)[self presentedViewController] viewControllers] firstObject] isKindOfClass:[FLEXHierarchyTableViewController class]];
if (viewsModalShown) {
[self resignKeyAndDismissViewControllerAnimated:YES completion:nil];
} else {
[self resignKeyAndDismissViewControllerAnimated:NO completion:nil];
NSArray *allViews = [self allViewsInHierarchy];
[self toggleViewsToolWithCompletion:nil];
}
- (void)toggleViewsToolWithCompletion:(void(^)(void))completion
{
[self toggleToolWithViewControllerProvider:^UIViewController *{
NSArray<UIView *> *allViews = [self allViewsInHierarchy];
NSDictionary *depthsForViews = [self hierarchyDepthsForViews:allViews];
FLEXHierarchyTableViewController *hierarchyTVC = [[FLEXHierarchyTableViewController alloc] initWithViews:allViews viewsAtTap:self.viewsAtTapPoint selectedView:self.selectedView depths:depthsForViews];
hierarchyTVC.delegate = self;
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:hierarchyTVC];
[self makeKeyAndPresentViewController:navigationController animated:YES completion:nil];
}
return [[UINavigationController alloc] initWithRootViewController:hierarchyTVC];
} completion:^{
if (completion) {
completion();
}
}];
}
- (void)toggleMenuTool
{
BOOL menuModalShown = [[self presentedViewController] isKindOfClass:[UINavigationController class]];
menuModalShown = menuModalShown && [[[(UINavigationController *)[self presentedViewController] viewControllers] firstObject] isKindOfClass:[FLEXGlobalsTableViewController class]];
if (menuModalShown) {
[self resignKeyAndDismissViewControllerAnimated:YES completion:nil];
} else {
[self resignKeyAndDismissViewControllerAnimated:NO completion:nil];
FLEXGlobalsTableViewController *globalsViewController = [[FLEXGlobalsTableViewController alloc] init];
[self toggleToolWithViewControllerProvider:^UIViewController *{
FLEXGlobalsTableViewController *globalsViewController = [FLEXGlobalsTableViewController new];
globalsViewController.delegate = self;
[FLEXGlobalsTableViewController setApplicationWindow:[[UIApplication sharedApplication] keyWindow]];
UINavigationController *navigationController = [[UINavigationController alloc] initWithRootViewController:globalsViewController];
[self makeKeyAndPresentViewController:navigationController animated:YES completion:nil];
}
[FLEXGlobalsTableViewController setApplicationWindow:[UIApplication.sharedApplication keyWindow]];
return [[UINavigationController alloc] initWithRootViewController:globalsViewController];
} completion:nil];
}
- (void)handleDownArrowKeyPressed
{
if (self.currentMode == FLEXExplorerModeMove) {
CGRect frame = self.selectedView.frame;
frame.origin.y += 1.0 / [[UIScreen mainScreen] scale];
frame.origin.y += 1.0 / UIScreen.mainScreen.scale;
self.selectedView.frame = frame;
} else if (self.currentMode == FLEXExplorerModeSelect && [self.viewsAtTapPoint count] > 0) {
} else if (self.currentMode == FLEXExplorerModeSelect && self.viewsAtTapPoint.count > 0) {
NSInteger selectedViewIndex = [self.viewsAtTapPoint indexOfObject:self.selectedView];
if (selectedViewIndex > 0) {
self.selectedView = [self.viewsAtTapPoint objectAtIndex:selectedViewIndex - 1];
@@ -885,11 +906,11 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
{
if (self.currentMode == FLEXExplorerModeMove) {
CGRect frame = self.selectedView.frame;
frame.origin.y -= 1.0 / [[UIScreen mainScreen] scale];
frame.origin.y -= 1.0 / UIScreen.mainScreen.scale;
self.selectedView.frame = frame;
} else if (self.currentMode == FLEXExplorerModeSelect && [self.viewsAtTapPoint count] > 0) {
} else if (self.currentMode == FLEXExplorerModeSelect && self.viewsAtTapPoint.count > 0) {
NSInteger selectedViewIndex = [self.viewsAtTapPoint indexOfObject:self.selectedView];
if (selectedViewIndex < [self.viewsAtTapPoint count] - 1) {
if (selectedViewIndex < self.viewsAtTapPoint.count - 1) {
self.selectedView = [self.viewsAtTapPoint objectAtIndex:selectedViewIndex + 1];
}
}
@@ -899,7 +920,7 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
{
if (self.currentMode == FLEXExplorerModeMove) {
CGRect frame = self.selectedView.frame;
frame.origin.x += 1.0 / [[UIScreen mainScreen] scale];
frame.origin.x += 1.0 / UIScreen.mainScreen.scale;
self.selectedView.frame = frame;
}
}
@@ -908,7 +929,7 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
{
if (self.currentMode == FLEXExplorerModeMove) {
CGRect frame = self.selectedView.frame;
frame.origin.x -= 1.0 / [[UIScreen mainScreen] scale];
frame.origin.x -= 1.0 / UIScreen.mainScreen.scale;
self.selectedView.frame = frame;
}
}
@@ -15,7 +15,7 @@
{
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = [UIColor clearColor];
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.
@@ -49,7 +49,7 @@
// 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));
@@ -1,16 +0,0 @@
//
// FLEXManager+Private.h
// PebbleApp
//
// Created by Javier Soto on 7/26/14.
// Copyright (c) 2014 Pebble Technology. All rights reserved.
//
#import "FLEXManager.h"
@interface FLEXManager ()
/// An array of FLEXGlobalsTableViewControllerEntry objects that have been registered by the user.
@property (nonatomic, readonly, strong) NSArray *userGlobalEntries;
@end
-8
View File
@@ -6,12 +6,4 @@
// Copyright (c) 2015 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
//! Project version number for FLEX.
FOUNDATION_EXPORT double FLEXVersionNumber;
//! Project version string for FLEX.
FOUNDATION_EXPORT const unsigned char FLEXVersionString[];
#import <FLEX/FLEXManager.h>
@@ -9,6 +9,12 @@
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#if !FLEX_AT_LEAST_IOS13_SDK
@class UIWindowScene;
#endif
typedef UIViewController *(^FLEXCustomContentViewerFuture)(NSData *data);
@interface FLEXManager : NSObject
+ (instancetype)sharedManager;
@@ -19,16 +25,26 @@
- (void)hideExplorer;
- (void)toggleExplorer;
/// 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));
#pragma mark - Network Debugging
/// If this property is set to YES, FLEX will swizzle NSURLConnection*Delegate and NSURLSession*Delegate methods
/// on classes that conform to the protocols. This allows you to view network activity history from the main FLEX menu.
/// Full responses are kept temporarily in a size limited cache and may be pruged under memory pressure.
@property (nonatomic, assign, getter=isNetworkDebuggingEnabled) BOOL networkDebuggingEnabled;
/// Full responses are kept temporarily in a size-limited cache and may be pruned under memory pressure.
@property (nonatomic, getter=isNetworkDebuggingEnabled) BOOL networkDebuggingEnabled;
/// Defaults to 25 MB if never set. Values set here are presisted across launches of the app.
/// Defaults to 25 MB if never set. Values set here are persisted across launches of the app.
/// The response cache uses an NSCache, so it may purge prior to hitting the limit when the app is under memory pressure.
@property (nonatomic, assign) NSUInteger networkResponseCacheByteLimit;
@property (nonatomic) NSUInteger networkResponseCacheByteLimit;
/// Requests whose host ends with one of the blacklisted entries in this array will be not be recorded (eg. google.com).
/// Wildcard or subdomain entries are not required (eg. google.com will match any subdomain under google.com).
/// Useful to remove requests that are typically noisy, such as analytics requests that you aren't interested in tracking.
@property (nonatomic, copy) NSArray<NSString *> *networkRequestHostBlacklist;
#pragma mark - Keyboard Shortcuts
@@ -36,7 +52,7 @@
/// 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;
@property (nonatomic) BOOL simulatorShortcutsEnabled;
/// Adds an action to run when the specified key & modifier combination is pressed
/// @param key A single character string matching a key on the keyboard
@@ -49,6 +65,10 @@
#pragma mark - Extensions
/// Default database password is @c nil by default.
/// Set this to the password you want the databases to open with.
@property (copy, nonatomic) NSString *defaultSqliteDatabasePassword;
/// Adds an entry at the bottom of the list of Global State items. Call this method before this view controller is displayed.
/// @param entryName The string to be displayed in the cell.
/// @param objectFutureBlock When you tap on the row, information about the object returned by this block will be displayed.
@@ -67,4 +87,13 @@
- (void)registerGlobalEntryWithName:(NSString *)entryName
viewControllerFutureBlock:(UIViewController * (^)(void))viewControllerFutureBlock;
/// Sets custom viewer for specific content type.
/// @param contentType Mime type like application/json
/// @param viewControllerFutureBlock Viewer (view controller) creation block
/// @note This method must be called from the main thread.
/// The viewControllerFutureBlock will be invoked from the main thread and may not return nil.
/// @note The passed block will be copied and retain for the duration of the application, you may want to use __weak references.
- (void)setCustomViewerForContentType:(NSString *)contentType
viewControllerFutureBlock:(FLEXCustomContentViewerFuture)viewControllerFutureBlock;
@end
@@ -0,0 +1,26 @@
//
// PTDatabaseManager.h
// Derived from:
//
// FMDatabase.h
// FMDB( https://github.com/ccgus/fmdb )
//
// Created by Peng Tao on 15/11/23.
//
// Licensed to Flying Meat Inc. under one or more contributor license agreements.
// See the LICENSE file distributed with this work for the terms under
// which Flying Meat Inc. licenses this file to you.
#import <Foundation/Foundation.h>
@protocol FLEXDatabaseManager <NSObject>
@required
- (instancetype)initWithPath:(NSString*)path;
- (BOOL)open;
- (NSArray<NSDictionary<NSString *, id> *> *)queryAllTables;
- (NSArray<NSString *> *)queryAllColumnsWithTableName:(NSString *)tableName;
- (NSArray<NSDictionary<NSString *, id> *> *)queryAllDataWithTableName:(NSString *)tableName;
@end
@@ -0,0 +1,48 @@
//
// PTMultiColumnTableView.h
// PTMultiColumnTableViewDemo
//
// Created by Peng Tao on 15/11/16.
// Copyright © 2015年 Peng Tao. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "FLEXTableColumnHeader.h"
@class FLEXMultiColumnTableView;
@protocol FLEXMultiColumnTableViewDelegate <NSObject>
@required
- (void)multiColumnTableView:(FLEXMultiColumnTableView *)tableView didTapLabelWithText:(NSString *)text;
- (void)multiColumnTableView:(FLEXMultiColumnTableView *)tableView didTapHeaderWithText:(NSString *)text sortType:(FLEXTableColumnHeaderSortType)sortType;
@end
@protocol FLEXMultiColumnTableViewDataSource <NSObject>
@required
- (NSInteger)numberOfColumnsInTableView:(FLEXMultiColumnTableView *)tableView;
- (NSInteger)numberOfRowsInTableView:(FLEXMultiColumnTableView *)tableView;
- (NSString *)columnNameInColumn:(NSInteger)column;
- (NSString *)rowNameInRow:(NSInteger)row;
- (NSString *)contentAtColumn:(NSInteger)column row:(NSInteger)row;
- (NSArray *)contentAtRow:(NSInteger)row;
- (CGFloat)multiColumnTableView:(FLEXMultiColumnTableView *)tableView widthForContentCellInColumn:(NSInteger)column;
- (CGFloat)multiColumnTableView:(FLEXMultiColumnTableView *)tableView heightForContentCellInRow:(NSInteger)row;
- (CGFloat)heightForTopHeaderInTableView:(FLEXMultiColumnTableView *)tableView;
- (CGFloat)widthForLeftHeaderInTableView:(FLEXMultiColumnTableView *)tableView;
@end
@interface FLEXMultiColumnTableView : UIView
@property (nonatomic, weak) id<FLEXMultiColumnTableViewDataSource>dataSource;
@property (nonatomic, weak) id<FLEXMultiColumnTableViewDelegate>delegate;
- (void)reloadData;
@end
@@ -0,0 +1,346 @@
//
// PTMultiColumnTableView.m
// PTMultiColumnTableViewDemo
//
// Created by Peng Tao on 15/11/16.
// Copyright © 2015年 Peng Tao. All rights reserved.
//
#import "FLEXMultiColumnTableView.h"
#import "FLEXTableContentCell.h"
#import "FLEXTableLeftCell.h"
@interface FLEXMultiColumnTableView ()
<UITableViewDataSource, UITableViewDelegate,UIScrollViewDelegate, FLEXTableContentCellDelegate>
@property (nonatomic) UIScrollView *contentScrollView;
@property (nonatomic) UIScrollView *headerScrollView;
@property (nonatomic) UITableView *leftTableView;
@property (nonatomic) UITableView *contentTableView;
@property (nonatomic) UIView *leftHeader;
@property (nonatomic) NSDictionary<NSString *, NSNumber *> *sortStatusDict;
@property (nonatomic) NSArray *rowData;
@end
static const CGFloat kColumnMargin = 1;
@implementation FLEXMultiColumnTableView
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self loadUI];
}
return self;
}
- (void)didMoveToSuperview
{
[super didMoveToSuperview];
[self reloadData];
}
- (void)layoutSubviews
{
[super layoutSubviews];
CGFloat width = self.frame.size.width;
CGFloat height = self.frame.size.height;
CGFloat topheaderHeight = [self topHeaderHeight];
CGFloat leftHeaderWidth = [self leftHeaderWidth];
CGFloat topInsets = 0.f;
if (@available (iOS 11.0, *)) {
topInsets = self.safeAreaInsets.top;
}
CGFloat contentWidth = 0.0;
NSInteger rowsCount = [self numberOfColumns];
for (int i = 0; i < rowsCount; i++) {
contentWidth += [self contentWidthForColumn:i];
}
self.leftTableView.frame = CGRectMake(0, topheaderHeight + 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 - 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, topInsets, [self leftHeaderWidth], [self topHeaderHeight]);
}
- (void)loadUI
{
[self loadHeaderScrollView];
[self loadContentScrollView];
[self loadLeftView];
}
- (void)reloadData
{
[self loadLeftViewData];
[self loadContentData];
[self loadHeaderData];
}
#pragma mark - UI
- (void)loadHeaderScrollView
{
UIScrollView *headerScrollView = [UIScrollView new];
headerScrollView.delegate = self;
self.headerScrollView = headerScrollView;
self.headerScrollView.backgroundColor = [UIColor colorWithWhite:0.803 alpha:0.850];
[self addSubview:headerScrollView];
}
- (void)loadContentScrollView
{
UIScrollView *scrollView = [UIScrollView new];
scrollView.bounces = NO;
scrollView.delegate = self;
UITableView *tableView = [UITableView new];
tableView.delegate = self;
tableView.dataSource = self;
tableView.separatorStyle = UITableViewCellSeparatorStyleNone;
[self addSubview:scrollView];
[scrollView addSubview:tableView];
self.contentScrollView = scrollView;
self.contentTableView = tableView;
}
- (void)loadLeftView
{
UITableView *leftTableView = [UITableView new];
leftTableView.delegate = self;
leftTableView.dataSource = self;
leftTableView.separatorStyle = UITableViewCellSeparatorStyleNone;
self.leftTableView = leftTableView;
[self addSubview:leftTableView];
UIView *leftHeader = [UIView new];
leftHeader.backgroundColor = [UIColor colorWithWhite:0.950 alpha:0.668];
self.leftHeader = leftHeader;
[self addSubview:leftHeader];
}
#pragma mark - Data
- (void)loadHeaderData
{
NSArray<UIView *> *subviews = self.headerScrollView.subviews;
for (UIView *subview in subviews) {
[subview removeFromSuperview];
}
CGFloat x = 0.0;
CGFloat w = 0.0;
for (int i = 0; i < [self numberOfColumns] ; i++) {
w = [self contentWidthForColumn:i] + [self columnMargin];
FLEXTableColumnHeader *cell = [[FLEXTableColumnHeader alloc] initWithFrame:CGRectMake(x, 0, w, [self topHeaderHeight] - 1)];
cell.label.text = [self columnTitleForColumn:i];
[self.headerScrollView addSubview:cell];
FLEXTableColumnHeaderSortType type = [self.sortStatusDict[[self columnTitleForColumn:i]] integerValue];
[cell changeSortStatusWithType:type];
UITapGestureRecognizer *gesture = [[UITapGestureRecognizer alloc] initWithTarget:self
action:@selector(contentHeaderTap:)];
[cell addGestureRecognizer:gesture];
cell.userInteractionEnabled = YES;
x = x + w;
}
}
- (void)contentHeaderTap:(UIGestureRecognizer *)gesture
{
FLEXTableColumnHeader *header = (FLEXTableColumnHeader *)gesture.view;
NSString *string = header.label.text;
FLEXTableColumnHeaderSortType currentType = [self.sortStatusDict[string] integerValue];
FLEXTableColumnHeaderSortType newType ;
switch (currentType) {
case FLEXTableColumnHeaderSortTypeNone:
newType = FLEXTableColumnHeaderSortTypeAsc;
break;
case FLEXTableColumnHeaderSortTypeAsc:
newType = FLEXTableColumnHeaderSortTypeDesc;
break;
case FLEXTableColumnHeaderSortTypeDesc:
newType = FLEXTableColumnHeaderSortTypeAsc;
break;
}
self.sortStatusDict = @{header.label.text : @(newType)};
[header changeSortStatusWithType:newType];
[self.delegate multiColumnTableView:self didTapHeaderWithText:string sortType:newType];
}
- (void)loadContentData
{
[self.contentTableView reloadData];
}
- (void)loadLeftViewData
{
[self.leftTableView reloadData];
}
- (UITableViewCell *)tableView:(UITableView *)tableView
cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UIColor *backgroundColor = UIColor.whiteColor;
if (indexPath.row % 2 != 0) {
backgroundColor = [UIColor colorWithWhite:0.950 alpha:0.750];
}
if (tableView != self.leftTableView) {
self.rowData = [self.dataSource contentAtRow:indexPath.row];
FLEXTableContentCell *cell = [FLEXTableContentCell cellWithTableView:tableView
columnNumber:[self numberOfColumns]];
cell.contentView.backgroundColor = backgroundColor;
cell.delegate = self;
for (int i = 0 ; i < cell.labels.count; i++) {
UILabel *label = cell.labels[i];
label.textColor = UIColor.blackColor;
NSString *content = [NSString stringWithFormat:@"%@",self.rowData[i]];
if ([content isEqualToString:@"<null>"]) {
label.textColor = UIColor.lightGrayColor;
content = @"NULL";
}
label.text = content;
label.backgroundColor = backgroundColor;
}
return cell;
}
else {
FLEXTableLeftCell *cell = [FLEXTableLeftCell cellWithTableView:tableView];
cell.contentView.backgroundColor = backgroundColor;
cell.titlelabel.text = [self rowTitleForRow:indexPath.row];
return cell;
}
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [self.dataSource numberOfRowsInTableView:self];
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return [self.dataSource multiColumnTableView:self heightForContentCellInRow:indexPath.row];
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
if (scrollView == self.contentScrollView) {
self.headerScrollView.contentOffset = scrollView.contentOffset;
}
else if (scrollView == self.headerScrollView) {
self.contentScrollView.contentOffset = scrollView.contentOffset;
}
else if (scrollView == self.leftTableView) {
self.contentTableView.contentOffset = scrollView.contentOffset;
}
else if (scrollView == self.contentTableView) {
self.leftTableView.contentOffset = scrollView.contentOffset;
}
}
#pragma mark -
#pragma mark UITableView Delegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
if (tableView == self.leftTableView) {
[self.contentTableView selectRowAtIndexPath:indexPath
animated:NO
scrollPosition:UITableViewScrollPositionNone];
}
else if (tableView == self.contentTableView) {
[self.leftTableView selectRowAtIndexPath:indexPath
animated:NO
scrollPosition:UITableViewScrollPositionNone];
}
}
#pragma mark -
#pragma mark DataSource Accessor
- (NSInteger)numberOfRows
{
return [self.dataSource numberOfRowsInTableView:self];
}
- (NSInteger)numberOfColumns
{
return [self.dataSource numberOfColumnsInTableView:self];
}
- (NSString *)columnTitleForColumn:(NSInteger)column
{
return [self.dataSource columnNameInColumn:column];
}
- (NSString *)rowTitleForRow:(NSInteger)row
{
return [self.dataSource rowNameInRow:row];
}
- (NSString *)contentAtColumn:(NSInteger)column row:(NSInteger)row;
{
return [self.dataSource contentAtColumn:column row:row];
}
- (CGFloat)contentWidthForColumn:(NSInteger)column
{
return [self.dataSource multiColumnTableView:self widthForContentCellInColumn:column];
}
- (CGFloat)contentHeightForRow:(NSInteger)row
{
return [self.dataSource multiColumnTableView:self heightForContentCellInRow:row];
}
- (CGFloat)topHeaderHeight
{
return [self.dataSource heightForTopHeaderInTableView:self];
}
- (CGFloat)leftHeaderWidth
{
return [self.dataSource widthForLeftHeaderInTableView:self];
}
- (CGFloat)columnMargin
{
return kColumnMargin;
}
- (void)tableContentCell:(FLEXTableContentCell *)tableView labelDidTapWithText:(NSString *)text
{
[self.delegate multiColumnTableView:self didTapLabelWithText:text];
}
@end
@@ -0,0 +1,14 @@
//
// FLEXRealmDatabaseManager.h
// FLEX
//
// Created by Tim Oliver on 28/01/2016.
// Copyright © 2016 Realm. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "FLEXDatabaseManager.h"
@interface FLEXRealmDatabaseManager : NSObject <FLEXDatabaseManager>
@end
@@ -0,0 +1,114 @@
//
// FLEXRealmDatabaseManager.m
// FLEX
//
// Created by Tim Oliver on 28/01/2016.
// Copyright © 2016 Realm. All rights reserved.
//
#import "FLEXRealmDatabaseManager.h"
#if __has_include(<Realm/Realm.h>)
#import <Realm/Realm.h>
#import <Realm/RLMRealm_Dynamic.h>
#else
#import "FLEXRealmDefines.h"
#endif
@interface FLEXRealmDatabaseManager ()
@property (nonatomic, copy) NSString *path;
@property (nonatomic) RLMRealm * realm;
@end
//#endif
@implementation FLEXRealmDatabaseManager
- (instancetype)initWithPath:(NSString*)aPath
{
Class realmClass = NSClassFromString(@"RLMRealm");
if (realmClass == nil) {
return nil;
}
self = [super init];
if (self) {
_path = aPath;
}
return self;
}
- (BOOL)open
{
Class realmClass = NSClassFromString(@"RLMRealm");
Class configurationClass = NSClassFromString(@"RLMRealmConfiguration");
if (realmClass == nil || configurationClass == nil) {
return NO;
}
NSError *error = nil;
id configuration = [configurationClass new];
[(RLMRealmConfiguration *)configuration setFileURL:[NSURL fileURLWithPath:self.path]];
self.realm = [realmClass realmWithConfiguration:configuration error:&error];
return (error == nil);
}
- (NSArray<NSDictionary<NSString *, id> *> *)queryAllTables
{
NSMutableArray<NSDictionary<NSString *, id> *> *allTables = [NSMutableArray array];
RLMSchema *schema = [self.realm schema];
for (RLMObjectSchema *objectSchema in schema.objectSchema) {
if (objectSchema.className == nil) {
continue;
}
NSDictionary<NSString *, id> *dictionary = @{@"name":objectSchema.className};
[allTables addObject:dictionary];
}
return allTables;
}
- (NSArray<NSString *> *)queryAllColumnsWithTableName:(NSString *)tableName
{
RLMObjectSchema *objectSchema = [[self.realm schema] schemaForClassName:tableName];
if (objectSchema == nil) {
return nil;
}
NSMutableArray<NSString *> *columnNames = [NSMutableArray array];
for (RLMProperty *property in objectSchema.properties) {
[columnNames addObject:property.name];
}
return columnNames;
}
- (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<NSDictionary<NSString *, id> *> *allDataEntries = [NSMutableArray array];
for (RLMObject *result in results) {
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];
}
[allDataEntries addObject:entry];
}
return allDataEntries;
}
@end
@@ -0,0 +1,46 @@
//
// Realm.h
// FLEX
//
// Created by Tim Oliver on 16/02/2016.
// Copyright © 2016 Realm. All rights reserved.
//
#if __has_include(<Realm/Realm.h>)
#else
@class RLMObject, RLMResults, RLMRealm, RLMRealmConfiguration, RLMSchema, RLMObjectSchema, RLMProperty;
@interface RLMRealmConfiguration : NSObject
@property (nonatomic, copy) NSURL *fileURL;
@end
@interface RLMRealm : NSObject
@property (nonatomic, readonly) RLMSchema *schema;
+ (RLMRealm *)realmWithConfiguration:(RLMRealmConfiguration *)configuration error:(NSError **)error;
- (RLMResults *)allObjects:(NSString *)className;
@end
@interface RLMSchema : NSObject
@property (nonatomic, readonly) NSArray<RLMObjectSchema *> *objectSchema;
- (RLMObjectSchema *)schemaForClassName:(NSString *)className;
@end
@interface RLMObjectSchema : NSObject
@property (nonatomic, readonly) NSString *className;
@property (nonatomic, readonly) NSArray<RLMProperty *> *properties;
@end
@interface RLMProperty : NSString
@property (nonatomic, readonly) NSString *name;
@end
@interface RLMResults : NSObject <NSFastEnumeration>
@property (nonatomic, readonly) NSInteger count;
@end
@interface RLMObject : NSObject
@end
#endif
@@ -0,0 +1,19 @@
//
// PTDatabaseManager.h
// Derived from:
//
// FMDatabase.h
// FMDB( https://github.com/ccgus/fmdb )
//
// Created by Peng Tao on 15/11/23.
//
// Licensed to Flying Meat Inc. under one or more contributor license agreements.
// See the LICENSE file distributed with this work for the terms under
// which Flying Meat Inc. licenses this file to you.
#import <Foundation/Foundation.h>
#import "FLEXDatabaseManager.h"
@interface FLEXSQLiteDatabaseManager : NSObject <FLEXDatabaseManager>
@end
@@ -0,0 +1,203 @@
//
// PTDatabaseManager.m
// PTDatabaseReader
//
// Created by Peng Tao on 15/11/23.
// Copyright © 2015年 Peng Tao. All rights reserved.
//
#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
{
sqlite3* _db;
NSString* _databasePath;
}
- (instancetype)initWithPath:(NSString*)aPath
{
self = [super init];
if (self) {
_databasePath = [aPath copy];
}
return self;
}
- (BOOL)open {
if (_db) {
return YES;
}
int err = sqlite3_open(_databasePath.UTF8String, &_db);
#if 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;
}
return YES;
}
- (BOOL)close {
if (!_db) {
return YES;
}
int rc;
BOOL retry;
BOOL triedFinalizingOpenStatements = NO;
do {
retry = NO;
rc = sqlite3_close(_db);
if (SQLITE_BUSY == rc || SQLITE_LOCKED == rc) {
if (!triedFinalizingOpenStatements) {
triedFinalizingOpenStatements = YES;
sqlite3_stmt *pStmt;
while ((pStmt = sqlite3_next_stmt(_db, nil)) !=0) {
NSLog(@"Closing leaked statement");
sqlite3_finalize(pStmt);
retry = YES;
}
}
}
else if (SQLITE_OK != rc) {
NSLog(@"error closing!: %d", rc);
}
}
while (retry);
_db = nil;
return YES;
}
- (NSArray<NSDictionary<NSString *, id> *> *)queryAllTables
{
return [self executeQuery:QUERY_TABLENAMES_SQL];
}
- (NSArray<NSString *> *)queryAllColumnsWithTableName:(NSString *)tableName
{
NSString *sql = [NSString stringWithFormat:@"PRAGMA table_info('%@')",tableName];
NSArray<NSDictionary<NSString *, id> *> *resultArray = [self executeQuery:sql];
NSMutableArray<NSString *> *array = [NSMutableArray array];
for (NSDictionary<NSString *, id> *dict in resultArray) {
NSString *columnName = (NSString *)dict[@"name"] ?: @"";
[array addObject:columnName];
}
return array;
}
- (NSArray<NSDictionary<NSString *, id> *> *)queryAllDataWithTableName:(NSString *)tableName
{
NSString *sql = [NSString stringWithFormat:@"SELECT * FROM %@",tableName];
return [self executeQuery:sql];
}
#pragma mark -
#pragma mark - Private
- (NSArray<NSDictionary<NSString *, id> *> *)executeQuery:(NSString *)sql
{
[self open];
NSMutableArray<NSDictionary<NSString *, id> *> *resultArray = [NSMutableArray array];
sqlite3_stmt *pstmt;
if (sqlite3_prepare_v2(_db, sql.UTF8String, -1, &pstmt, 0) == SQLITE_OK) {
while (sqlite3_step(pstmt) == SQLITE_ROW) {
NSUInteger num_cols = (NSUInteger)sqlite3_data_count(pstmt);
if (num_cols > 0) {
NSMutableDictionary<NSString *, id> *dict = [NSMutableDictionary dictionaryWithCapacity:num_cols];
int columnCount = sqlite3_column_count(pstmt);
int columnIdx = 0;
for (columnIdx = 0; columnIdx < columnCount; columnIdx++) {
NSString *columnName = [NSString stringWithUTF8String:sqlite3_column_name(pstmt, columnIdx)];
id objectValue = [self objectForColumnIndex:columnIdx stmt:pstmt];
[dict setObject:objectValue forKey:columnName];
}
[resultArray addObject:dict];
}
}
}
[self close];
return resultArray;
}
- (id)objectForColumnIndex:(int)columnIdx stmt:(sqlite3_stmt*)stmt {
int columnType = sqlite3_column_type(stmt, columnIdx);
id returnValue = nil;
if (columnType == SQLITE_INTEGER) {
returnValue = [NSNumber numberWithLongLong:sqlite3_column_int64(stmt, columnIdx)];
}
else if (columnType == SQLITE_FLOAT) {
returnValue = [NSNumber numberWithDouble:sqlite3_column_double(stmt, columnIdx)];
}
else if (columnType == SQLITE_BLOB) {
returnValue = [self dataForColumnIndex:columnIdx stmt:stmt];
}
else {
//default to a string for everything else
returnValue = [self stringForColumnIndex:columnIdx stmt:stmt];
}
if (returnValue == nil) {
returnValue = [NSNull null];
}
return returnValue;
}
- (NSString *)stringForColumnIndex:(int)columnIdx stmt:(sqlite3_stmt *)stmt {
if (sqlite3_column_type(stmt, columnIdx) == SQLITE_NULL || (columnIdx < 0)) {
return nil;
}
const char *c = (const char *)sqlite3_column_text(stmt, columnIdx);
if (!c) {
// null row.
return nil;
}
return [NSString stringWithUTF8String:c];
}
- (NSData *)dataForColumnIndex:(int)columnIdx stmt:(sqlite3_stmt *)stmt{
if (sqlite3_column_type(stmt, columnIdx) == SQLITE_NULL || (columnIdx < 0)) {
return nil;
}
const char *dataBuffer = sqlite3_column_blob(stmt, columnIdx);
int dataSize = sqlite3_column_bytes(stmt, columnIdx);
if (dataBuffer == NULL) {
return nil;
}
return [NSData dataWithBytes:(const void *)dataBuffer length:(NSUInteger)dataSize];
}
@end
@@ -0,0 +1,24 @@
//
// FLEXTableContentHeaderCell.h
// FLEX
//
// Created by Peng Tao on 15/11/26.
// Copyright © 2015年 f. All rights reserved.
//
#import <UIKit/UIKit.h>
typedef NS_ENUM(NSUInteger, FLEXTableColumnHeaderSortType) {
FLEXTableColumnHeaderSortTypeNone = 0,
FLEXTableColumnHeaderSortTypeAsc,
FLEXTableColumnHeaderSortTypeDesc,
};
@interface FLEXTableColumnHeader : UIView
@property (nonatomic) UILabel *label;
- (void)changeSortStatusWithType:(FLEXTableColumnHeaderSortType)type;
@end
@@ -0,0 +1,60 @@
//
// FLEXTableContentHeaderCell.m
// FLEX
//
// Created by Peng Tao on 15/11/26.
// Copyright © 2015年 f. All rights reserved.
//
#import "FLEXTableColumnHeader.h"
@implementation FLEXTableColumnHeader
{
UILabel *_arrowLabel;
}
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = UIColor.whiteColor;
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(5, 0, frame.size.width - 25, frame.size.height)];
label.font = [UIFont systemFontOfSize:13.0];
[self addSubview:label];
self.label = label;
_arrowLabel = [[UILabel alloc] initWithFrame:CGRectMake(frame.size.width - 20, 0, 20, frame.size.height)];
_arrowLabel.font = [UIFont systemFontOfSize:13.0];
[self addSubview:_arrowLabel];
UIView *line = [[UIView alloc] initWithFrame:CGRectMake(frame.size.width - 1, 2, 1, frame.size.height - 4)];
line.backgroundColor = [UIColor colorWithWhite:0.803 alpha:0.850];
[self addSubview:line];
}
return self;
}
- (void)changeSortStatusWithType:(FLEXTableColumnHeaderSortType)type
{
switch (type) {
case FLEXTableColumnHeaderSortTypeNone:
_arrowLabel.text = @"";
break;
case FLEXTableColumnHeaderSortTypeAsc:
_arrowLabel.text = @"⬆️";
break;
case FLEXTableColumnHeaderSortTypeDesc:
_arrowLabel.text = @"⬇️";
break;
}
}
@end
@@ -0,0 +1,27 @@
//
// FLEXTableContentCell.h
// FLEX
//
// Created by Peng Tao on 15/11/24.
// Copyright © 2015年 f. All rights reserved.
//
#import <UIKit/UIKit.h>
@class FLEXTableContentCell;
@protocol FLEXTableContentCellDelegate <NSObject>
@optional
- (void)tableContentCell:(FLEXTableContentCell *)tableView labelDidTapWithText:(NSString *)text;
@end
@interface FLEXTableContentCell : UITableViewCell
@property (nonatomic) NSArray<UILabel *> *labels;
@property (nonatomic, weak) id<FLEXTableContentCellDelegate> delegate;
+ (instancetype)cellWithTableView:(UITableView *)tableView columnNumber:(NSInteger)number;
@end
@@ -0,0 +1,66 @@
//
// FLEXTableContentCell.m
// FLEX
//
// Created by Peng Tao on 15/11/24.
// Copyright © 2015年 f. All rights reserved.
//
#import "FLEXTableContentCell.h"
#import "FLEXMultiColumnTableView.h"
@interface FLEXTableContentCell ()
@end
@implementation FLEXTableContentCell
+ (instancetype)cellWithTableView:(UITableView *)tableView columnNumber:(NSInteger)number;
{
static NSString *identifier = @"FLEXTableContentCell";
FLEXTableContentCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
if (!cell) {
cell = [[FLEXTableContentCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
NSMutableArray<UILabel *> *labels = [NSMutableArray array];
for (int i = 0; i < number ; i++) {
UILabel *label = [[UILabel alloc] initWithFrame:CGRectZero];
label.backgroundColor = UIColor.whiteColor;
label.font = [UIFont systemFontOfSize:13.0];
label.textAlignment = NSTextAlignmentLeft;
label.backgroundColor = UIColor.greenColor;
[labels addObject:label];
UITapGestureRecognizer *gesture = [[UITapGestureRecognizer alloc] initWithTarget:cell
action:@selector(labelDidTap:)];
[label addGestureRecognizer:gesture];
label.userInteractionEnabled = YES;
[cell.contentView addSubview:label];
cell.contentView.backgroundColor = UIColor.whiteColor;
}
cell.labels = labels;
}
return cell;
}
- (void)layoutSubviews
{
[super layoutSubviews];
CGFloat labelWidth = self.contentView.frame.size.width / self.labels.count;
CGFloat labelHeight = self.contentView.frame.size.height;
for (int i = 0; i < self.labels.count; i++) {
UILabel *label = self.labels[i];
label.frame = CGRectMake(labelWidth * i + 5, 0, (labelWidth - 10), labelHeight);
}
}
- (void)labelDidTap:(UIGestureRecognizer *)gesture
{
UILabel *label = (UILabel *)gesture.view;
if ([self.delegate respondsToSelector:@selector(tableContentCell:labelDidTapWithText:)]) {
[self.delegate tableContentCell:self labelDidTapWithText:label.text];
}
}
@end
@@ -0,0 +1,16 @@
//
// PTTableContentViewController.h
// PTDatabaseReader
//
// Created by Peng Tao on 15/11/23.
// Copyright © 2015年 Peng Tao. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface FLEXTableContentViewController : UIViewController
@property (nonatomic) NSArray<NSString *> *columnsArray;
@property (nonatomic) NSArray<NSDictionary<NSString *, id> *> *contentsArray;
@end
@@ -0,0 +1,183 @@
//
// PTTableContentViewController.m
// PTDatabaseReader
//
// Created by Peng Tao on 15/11/23.
// Copyright © 2015年 Peng Tao. All rights reserved.
//
#import "FLEXTableContentViewController.h"
#import "FLEXMultiColumnTableView.h"
#import "FLEXWebViewController.h"
@interface FLEXTableContentViewController ()<FLEXMultiColumnTableViewDataSource, FLEXMultiColumnTableViewDelegate>
@property (nonatomic) FLEXMultiColumnTableView *multiColumnView;
@end
@implementation FLEXTableContentViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.edgesForExtendedLayout = UIRectEdgeNone;
[self.view addSubview:self.multiColumnView];
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[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
{
return self.columnsArray.count;
}
- (NSInteger)numberOfRowsInTableView:(FLEXMultiColumnTableView *)tableView
{
return self.contentsArray.count;
}
- (NSString *)columnNameInColumn:(NSInteger)column
{
return self.columnsArray[column];
}
- (NSString *)rowNameInRow:(NSInteger)row
{
return [NSString stringWithFormat:@"%ld",(long)row];
}
- (NSString *)contentAtColumn:(NSInteger)column row:(NSInteger)row
{
if (self.contentsArray.count > row) {
NSDictionary<NSString *, id> *dic = self.contentsArray[row];
if (self.contentsArray.count > column) {
return [NSString stringWithFormat:@"%@",[dic objectForKey:self.columnsArray[column]]];
}
}
return @"";
}
- (NSArray *)contentAtRow:(NSInteger)row
{
NSMutableArray *result = [NSMutableArray array];
if (self.contentsArray.count > row) {
NSDictionary<NSString *, id> *dic = self.contentsArray[row];
for (int i = 0; i < self.columnsArray.count; i ++) {
[result addObject:dic[self.columnsArray[i]]];
}
return result;
}
return nil;
}
- (CGFloat)multiColumnTableView:(FLEXMultiColumnTableView *)tableView
heightForContentCellInRow:(NSInteger)row
{
return 40;
}
- (CGFloat)multiColumnTableView:(FLEXMultiColumnTableView *)tableView
widthForContentCellInColumn:(NSInteger)column
{
return 120;
}
- (CGFloat)heightForTopHeaderInTableView:(FLEXMultiColumnTableView *)tableView
{
return 40;
}
- (CGFloat)widthForLeftHeaderInTableView:(FLEXMultiColumnTableView *)tableView
{
NSString *str = [NSString stringWithFormat:@"%lu",(unsigned long)self.contentsArray.count];
NSDictionary<NSString *, id> *attrs = @{@"NSFontAttributeName":[UIFont systemFontOfSize:17.0]};
CGSize size = [str boundingRectWithSize:CGSizeMake(CGFLOAT_MAX, 14)
options:NSStringDrawingUsesLineFragmentOrigin
attributes:attrs context:nil].size;
return size.width + 20;
}
#pragma mark -
#pragma mark MultiColumnTableView Delegate
- (void)multiColumnTableView:(FLEXMultiColumnTableView *)tableView didTapLabelWithText:(NSString *)text
{
FLEXWebViewController * detailViewController = [[FLEXWebViewController alloc] initWithText:text];
[self.navigationController pushViewController:detailViewController animated:YES];
}
- (void)multiColumnTableView:(FLEXMultiColumnTableView *)tableView didTapHeaderWithText:(NSString *)text sortType:(FLEXTableColumnHeaderSortType)sortType
{
NSArray<NSDictionary<NSString *, id> *> *sortContentData = [self.contentsArray sortedArrayUsingComparator:^NSComparisonResult(NSDictionary<NSString *, id> * obj1, NSDictionary<NSString *, id> * obj2) {
if ([obj1 objectForKey:text] == [NSNull null]) {
return NSOrderedAscending;
}
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 *contentReverseEnumerator = sortContentData.reverseObjectEnumerator;
sortContentData = [NSArray arrayWithArray:contentReverseEnumerator.allObjects];
}
self.contentsArray = sortContentData;
[self.multiColumnView reloadData];
}
#pragma mark -
#pragma mark About Transition
- (void)willTransitionToTraitCollection:(UITraitCollection *)newCollection
withTransitionCoordinator:(id <UIViewControllerTransitionCoordinator>)coordinator
{
[super willTransitionToTraitCollection:newCollection
withTransitionCoordinator:coordinator];
[coordinator animateAlongsideTransition:^(id <UIViewControllerTransitionCoordinatorContext> context) {
if (newCollection.verticalSizeClass == UIUserInterfaceSizeClassCompact) {
self->_multiColumnView.frame = CGRectMake(0, 32, self.view.frame.size.width, self.view.frame.size.height - 32);
}
else {
self->_multiColumnView.frame = CGRectMake(0, 64, self.view.frame.size.width, self.view.frame.size.height - 64);
}
[self.view setNeedsLayout];
} completion:nil];
}
@end
@@ -0,0 +1,17 @@
//
// FLEXTableLeftCell.h
// FLEX
//
// Created by Peng Tao on 15/11/24.
// Copyright © 2015年 f. All rights reserved.
//
#import <UIKit/UIKit.h>
@interface FLEXTableLeftCell : UITableViewCell
@property (nonatomic) UILabel *titlelabel;
+ (instancetype)cellWithTableView:(UITableView *)tableView;
@end
@@ -0,0 +1,35 @@
//
// FLEXTableLeftCell.m
// FLEX
//
// Created by Peng Tao on 15/11/24.
// Copyright © 2015年 f. All rights reserved.
//
#import "FLEXTableLeftCell.h"
@implementation FLEXTableLeftCell
+ (instancetype)cellWithTableView:(UITableView *)tableView
{
static NSString *identifier = @"FLEXTableLeftCell";
FLEXTableLeftCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
if (!cell) {
cell = [[FLEXTableLeftCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
UILabel *textLabel = [[UILabel alloc] initWithFrame:CGRectZero];
textLabel.textAlignment = NSTextAlignmentCenter;
textLabel.font = [UIFont systemFontOfSize:13.0];
textLabel.backgroundColor = UIColor.clearColor;
[cell.contentView addSubview:textLabel];
cell.titlelabel = textLabel;
}
return cell;
}
- (void)layoutSubviews
{
[super layoutSubviews];
self.titlelabel.frame = self.contentView.frame;
}
@end
@@ -0,0 +1,16 @@
//
// PTTableListViewController.h
// PTDatabaseReader
//
// Created by Peng Tao on 15/11/23.
// Copyright © 2015年 Peng Tao. All rights reserved.
//
#import "FLEXTableViewController.h"
@interface FLEXTableListViewController : FLEXTableViewController
+ (BOOL)supportsExtension:(NSString *)extension;
- (instancetype)initWithPath:(NSString *)path;
@end
@@ -0,0 +1,163 @@
//
// PTTableListViewController.m
// PTDatabaseReader
//
// Created by Peng Tao on 15/11/23.
// Copyright © 2015年 Peng Tao. All rights reserved.
//
#import "FLEXTableListViewController.h"
#import "FLEXDatabaseManager.h"
#import "FLEXSQLiteDatabaseManager.h"
#import "FLEXRealmDatabaseManager.h"
#import "FLEXTableContentViewController.h"
@interface FLEXTableListViewController ()
{
id<FLEXDatabaseManager> _dbm;
NSString *_databasePath;
}
@property (nonatomic) NSArray<NSString *> *tables;
@property (nonatomic) NSArray<NSString *> *filteredTables;
+ (NSArray<NSString *> *)supportedSQLiteExtensions;
+ (NSArray<NSString *> *)supportedRealmExtensions;
@end
@implementation FLEXTableListViewController
- (instancetype)initWithPath:(NSString *)path
{
self = [super initWithStyle:UITableViewStyleGrouped];
if (self) {
_databasePath = [path copy];
_dbm = [self databaseManagerForFileAtPath:_databasePath];
[_dbm open];
[self getAllTables];
}
return self;
}
- (id<FLEXDatabaseManager>)databaseManagerForFileAtPath:(NSString *)path
{
NSString *pathExtension = path.pathExtension.lowercaseString;
NSArray<NSString *> *sqliteExtensions = [FLEXTableListViewController supportedSQLiteExtensions];
if ([sqliteExtensions indexOfObject:pathExtension] != NSNotFound) {
return [[FLEXSQLiteDatabaseManager alloc] initWithPath:path];
}
NSArray<NSString *> *realmExtensions = [FLEXTableListViewController supportedRealmExtensions];
if (realmExtensions != nil && [realmExtensions indexOfObject:pathExtension] != NSNotFound) {
return [[FLEXRealmDatabaseManager alloc] initWithPath:path];
}
return nil;
}
- (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;
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.showsSearchBar = YES;
}
#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.filteredTables[indexPath.row];
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
FLEXTableContentViewController *contentViewController = [FLEXTableContentViewController new];
contentViewController.contentsArray = [_dbm queryAllDataWithTableName:self.filteredTables[indexPath.row]];
contentViewController.columnsArray = [_dbm queryAllColumnsWithTableName:self.filteredTables[indexPath.row]];
contentViewController.title = self.filteredTables[indexPath.row];
[self.navigationController pushViewController:contentViewController animated:YES];
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
return [NSString stringWithFormat:@"Tables (%lu)", (unsigned long)self.filteredTables.count];
}
#pragma mark - FLEXTableListViewController
+ (BOOL)supportsExtension:(NSString *)extension
{
extension = extension.lowercaseString;
NSArray<NSString *> *sqliteExtensions = [FLEXTableListViewController supportedSQLiteExtensions];
if (sqliteExtensions.count > 0 && [sqliteExtensions indexOfObject:extension] != NSNotFound) {
return YES;
}
NSArray<NSString *> *realmExtensions = [FLEXTableListViewController supportedRealmExtensions];
if (realmExtensions.count > 0 && [realmExtensions indexOfObject:extension] != NSNotFound) {
return YES;
}
return NO;
}
+ (NSArray<NSString *> *)supportedSQLiteExtensions
{
return @[@"db", @"sqlite", @"sqlite3"];
}
+ (NSArray<NSString *> *)supportedRealmExtensions
{
if (NSClassFromString(@"RLMRealm") == nil) {
return nil;
}
return @[@"realm"];
}
@end
@@ -0,0 +1,21 @@
FMDB
Copyright (c) 2008-2014 Flying Meat Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
@@ -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 "FLEXGlobalsTableViewController.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXObjectExplorerViewController.h"
#import "FLEXRuntimeUtility.h"
#import "FLEXUtility.h"
@interface FLEXGlobalsTableViewController (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 ^(FLEXGlobalsTableViewController *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 FLEXGlobalsTableViewController (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
@@ -6,10 +6,11 @@
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "FLEXTableViewController.h"
#import "FLEXGlobalsEntry.h"
@interface FLEXClassesTableViewController : UITableViewController
@interface FLEXClassesTableViewController : FLEXTableViewController <FLEXGlobalsEntry>
@property (nonatomic, copy) NSString *binaryImageName;
+ (instancetype)binaryImageName:(NSString *)binaryImageName;
@end
@@ -12,48 +12,66 @@
#import "FLEXUtility.h"
#import <objc/runtime.h>
@interface FLEXClassesTableViewController () <UISearchBarDelegate>
@interface FLEXClassesTableViewController ()
@property (nonatomic, strong) NSArray *classNames;
@property (nonatomic, strong) NSArray *filteredClassNames;
@property (nonatomic, strong) UISearchBar *searchBar;
@property (nonatomic, copy) NSArray<NSString *> *classNames;
@property (nonatomic, copy) NSArray<NSString *> *filteredClassNames;
@property (nonatomic, copy) NSString *binaryImageName;
@end
@implementation FLEXClassesTableViewController
#pragma mark - Initialization
+ (instancetype)binaryImageName:(NSString *)binaryImageName
{
return [[self alloc] initWithBinaryImageName:binaryImageName];
}
- (id)initWithBinaryImageName:(NSString *)binaryImageName
{
NSParameterAssert(binaryImageName);
self = [super init];
if (self) {
self.binaryImageName = binaryImageName;
[self loadClassNames];
}
return self;
}
#pragma mark - Internal
- (void)viewDidLoad
{
[super viewDidLoad];
self.searchBar = [[UISearchBar alloc] init];
self.searchBar.placeholder = [FLEXUtility searchBarPlaceholderText];
self.searchBar.delegate = self;
[self.searchBar sizeToFit];
self.tableView.tableHeaderView = self.searchBar;
self.showsSearchBar = YES;
[self updateTitle];
}
- (void)setBinaryImageName:(NSString *)binaryImageName
- (void)updateTitle
{
if (![_binaryImageName isEqual:binaryImageName]) {
_binaryImageName = binaryImageName;
[self loadClassNames];
[self updateTitle];
}
NSString *shortImageName = self.binaryImageName.lastPathComponent;
self.title = [NSString stringWithFormat:@"%@ Classes (%lu)",
shortImageName, (unsigned long)self.filteredClassNames.count
];
}
- (void)setClassNames:(NSArray *)classNames
- (void)setClassNames:(NSArray<NSString *> *)classNames
{
_classNames = classNames;
self.filteredClassNames = classNames;
_classNames = self.filteredClassNames = classNames.copy;
}
- (void)loadClassNames
{
unsigned int classNamesCount = 0;
const char **classNames = objc_copyClassNamesForImage([self.binaryImageName UTF8String], &classNamesCount);
const char **classNames = objc_copyClassNamesForImage(self.binaryImageName.UTF8String, &classNamesCount);
if (classNames) {
NSMutableArray *classNameStrings = [NSMutableArray array];
NSMutableArray<NSString *> *classNameStrings = [NSMutableArray array];
for (unsigned int i = 0; i < classNamesCount; i++) {
const char *className = classNames[i];
NSString *classNameString = [NSString stringWithUTF8String:className];
@@ -66,20 +84,25 @@
}
}
- (void)updateTitle
{
NSString *shortImageName = [[self.binaryImageName componentsSeparatedByString:@"/"] lastObject];
self.title = [NSString stringWithFormat:@"%@ Classes (%lu)", shortImageName, (unsigned long)[self.filteredClassNames count]];
#pragma mark - FLEXGlobalsEntry
+ (NSString *)globalsEntryTitle:(FLEXGlobalsRow)row {
return [NSString stringWithFormat:@"📕 %@ Classes", [FLEXUtility applicationName]];
}
+ (UIViewController *)globalsEntryViewController:(FLEXGlobalsRow)row {
return [self binaryImageName:[FLEXUtility applicationImageName]];
}
#pragma mark - Search
#pragma mark - Search bar
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
- (void)updateSearchResults:(NSString *)searchText
{
if ([searchText length] > 0) {
NSPredicate *searchPreidcate = [NSPredicate predicateWithFormat:@"SELF CONTAINS[cd] %@", searchText];
self.filteredClassNames = [self.classNames filteredArrayUsingPredicate:searchPreidcate];
if (searchText.length > 0) {
NSPredicate *searchPredicate = [NSPredicate predicateWithFormat:@"SELF CONTAINS[cd] %@", searchText];
self.filteredClassNames = [self.classNames filteredArrayUsingPredicate:searchPredicate].reverseObjectEnumerator.allObjects;
} else {
self.filteredClassNames = self.classNames;
}
@@ -87,17 +110,6 @@
[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
@@ -108,7 +120,7 @@
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [self.filteredClassNames count];
return self.filteredClassNames.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
@@ -131,8 +143,8 @@
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *className = [self.filteredClassNames objectAtIndex:indexPath.row];
Class selectedClass = objc_getClass([className UTF8String]);
NSString *className = self.filteredClassNames[indexPath.row];
Class selectedClass = objc_getClass(className.UTF8String);
FLEXObjectExplorerViewController *objectExplorer = [FLEXObjectExplorerFactory explorerViewControllerForObject:selectedClass];
[self.navigationController pushViewController:objectExplorer animated:YES];
}
@@ -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;
@@ -50,7 +54,7 @@
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleSubtitle reuseIdentifier:CellIdentifier];
cell.textLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
cell.detailTextLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
cell.detailTextLabel.textColor = [UIColor grayColor];
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,391 +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"
@interface FLEXFileBrowserTableViewCell : UITableViewCell
@end
@interface FLEXFileBrowserTableViewController () <FLEXFileBrowserFileOperationControllerDelegate>
@property (nonatomic, copy) NSString *path;
@property (nonatomic, copy) NSArray *childPaths;
@property (nonatomic, copy) NSString *searchString;
@property (nonatomic, strong) NSArray *searchPaths;
@property (nonatomic, strong) NSNumber *recursiveSize;
@property (nonatomic, strong) NSNumber *searchPathsSize;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
@property (nonatomic, strong) UISearchDisplayController *searchController;
#pragma clang diagnostic pop
@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];
//add search controller
UISearchBar *searchBar = [UISearchBar new];
[searchBar sizeToFit];
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
self.searchController = [[UISearchDisplayController alloc] initWithSearchBar:searchBar contentsController:self];
#pragma clang diagnostic pop
self.searchController.delegate = self;
self.searchController.searchResultsDataSource = self;
self.searchController.searchResultsDelegate = self;
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.searchController.searchResultsTableView reloadData];
}
#pragma mark - UISearchDisplayDelegate
- (BOOL)searchDisplayController:(UISearchDisplayController *)controller shouldReloadTableForSearchString:(NSString *)searchString
{
self.searchString = searchString;
[self reloadSearchPaths];
return YES;
}
- (void)searchDisplayController:(UISearchDisplayController *)controller willHideSearchResultsTableView:(UITableView *)tableView
{
//confirm to clear all operations
[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
{
if (tableView == self.tableView) {
return [self.childPaths count];
} else {
return [self.searchPaths count];
}
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
NSNumber *currentSize = nil;
NSArray *currentPaths = nil;
if (tableView == self.tableView) {
currentSize = self.recursiveSize;
currentPaths = self.childPaths;
} else {
currentSize = self.searchPathsSize;
currentPaths = self.searchPaths;
}
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 = nil;
if (tableView == self.tableView) {
NSString *subpath = [self.childPaths objectAtIndex:indexPath.row];
fullPath = [self.path stringByAppendingPathComponent:subpath];
} else {
fullPath = [self.searchPaths objectAtIndex:indexPath.row];
}
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 *subpath = nil;
NSString *fullPath = nil;
if (tableView == self.tableView) {
subpath = [self.childPaths objectAtIndex:indexPath.row];
fullPath = [self.path stringByAppendingPathComponent:subpath];
} else {
fullPath = [self.searchPaths objectAtIndex:indexPath.row];
subpath = [fullPath lastPathComponent];
}
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:[fullPath pathExtension]]) {
UIImage *image = [UIImage imageWithContentsOfFile:fullPath];
drillInViewController = [[FLEXImagePreviewViewController alloc] initWithImage:image];
} else {
// Special case keyed archives, json, and plists to get more readable data.
NSString *prettyString = nil;
if ([[subpath pathExtension] isEqual:@"archive"]) {
prettyString = [[NSKeyedUnarchiver unarchiveObjectWithFile:fullPath] description];
} else if ([[subpath pathExtension] isEqualToString:@"json"]) {
prettyString = [FLEXUtility prettyJSONStringFromData:[NSData dataWithContentsOfFile:fullPath]];
} else if ([[subpath pathExtension] isEqualToString:@"plist"]) {
NSData *fileData = [NSData dataWithContentsOfFile:fullPath];
prettyString = [[NSPropertyListSerialization propertyListWithData:fileData options:0 format:NULL error:NULL] description];
}
if ([prettyString length] > 0) {
drillInViewController = [[FLEXWebViewController alloc] initWithText:prettyString];
} else if ([FLEXWebViewController supportsPathExtension:[subpath pathExtension]]) {
drillInViewController = [[FLEXWebViewController alloc] initWithURL:[NSURL fileURLWithPath: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
{
NSString *fullPath = nil;
NSIndexPath *indexPath = [self.tableView indexPathForCell:sender];
if (indexPath) {
NSString *subpath = [self.childPaths objectAtIndex:indexPath.row];
fullPath = [self.path stringByAppendingPathComponent:subpath];
} else {
indexPath = [self.searchController.searchResultsTableView indexPathForCell:sender];
fullPath = [self.searchPaths objectAtIndex:indexPath.row];
}
self.fileOperationController = [[FLEXFileBrowserFileRenameOperationController alloc] initWithPath:fullPath];
self.fileOperationController.delegate = self;
[self.fileOperationController show];
}
- (void)fileBrowserDelete:(UITableViewCell *)sender
{
NSString *fullPath = nil;
NSIndexPath *indexPath = [self.tableView indexPathForCell:sender];
if (indexPath) {
NSString *subpath = [self.childPaths objectAtIndex:indexPath.row];
fullPath = [self.path stringByAppendingPathComponent:subpath];
} else {
indexPath = [self.searchController.searchResultsTableView indexPathForCell:sender];
fullPath = [self.searchPaths objectAtIndex:indexPath.row];
}
self.fileOperationController = [[FLEXFileBrowserFileDeleteOperationController alloc] initWithPath:fullPath];
self.fileOperationController.delegate = self;
[self.fileOperationController show];
}
- (void)reloadDisplayedPaths
{
if (self.searchController.isActive) {
[self reloadSearchPaths];
[self.searchController.searchResultsTableView reloadData];
} else {
[self reloadChildPaths];
[self.tableView reloadData];
}
}
- (void)reloadChildPaths
{
self.childPaths = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:self.path error:NULL];
}
- (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.searchString];
newOperation.delegate = self;
[self.operationQueue addOperation:newOperation];
}
@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,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
@@ -12,39 +12,79 @@
#import "FLEXRuntimeUtility.h"
#import "FLEXUtility.h"
#import "FLEXHeapEnumerator.h"
#import "FLEXObjectRef.h"
#import <malloc/malloc.h>
@interface FLEXInstancesTableViewController ()
@property (nonatomic, strong) NSArray *instances;
@property (nonatomic, strong) NSArray *fieldNames;
/// Array of [[section], [section], ...]
/// where [section] is [["row title", instance], ["row title", instance], ...]
@property (nonatomic) NSArray<FLEXObjectRef *> *instances;
@property (nonatomic) NSArray<NSArray<FLEXObjectRef*>*> *sections;
@property (nonatomic) NSArray<NSString *> *sectionTitles;
@property (nonatomic) NSArray<NSPredicate *> *predicates;
@property (nonatomic, readonly) NSInteger maxSections;
@end
@implementation FLEXInstancesTableViewController
- (id)initWithReferences:(NSArray<FLEXObjectRef *> *)references {
return [self initWithReferences:references predicates:nil sectionTitles:nil];
}
- (id)initWithReferences:(NSArray<FLEXObjectRef *> *)references
predicates:(NSArray<NSPredicate *> *)predicates
sectionTitles:(NSArray<NSString *> *)sectionTitles {
NSParameterAssert(predicates.count == sectionTitles.count);
self = [super init];
if (self) {
self.instances = references;
self.predicates = predicates;
self.sectionTitles = sectionTitles;
if (predicates.count) {
[self buildSections];
} else {
self.sections = @[references];
}
}
return self;
}
+ (instancetype)instancesTableViewControllerForClassName:(NSString *)className
{
const char *classNameCString = [className UTF8String];
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.
// 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.
[instances addObject:object];
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;
NSArray<FLEXObjectRef *> *references = [FLEXObjectRef referencingAll:instances];
FLEXInstancesTableViewController *viewController = [[self alloc] initWithReferences:references];
viewController.title = [NSString stringWithFormat:@"%@ (%lu)", className, (unsigned long)instances.count];
return viewController;
}
+ (instancetype)instancesTableViewControllerForInstancesReferencingObject:(id)object
{
NSMutableArray *instances = [NSMutableArray array];
NSMutableArray *fieldNames = [NSMutableArray array];
NSMutableArray<FLEXObjectRef *> *instances = [NSMutableArray array];
[FLEXHeapEnumerator enumerateLiveObjectsUsingBlock:^(__unsafe_unretained id tryObject, __unsafe_unretained Class actualClass) {
// Skip Swift objects
if ([actualClass isKindOfClass:NSClassFromString(@"SwiftObject")]) {
return;
}
// 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;
@@ -54,12 +94,11 @@
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]) {
if (typeEncoding[0] == FLEXTypeEncodingObjcObject || typeEncoding[0] == FLEXTypeEncodingObjcClass) {
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))];
[instances addObject:[FLEXObjectRef referencing:tryObject ivar:@(ivar_getName(ivar))]];
return;
}
}
@@ -67,11 +106,85 @@
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;
NSArray<NSPredicate *> *predicates = [self defaultPredicates];
NSArray<NSString *> *sectionTitles = [self defaultSectionTitles];
FLEXInstancesTableViewController *viewController = [[self alloc] initWithReferences:instances
predicates:predicates
sectionTitles:sectionTitles];
viewController.title = [NSString stringWithFormat:@"Referencing %@ %p", NSStringFromClass(object_getClass(object)), object];
return viewController;
}
+ (NSPredicate *)defaultPredicateForSection:(NSInteger)section
{
// These are the types of references that we typically don't care about.
// We want this list of "object-ivar pairs" split into two sections.
BOOL(^isObserver)(FLEXObjectRef *, NSDictionary *) = ^BOOL(FLEXObjectRef *ref, NSDictionary *bindings) {
NSString *row = ref.reference;
return [row isEqualToString:@"__NSObserver object"] ||
[row isEqualToString:@"_CFXNotificationObjcObserverRegistration _object"];
};
/// These are common AutoLayout related references we also rarely care about.
BOOL(^isConstraintRelated)(FLEXObjectRef *, NSDictionary *) = ^BOOL(FLEXObjectRef *ref, NSDictionary *bindings) {
static NSSet *ignored = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
ignored = [NSSet setWithArray:@[
@"NSLayoutConstraint _container",
@"NSContentSizeLayoutConstraint _container",
@"NSAutoresizingMaskLayoutConstraint _container",
@"MASViewConstraint _installedView",
@"MASLayoutConstraint _container",
@"MASViewAttribute _view"
]];
});
NSString *row = ref.reference;
return ([row hasPrefix:@"NSLayout"] && [row hasSuffix:@" _referenceItem"]) ||
([row hasPrefix:@"NSIS"] && [row hasSuffix:@" _delegate"]) ||
([row hasPrefix:@"_NSAutoresizingMask"] && [row hasSuffix:@" _referenceItem"]) ||
[ignored containsObject:row];
};
BOOL(^isEssential)(FLEXObjectRef *, NSDictionary *) = ^BOOL(FLEXObjectRef *ref, NSDictionary *bindings) {
return !(isObserver(ref, bindings) || isConstraintRelated(ref, bindings));
};
switch (section) {
case 0: return [NSPredicate predicateWithBlock:isEssential];
case 1: return [NSPredicate predicateWithBlock:isConstraintRelated];
case 2: return [NSPredicate predicateWithBlock:isObserver];
default: return nil;
}
}
+ (NSArray<NSPredicate *> *)defaultPredicates {
return @[[self defaultPredicateForSection:0],
[self defaultPredicateForSection:1],
[self defaultPredicateForSection:2]];
}
+ (NSArray<NSString *> *)defaultSectionTitles {
return @[@"", @"AutoLayout", @"Trivial"];
}
- (void)buildSections
{
NSInteger maxSections = self.maxSections;
NSMutableArray *sections = [NSMutableArray array];
for (NSInteger i = 0; i < maxSections; i++) {
NSPredicate *predicate = self.predicates[i];
[sections addObject:[self.instances filteredArrayUsingPredicate:predicate]];
}
self.sections = sections;
}
- (NSInteger)maxSections {
return self.predicates.count ?: 1;
}
@@ -79,12 +192,12 @@
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
return self.maxSections;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [self.instances count];
return self.sections[section].count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
@@ -97,28 +210,35 @@
UIFont *cellFont = [FLEXUtility defaultTableViewCellLabelFont];
cell.textLabel.font = cellFont;
cell.detailTextLabel.font = cellFont;
cell.detailTextLabel.textColor = [UIColor grayColor];
cell.detailTextLabel.textColor = UIColor.grayColor;
}
id instance = [self.instances objectAtIndex:indexPath.row];
NSString *title = nil;
if ((NSInteger)[self.fieldNames count] > indexPath.row) {
title = [NSString stringWithFormat:@"%@ %@", NSStringFromClass(object_getClass(instance)), [self.fieldNames objectAtIndex:indexPath.row]];
} else {
title = [NSString stringWithFormat:@"%@ %p", NSStringFromClass(object_getClass(instance)), instance];
}
cell.textLabel.text = title;
cell.detailTextLabel.text = [FLEXRuntimeUtility descriptionForIvarOrPropertyValue:instance];
FLEXObjectRef *row = self.sections[indexPath.section][indexPath.row];
cell.textLabel.text = row.reference;
cell.detailTextLabel.text = [FLEXRuntimeUtility descriptionForIvarOrPropertyValue:row.object];
return cell;
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
if (self.sectionTitles.count) {
// Return nil instead of empty strings
NSString *title = self.sectionTitles[section];
if (title.length) {
return title;
}
}
return nil;
}
#pragma mark - Table View Delegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
id instance = [self.instances objectAtIndex:indexPath.row];
id instance = self.instances[indexPath.row].object;
FLEXObjectExplorerViewController *drillInViewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:instance];
[self.navigationController pushViewController:drillInViewController animated:YES];
}
@@ -6,8 +6,9 @@
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "FLEXTableViewController.h"
#import "FLEXGlobalsEntry.h"
@interface FLEXLibrariesTableViewController : UITableViewController
@interface FLEXLibrariesTableViewController : FLEXTableViewController <FLEXGlobalsEntry>
@end
@@ -9,14 +9,16 @@
#import "FLEXLibrariesTableViewController.h"
#import "FLEXUtility.h"
#import "FLEXClassesTableViewController.h"
#import "FLEXObjectExplorerFactory.h"
#import <objc/runtime.h>
@interface FLEXLibrariesTableViewController () <UISearchBarDelegate>
@interface FLEXLibrariesTableViewController ()
@property (nonatomic, strong) NSArray *imageNames;
@property (nonatomic, strong) NSArray *filteredImageNames;
@property (nonatomic) NSArray<NSString *> *imageNames;
@property (nonatomic) NSArray<NSString *> *filteredImageNames;
@property (nonatomic) NSString *headerTitle;
@property (nonatomic, strong) UISearchBar *searchBar;
@property (nonatomic) Class foundClass;
@end
@@ -35,11 +37,37 @@
{
[super viewDidLoad];
self.searchBar = [[UISearchBar alloc] init];
self.searchBar.delegate = self;
self.searchBar.placeholder = [FLEXUtility searchBarPlaceholderText];
[self.searchBar sizeToFit];
self.tableView.tableHeaderView = self.searchBar;
self.showsSearchBar = YES;
[self updateHeaderTitle];
}
- (void)updateHeaderTitle
{
if (self.foundClass) {
self.headerTitle = @"Looking for this?";
} else if (self.imageNames.count == self.filteredImageNames.count) {
// Unfiltered
self.headerTitle = [NSString stringWithFormat:@"%@ libraries", @(self.imageNames.count)];
} else {
self.headerTitle = [NSString
stringWithFormat:@"%@ of %@ libraries",
@(self.filteredImageNames.count), @(self.imageNames.count)
];
}
}
#pragma mark - FLEXGlobalsEntry
+ (NSString *)globalsEntryTitle:(FLEXGlobalsRow)row {
return @"📚 System Libraries";
}
+ (UIViewController *)globalsEntryViewController:(FLEXGlobalsRow)row {
FLEXLibrariesTableViewController *librariesViewController = [self new];
librariesViewController.title = [self globalsEntryTitle:row];
return librariesViewController;
}
@@ -50,7 +78,7 @@
unsigned int imageNamesCount = 0;
const char **imageNames = objc_copyImageNames(&imageNamesCount);
if (imageNames) {
NSMutableArray *imageNameStrings = [NSMutableArray array];
NSMutableArray<NSString *> *imageNameStrings = [NSMutableArray array];
NSString *appImageName = [FLEXUtility applicationImageName];
for (unsigned int i = 0; i < imageNamesCount; i++) {
const char *imageName = imageNames[i];
@@ -63,9 +91,9 @@
// Sort alphabetically
self.imageNames = [imageNameStrings sortedArrayWithOptions:0 usingComparator:^NSComparisonResult(NSString *name1, NSString *name2) {
NSString *shortName1 = [self shortNameForImageName:name1];
NSString *shortName2 = [self shortNameForImageName:name2];
return [shortName1 caseInsensitiveCompare:shortName2];
NSString *shortName1 = [self shortNameForImageName:name1];
NSString *shortName2 = [self shortNameForImageName:name2];
return [shortName1 caseInsensitiveCompare:shortName2];
}];
free(imageNames);
@@ -74,16 +102,14 @@
- (NSString *)shortNameForImageName:(NSString *)imageName
{
NSString *shortName = nil;
NSArray *components = [imageName componentsSeparatedByString:@"/"];
NSUInteger componentsCount = [components count];
if (componentsCount >= 2) {
shortName = [NSString stringWithFormat:@"%@/%@", components[componentsCount - 2], components[componentsCount - 1]];
NSArray<NSString *> *components = [imageName componentsSeparatedByString:@"/"];
if (components.count >= 2) {
return [NSString stringWithFormat:@"%@/%@", components[components.count - 2], components[components.count - 1]];
}
return shortName;
return imageName.lastPathComponent;
}
- (void)setImageNames:(NSArray *)imageNames
- (void)setImageNames:(NSArray<NSString *> *)imageNames
{
if (![_imageNames isEqual:imageNames]) {
_imageNames = imageNames;
@@ -94,10 +120,10 @@
#pragma mark - Filtering
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
- (void)updateSearchResults:(NSString *)searchText
{
if ([searchText length] > 0) {
NSPredicate *searchPreidcate = [NSPredicate predicateWithBlock:^BOOL(id evaluatedObject, NSDictionary *bindings) {
if (searchText.length) {
NSPredicate *searchPredicate = [NSPredicate predicateWithBlock:^BOOL(NSString *evaluatedObject, NSDictionary<NSString *, id> *bindings) {
BOOL matches = NO;
NSString *shortName = [self shortNameForImageName:evaluatedObject];
if ([shortName rangeOfString:searchText options:NSCaseInsensitiveSearch].length > 0) {
@@ -105,55 +131,69 @@
}
return matches;
}];
self.filteredImageNames = [self.imageNames filteredArrayUsingPredicate:searchPreidcate];
self.filteredImageNames = [self.imageNames filteredArrayUsingPredicate:searchPredicate];
} else {
self.filteredImageNames = self.imageNames;
}
self.foundClass = NSClassFromString(searchText);
[self updateHeaderTitle];
[self.tableView reloadData];
}
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar
{
[searchBar resignFirstResponder];
}
#pragma mark - Table View Data Source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return 1;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [self.filteredImageNames count];
return self.filteredImageNames.count + (self.foundClass ? 1 : 0);
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
static NSString *CellIdentifier = @"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier];
static NSString *cellIdentifier = @"Cell";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellIdentifier];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:CellIdentifier];
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellIdentifier];
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
cell.textLabel.font = [FLEXUtility defaultTableViewCellLabelFont];
}
NSString *fullImageName = self.filteredImageNames[indexPath.row];
cell.textLabel.text = [self shortNameForImageName:fullImageName];
NSString *executablePath;
if (self.foundClass) {
if (indexPath.row == 0) {
cell.textLabel.text = [NSString stringWithFormat:@"Class \"%@\"", self.searchText];
return cell;
} else {
executablePath = self.filteredImageNames[indexPath.row-1];
}
} else {
executablePath = self.filteredImageNames[indexPath.row];
}
cell.textLabel.text = [self shortNameForImageName:executablePath];
return cell;
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
return self.headerTitle;
}
#pragma mark - Table View Delegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
FLEXClassesTableViewController *classesViewController = [[FLEXClassesTableViewController alloc] init];
classesViewController.binaryImageName = self.filteredImageNames[indexPath.row];
[self.navigationController pushViewController:classesViewController animated:YES];
if (indexPath.row == 0 && self.foundClass) {
[self.navigationController pushViewController:[FLEXObjectExplorerFactory
explorerViewControllerForObject:self.foundClass
] animated:YES];
} else {
[self.navigationController pushViewController:[FLEXClassesTableViewController
binaryImageName:self.filteredImageNames[self.foundClass ? 0 : indexPath.row]
] animated:YES];
}
}
@end
@@ -6,8 +6,9 @@
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "FLEXTableViewController.h"
#import "FLEXGlobalsEntry.h"
@interface FLEXLiveObjectsTableViewController : UITableViewController
@interface FLEXLiveObjectsTableViewController : FLEXTableViewController <FLEXGlobalsEntry>
@end
@@ -10,43 +10,50 @@
#import "FLEXHeapEnumerator.h"
#import "FLEXInstancesTableViewController.h"
#import "FLEXUtility.h"
#import "FLEXScopeCarousel.h"
#import "FLEXTableView.h"
#import <objc/runtime.h>
static const NSInteger kFLEXLiveObjectsSortAlphabeticallyIndex = 0;
static const NSInteger kFLEXLiveObjectsSortByCountIndex = 1;
static const NSInteger kFLEXLiveObjectsSortBySizeIndex = 2;
@interface FLEXLiveObjectsTableViewController () <UISearchBarDelegate>
@interface FLEXLiveObjectsTableViewController ()
@property (nonatomic, strong) NSDictionary *instanceCountsForClassNames;
@property (nonatomic, readonly) NSArray *allClassNames;
@property (nonatomic, strong) NSArray *filteredClassNames;
@property (nonatomic, strong) UISearchBar *searchBar;
@property (nonatomic) NSDictionary<NSString *, NSNumber *> *instanceCountsForClassNames;
@property (nonatomic) NSDictionary<NSString *, NSNumber *> *instanceSizesForClassNames;
@property (nonatomic, readonly) NSArray<NSString *> *allClassNames;
@property (nonatomic) NSArray<NSString *> *filteredClassNames;
@property (nonatomic) NSString *headerTitle;
@end
@implementation FLEXLiveObjectsTableViewController
- (void)loadView
{
self.tableView = [FLEXTableView flexDefaultTableView];
}
- (void)viewDidLoad
{
[super viewDidLoad];
// self.title = @"Live Objects";
self.showsSearchBar = YES;
self.searchBarDebounceInterval = kFLEXDebounceInstant;
self.showsCarousel = YES;
self.carousel.items = @[@"A→Z", @"Count", @"Size"];
self.searchBar = [[UISearchBar alloc] init];
self.searchBar.placeholder = [FLEXUtility searchBarPlaceholderText];
self.searchBar.delegate = self;
self.searchBar.showsScopeBar = YES;
self.searchBar.scopeButtonTitles = @[@"Sort Alphabetically", @"Sort by Count"];
[self.searchBar sizeToFit];
self.tableView.tableHeaderView = self.searchBar;
self.refreshControl = [[UIRefreshControl alloc] init];
self.refreshControl = [UIRefreshControl new];
[self.refreshControl addTarget:self action:@selector(refreshControlDidRefresh:) forControlEvents:UIControlEventValueChanged];
[self reloadTableData];
}
- (NSArray *)allClassNames
- (NSArray<NSString *> *)allClassNames
{
return [self.instanceCountsForClassNames allKeys];
return self.instanceCountsForClassNames.allKeys;
}
- (void)reloadTableData
@@ -72,20 +79,23 @@ static const NSInteger kFLEXLiveObjectsSortByCountIndex = 1;
}];
// Convert our CF primitive dictionary into a nicer mapping of class name strings to counts that we will use as the table's model.
NSMutableDictionary *mutableCountsForClassNames = [NSMutableDictionary dictionary];
NSMutableDictionary<NSString *, NSNumber *> *mutableCountsForClassNames = [NSMutableDictionary dictionary];
NSMutableDictionary<NSString *, NSNumber *> *mutableSizesForClassNames = [NSMutableDictionary dictionary];
for (unsigned int i = 0; i < classCount; i++) {
Class class = classes[i];
NSUInteger instanceCount = (NSUInteger)CFDictionaryGetValue(mutableCountsForClasses, (__bridge const void *)(class));
NSString *className = @(class_getName(class));
if (instanceCount > 0) {
NSString *className = @(class_getName(class));
[mutableCountsForClassNames setObject:@(instanceCount) forKey:className];
}
[mutableSizesForClassNames setObject:@(class_getInstanceSize(class)) forKey:className];
}
free(classes);
self.instanceCountsForClassNames = mutableCountsForClassNames;
self.instanceSizesForClassNames = mutableSizesForClassNames;
[self updateTableDataForSearchFilter];
[self updateSearchResults:nil];
}
- (void)refreshControlDidRefresh:(id)sender
@@ -94,74 +104,93 @@ static const NSInteger kFLEXLiveObjectsSortByCountIndex = 1;
[self.refreshControl endRefreshing];
}
- (void)updateTitle
- (void)updateHeaderTitle
{
NSString *title = @"Live Objects";
NSUInteger totalCount = 0;
NSUInteger totalSize = 0;
for (NSString *className in self.allClassNames) {
totalCount += [[self.instanceCountsForClassNames objectForKey:className] unsignedIntegerValue];
NSUInteger count = self.instanceCountsForClassNames[className].unsignedIntegerValue;
totalCount += count;
totalSize += count * self.instanceSizesForClassNames[className].unsignedIntegerValue;
}
NSUInteger filteredCount = 0;
NSUInteger filteredSize = 0;
for (NSString *className in self.filteredClassNames) {
filteredCount += [[self.instanceCountsForClassNames objectForKey:className] unsignedIntegerValue];
NSUInteger count = self.instanceCountsForClassNames[className].unsignedIntegerValue;
filteredCount += count;
filteredSize += count * self.instanceSizesForClassNames[className].unsignedIntegerValue;
}
if (filteredCount == totalCount) {
// Unfiltered
title = [title stringByAppendingFormat:@" (%lu)", (unsigned long)totalCount];
self.headerTitle = [NSString
stringWithFormat:@"%@ objects, %@",
@(totalCount), [NSByteCountFormatter
stringFromByteCount:totalSize
countStyle:NSByteCountFormatterCountStyleFile
]
];
} else {
title = [title stringByAppendingFormat:@" (filtered, %lu)", (unsigned long)filteredCount];
self.headerTitle = [NSString
stringWithFormat:@"%@ of %@ objects, %@",
@(filteredCount), @(totalCount), [NSByteCountFormatter
stringFromByteCount:filteredSize
countStyle:NSByteCountFormatterCountStyleFile
]
];
}
}
#pragma mark - FLEXGlobalsEntry
+ (NSString *)globalsEntryTitle:(FLEXGlobalsRow)row {
return @"💩 Heap Objects";
}
+ (UIViewController *)globalsEntryViewController:(FLEXGlobalsRow)row {
FLEXLiveObjectsTableViewController *liveObjectsViewController = [self new];
liveObjectsViewController.title = [self globalsEntryTitle:row];
return liveObjectsViewController;
}
#pragma mark - Search bar
- (void)updateSearchResults:(NSString *)filter
{
NSInteger selectedScope = self.selectedScope;
self.title = title;
}
#pragma mark - Search
- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText
{
[self updateTableDataForSearchFilter];
}
- (void)searchBarSearchButtonClicked:(UISearchBar *)searchBar
{
[searchBar resignFirstResponder];
}
- (void)searchBar:(UISearchBar *)searchBar selectedScopeButtonIndexDidChange:(NSInteger)selectedScope
{
[self updateTableDataForSearchFilter];
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
// Dismiss the keyboard when interacting with filtered results.
[self.searchBar endEditing:YES];
}
- (void)updateTableDataForSearchFilter
{
if ([self.searchBar.text length] > 0) {
NSPredicate *searchPreidcate = [NSPredicate predicateWithFormat:@"SELF CONTAINS[cd] %@", self.searchBar.text];
self.filteredClassNames = [self.allClassNames filteredArrayUsingPredicate:searchPreidcate];
if (filter.length) {
NSPredicate *searchPredicate = [NSPredicate predicateWithFormat:@"SELF CONTAINS[cd] %@", filter];
self.filteredClassNames = [self.allClassNames filteredArrayUsingPredicate:searchPredicate];
} else {
self.filteredClassNames = self.allClassNames;
}
if (self.searchBar.selectedScopeButtonIndex == kFLEXLiveObjectsSortAlphabeticallyIndex) {
if (selectedScope == kFLEXLiveObjectsSortAlphabeticallyIndex) {
self.filteredClassNames = [self.filteredClassNames sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)];
} else if (self.searchBar.selectedScopeButtonIndex == kFLEXLiveObjectsSortByCountIndex) {
} else if (selectedScope == kFLEXLiveObjectsSortByCountIndex) {
self.filteredClassNames = [self.filteredClassNames sortedArrayUsingComparator:^NSComparisonResult(NSString *className1, NSString *className2) {
NSNumber *count1 = [self.instanceCountsForClassNames objectForKey:className1];
NSNumber *count2 = [self.instanceCountsForClassNames objectForKey:className2];
NSNumber *count1 = self.instanceCountsForClassNames[className1];
NSNumber *count2 = self.instanceCountsForClassNames[className2];
// Reversed for descending counts.
return [count2 compare:count1];
}];
} else if (selectedScope == kFLEXLiveObjectsSortBySizeIndex) {
self.filteredClassNames = [self.filteredClassNames sortedArrayUsingComparator:^NSComparisonResult(NSString *className1, NSString *className2) {
NSNumber *count1 = self.instanceCountsForClassNames[className1];
NSNumber *count2 = self.instanceCountsForClassNames[className2];
NSNumber *size1 = self.instanceSizesForClassNames[className1];
NSNumber *size2 = self.instanceSizesForClassNames[className2];
// Reversed for descending sizes.
return [@(count2.integerValue * size2.integerValue) compare:@(count1.integerValue * size1.integerValue)];
}];
}
[self updateTitle];
[self updateHeaderTitle];
[self.tableView reloadData];
}
@@ -175,32 +204,43 @@ static const NSInteger kFLEXLiveObjectsSortByCountIndex = 1;
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [self.filteredClassNames count];
return self.filteredClassNames.count;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
- (UITableViewCell *)tableView:(__kindof 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];
}
UITableViewCell *cell = [tableView
dequeueReusableCellWithIdentifier:kFLEXDefaultCell
forIndexPath:indexPath
];
NSString *className = self.filteredClassNames[indexPath.row];
NSNumber *count = [self.instanceCountsForClassNames objectForKey:className];
cell.textLabel.text = [NSString stringWithFormat:@"%@ (%ld)", className, (long)[count integerValue]];
NSNumber *count = self.instanceCountsForClassNames[className];
NSNumber *size = self.instanceSizesForClassNames[className];
unsigned long totalSize = count.unsignedIntegerValue * size.unsignedIntegerValue;
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
cell.textLabel.text = [NSString stringWithFormat:@"%@ (%ld, %@)",
className, (long)[count integerValue],
[NSByteCountFormatter
stringFromByteCount:totalSize
countStyle:NSByteCountFormatterCountStyleFile
]
];
return cell;
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
return self.headerTitle;
}
#pragma mark - Table view delegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
NSString *className = [self.filteredClassNames objectAtIndex:indexPath.row];
NSString *className = self.filteredClassNames[indexPath.row];
FLEXInstancesTableViewController *instancesViewController = [FLEXInstancesTableViewController instancesTableViewControllerForClassName:className];
[self.navigationController pushViewController:instancesViewController animated:YES];
}
@@ -0,0 +1,22 @@
//
// FLEXObjectRef.h
// FLEX
//
// Created by Tanner Bennett on 7/24/18.
// Copyright (c) 2018 Flipboard. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface FLEXObjectRef : NSObject
+ (instancetype)referencing:(id)object;
+ (instancetype)referencing:(id)object ivar:(NSString *)ivarName;
+ (NSArray<FLEXObjectRef *> *)referencingAll:(NSArray *)objects;
/// For example, "NSString 0x1d4085d0" or "NSLayoutConstraint _object"
@property (nonatomic, readonly) NSString *reference;
@property (nonatomic, readonly) id object;
@end
@@ -0,0 +1,47 @@
//
// FLEXObjectRef.m
// FLEX
//
// Created by Tanner Bennett on 7/24/18.
// Copyright (c) 2018 Flipboard. All rights reserved.
//
#import "FLEXObjectRef.h"
#import <objc/runtime.h>
@implementation FLEXObjectRef
+ (instancetype)referencing:(id)object {
return [[self alloc] initWithObject:object ivarName:nil];
}
+ (instancetype)referencing:(id)object ivar:(NSString *)ivarName {
return [[self alloc] initWithObject:object ivarName:ivarName];
}
+ (NSArray<FLEXObjectRef *> *)referencingAll:(NSArray *)objects {
NSMutableArray<FLEXObjectRef *> *refs = [NSMutableArray array];
for (id obj in objects) {
[refs addObject:[self referencing:obj]];
}
return refs;
}
- (id)initWithObject:(id)object ivarName:(NSString *)ivar {
self = [super init];
if (self) {
_object = object;
NSString *class = NSStringFromClass(object_getClass(object));
if (ivar) {
_reference = [NSString stringWithFormat:@"%@ %@", class, ivar];
} else {
_reference = [NSString stringWithFormat:@"%@ %p", class, object];
}
}
return self;
}
@end
@@ -8,11 +8,12 @@
#import "FLEXWebViewController.h"
#import "FLEXUtility.h"
#import <WebKit/WebKit.h>
@interface FLEXWebViewController () <UIWebViewDelegate>
@interface FLEXWebViewController () <WKNavigationDelegate>
@property (nonatomic, strong) UIWebView *webView;
@property (nonatomic, strong) NSString *originalText;
@property (nonatomic) WKWebView *webView;
@property (nonatomic) NSString *originalText;
@end
@@ -22,10 +23,14 @@
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
self.webView = [[UIWebView alloc] init];
self.webView.delegate = self;
self.webView.dataDetectorTypes = UIDataDetectorTypeLink;
self.webView.scalesPageToFit = YES;
WKWebViewConfiguration *configuration = [WKWebViewConfiguration new];
if (@available(iOS 10.0, *)) {
configuration.dataDetectorTypes = UIDataDetectorTypeLink;
}
self.webView = [[WKWebView alloc] initWithFrame:CGRectZero configuration:configuration];
self.webView.navigationDelegate = self;
}
return self;
}
@@ -53,9 +58,9 @@
- (void)dealloc
{
// UIWebView's delegate is assign so we need to clear it manually.
if (_webView.delegate == self) {
_webView.delegate = nil;
// WKWebView's delegate is assigned so we need to clear it manually.
if (_webView.navigationDelegate == self) {
_webView.navigationDelegate = nil;
}
}
@@ -67,33 +72,34 @@
self.webView.frame = self.view.bounds;
self.webView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
if ([self.originalText length] > 0) {
if (self.originalText.length > 0) {
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Copy" style:UIBarButtonItemStylePlain target:self action:@selector(copyButtonTapped:)];
}
}
- (void)copyButtonTapped:(id)sender
{
[[UIPasteboard generalPasteboard] setString:self.originalText];
[UIPasteboard.generalPasteboard setString:self.originalText];
}
#pragma mark - UIWebView Delegate
#pragma mark - WKWebView Delegate
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
{
BOOL shouldStart = NO;
if (navigationType == UIWebViewNavigationTypeOther) {
WKNavigationActionPolicy policy = WKNavigationActionPolicyCancel;
if (navigationAction.navigationType == WKNavigationTypeOther) {
// Allow the initial load
shouldStart = YES;
policy = WKNavigationActionPolicyAllow;
} else {
// For clicked links, push another web view controller onto the navigation stack so that hitting the back button works as expected.
// Don't allow the current web view do handle the navigation.
// Don't allow the current web view to handle the navigation.
NSURLRequest *request = navigationAction.request;
FLEXWebViewController *webVC = [[[self class] alloc] initWithURL:[request URL]];
webVC.title = [[request URL] absoluteString];
[self.navigationController pushViewController:webVC animated:YES];
}
return shouldStart;
decisionHandler(policy);
}
@@ -102,26 +108,26 @@
+ (BOOL)supportsPathExtension:(NSString *)extension
{
BOOL supported = NO;
NSSet *supportedExtensions = [self webViewSupportedPathExtensions];
NSSet<NSString *> *supportedExtensions = [self webViewSupportedPathExtensions];
if ([supportedExtensions containsObject:[extension lowercaseString]]) {
supported = YES;
}
return supported;
}
+ (NSSet *)webViewSupportedPathExtensions
+ (NSSet<NSString *> *)webViewSupportedPathExtensions
{
static NSSet *pathExtenstions = nil;
static NSSet<NSString *> *pathExtensions = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// Note that this is not exhaustive, but all these extensions should work well in the web view.
// See https://developer.apple.com/library/ios/documentation/AppleApplications/Reference/SafariWebContent/CreatingContentforSafarioniPhone/CreatingContentforSafarioniPhone.html#//apple_ref/doc/uid/TP40006482-SW7
pathExtenstions = [NSSet setWithArray:@[@"jpg", @"jpeg", @"png", @"gif", @"pdf", @"svg", @"tiff", @"3gp", @"3gpp", @"3g2",
@"3gp2", @"aiff", @"aif", @"aifc", @"cdda", @"amr", @"mp3", @"swa", @"mp4", @"mpeg",
@"mpg", @"mp3", @"wav", @"bwf", @"m4a", @"m4b", @"m4p", @"mov", @"qt", @"mqv", @"m4v"]];
// See https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariWebContent/CreatingContentforSafarioniPhone/CreatingContentforSafarioniPhone.html#//apple_ref/doc/uid/TP40006482-SW7
pathExtensions = [NSSet<NSString *> setWithArray:@[@"jpg", @"jpeg", @"png", @"gif", @"pdf", @"svg", @"tiff", @"3gp", @"3gpp", @"3g2",
@"3gp2", @"aiff", @"aif", @"aifc", @"cdda", @"amr", @"mp3", @"swa", @"mp4", @"mpeg",
@"mpg", @"mp3", @"wav", @"bwf", @"m4a", @"m4b", @"m4p", @"mov", @"qt", @"mqv", @"m4v"]];
});
return pathExtenstions;
return pathExtensions;
}
@end
@@ -1,6 +1,6 @@
//
// FLEXFileBrowserSearchOperation.h
// UICatalog
// FLEX
//
// Created by 啟倫 陳 on 2014/8/4.
// Copyright (c) 2014年 f. All rights reserved.
@@ -20,6 +20,6 @@
@protocol FLEXFileBrowserSearchOperationDelegate <NSObject>
- (void)fileBrowserSearchOperationResult:(NSArray *)searchResult size:(uint64_t)size;
- (void)fileBrowserSearchOperationResult:(NSArray<NSString *> *)searchResult size:(uint64_t)size;
@end
@@ -1,6 +1,6 @@
//
// FLEXFileBrowserSearchOperation.m
// UICatalog
// FLEX
//
// Created by on 2014/8/4.
// Copyright (c) 2014 f. All rights reserved.
@@ -17,7 +17,7 @@
- (id)flex_pop
{
id anObject = [self lastObject];
id anObject = self.lastObject;
[self removeLastObject];
return anObject;
}
@@ -26,8 +26,8 @@
@interface FLEXFileBrowserSearchOperation ()
@property (nonatomic, strong) NSString *path;
@property (nonatomic, strong) NSString *searchString;
@property (nonatomic) NSString *path;
@property (nonatomic) NSString *searchString;
@end
@@ -37,8 +37,8 @@
- (uint64_t)totalSizeAtPath:(NSString *)path
{
NSFileManager *fileManager = [NSFileManager defaultManager];
NSDictionary *attributes = [fileManager attributesOfItemAtPath:path error:NULL];
NSFileManager *fileManager = NSFileManager.defaultManager;
NSDictionary<NSString *, id> *attributes = [fileManager attributesOfItemAtPath:path error:NULL];
uint64_t totalSize = [attributes fileSize];
for (NSString *fileName in [fileManager enumeratorAtPath:path]) {
@@ -64,17 +64,17 @@
- (void)main
{
NSFileManager *fileManager = [NSFileManager defaultManager];
NSMutableArray *searchPaths = [NSMutableArray array];
NSMutableDictionary *sizeMapping = [NSMutableDictionary dictionary];
NSFileManager *fileManager = NSFileManager.defaultManager;
NSMutableArray<NSString *> *searchPaths = [NSMutableArray array];
NSMutableDictionary<NSString *, NSNumber *> *sizeMapping = [NSMutableDictionary dictionary];
uint64_t totalSize = 0;
NSMutableArray *stack = [NSMutableArray array];
NSMutableArray<NSString *> *stack = [NSMutableArray array];
[stack flex_push:self.path];
//recursive found all match searchString paths, and precomputing there size
while ([stack count]) {
while (stack.count) {
NSString *currentPath = [stack flex_pop];
NSArray *directoryPath = [fileManager contentsOfDirectoryAtPath:currentPath error:nil];
NSArray<NSString *> *directoryPath = [fileManager contentsOfDirectoryAtPath:currentPath error:nil];
for (NSString *subPath in directoryPath) {
NSString *fullPath = [currentPath stringByAppendingPathComponent:subPath];
@@ -99,7 +99,7 @@
}
//sort
NSArray *sortedArray = [searchPaths sortedArrayUsingComparator:^NSComparisonResult(NSString *path1, NSString *path2) {
NSArray<NSString *> *sortedArray = [searchPaths sortedArrayUsingComparator:^NSComparisonResult(NSString *path1, NSString *path2) {
uint64_t pathSize1 = [sizeMapping[path1] unsignedLongLongValue];
uint64_t pathSize2 = [sizeMapping[path2] unsignedLongLongValue];
if (pathSize1 < pathSize2) {
@@ -6,12 +6,13 @@
// Based on previous work by Evan Doll
//
#import <UIKit/UIKit.h>
#import "FLEXTableViewController.h"
#import "FLEXGlobalsEntry.h"
#import "FLEXFileBrowserSearchOperation.h"
@interface FLEXFileBrowserTableViewController : UITableViewController <UISearchDisplayDelegate, FLEXFileBrowserSearchOperationDelegate>
@interface FLEXFileBrowserTableViewController : FLEXTableViewController <FLEXGlobalsEntry>
+ (instancetype)path:(NSString *)path;
- (id)initWithPath:(NSString *)path;
@end
@@ -0,0 +1,506 @@
//
// FLEXFileBrowserTableViewController.m
// Flipboard
//
// Created by Ryan Olson on 6/9/14.
//
//
#import "FLEXFileBrowserTableViewController.h"
#import "FLEXUtility.h"
#import "FLEXWebViewController.h"
#import "FLEXImagePreviewViewController.h"
#import "FLEXTableListViewController.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXObjectExplorerViewController.h"
@interface FLEXFileBrowserTableViewCell : UITableViewCell
@end
@interface FLEXFileBrowserTableViewController () <FLEXFileBrowserSearchOperationDelegate>
@property (nonatomic, copy) NSString *path;
@property (nonatomic, copy) NSArray<NSString *> *childPaths;
@property (nonatomic) NSArray<NSString *> *searchPaths;
@property (nonatomic) NSNumber *recursiveSize;
@property (nonatomic) NSNumber *searchPathsSize;
@property (nonatomic) NSOperationQueue *operationQueue;
@property (nonatomic) UIDocumentInteractionController *documentController;
@end
@implementation FLEXFileBrowserTableViewController
+ (instancetype)path:(NSString *)path
{
return [[self alloc] initWithPath:path];
}
- (id)init
{
return [self initWithPath:NSHomeDirectory()];
}
- (id)initWithPath:(NSString *)path
{
self = [super init];
if (self) {
self.path = path;
self.title = [path lastPathComponent];
self.operationQueue = [NSOperationQueue new];
//computing path size
FLEXFileBrowserTableViewController *__weak weakSelf = self;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSFileManager *fileManager = NSFileManager.defaultManager;
NSDictionary<NSString *, id> *attributes = [fileManager attributesOfItemAtPath:path error:NULL];
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 reloadCurrentPath];
}
return self;
}
#pragma mark - UIViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.showsSearchBar = YES;
self.searchBarDebounceInterval = kFLEXDebounceForAsyncSearch;
}
#pragma mark - FLEXGlobalsEntry
+ (NSString *)globalsEntryTitle:(FLEXGlobalsRow)row {
switch (row) {
case FLEXGlobalsRowBrowseBundle: return @"📁 Browse Bundle Directory";
case FLEXGlobalsRowBrowseContainer: return @"📁 Browse Container Directory";
default: return nil;
}
}
+ (UIViewController *)globalsEntryViewController:(FLEXGlobalsRow)row {
switch (row) {
case FLEXGlobalsRowBrowseBundle: return [[self alloc] initWithPath:NSBundle.mainBundle.bundlePath];
case FLEXGlobalsRowBrowseContainer: return [[self alloc] initWithPath:NSHomeDirectory()];
default: return [self new];
}
}
#pragma mark - FLEXFileBrowserSearchOperationDelegate
- (void)fileBrowserSearchOperationResult:(NSArray<NSString *> *)searchResult size:(uint64_t)size
{
self.searchPaths = searchResult;
self.searchPathsSize = @(size);
[self.tableView reloadData];
}
#pragma mark - Search bar
- (void)updateSearchResults:(NSString *)newText
{
[self reloadDisplayedPaths];
}
#pragma mark UISearchControllerDelegate
- (void)willDismissSearchController:(UISearchController *)searchController
{
[self.operationQueue cancelAllOperations];
[self reloadCurrentPath];
[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<NSString *> *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<NSString *, id> *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 item%@", (unsigned long)count, (count == 1 ? @"" : @"s")];
} else {
NSString *sizeString = [NSByteCountFormatter stringFromByteCount:attributes.fileSize countStyle:NSByteCountFormatterCountStyleFile];
subtitle = [NSString stringWithFormat:@"%@ - %@", sizeString, attributes.fileModificationDate ?: @"Never modified"];
}
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.
UIImage *image = [UIImage imageWithContentsOfFile:fullPath];
NSString *cellIdentifier = image ? 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 (image) {
cell.imageView.contentMode = UIViewContentModeScaleAspectFit;
cell.imageView.image = image;
}
return cell;
}
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
[self.tableView deselectRowAtIndexPath:indexPath animated:YES];
NSString *fullPath = [self filePathAtIndexPath:indexPath];
NSString *subpath = fullPath.lastPathComponent;
NSString *pathExtension = subpath.pathExtension;
BOOL isDirectory = NO;
BOOL stillExists = [NSFileManager.defaultManager fileExistsAtPath:fullPath isDirectory:&isDirectory];
UITableViewCell *cell = [self.tableView cellForRowAtIndexPath:indexPath];
UIImage *image = cell.imageView.image;
if (!stillExists) {
[FLEXAlert showAlert:@"File Not Found" message:@"The file at the specified path no longer exists." from:self];
[self reloadDisplayedPaths];
return;
}
UIViewController *drillInViewController = nil;
if (isDirectory) {
drillInViewController = [[[self class] alloc] initWithPath:fullPath];
} else if (image) {
drillInViewController = [[FLEXImagePreviewViewController alloc] initWithImage:image];
} else {
NSData *fileData = [NSData dataWithContentsOfFile:fullPath];
if (!fileData.length) {
[FLEXAlert showAlert:@"Empty File" message:@"No data returned from the file." from:self];
return;
}
// Special case keyed archives, json, and plists to get more readable data.
NSString *prettyString = nil;
if ([pathExtension isEqualToString:@"json"]) {
prettyString = [FLEXUtility prettyJSONStringFromData:fileData];
} else {
// Regardless of file extension...
id object = nil;
@try {
// Try to decode an archived object regardless of file extension
object = [NSKeyedUnarchiver unarchiveObjectWithData:fileData];
} @catch (NSException *e) { }
// Try to decode other things instead
object = object
?: [NSPropertyListSerialization propertyListWithData:fileData
options:0
format:NULL
error:NULL]
?: [NSDictionary dictionaryWithContentsOfFile:fullPath]
?: [NSArray arrayWithContentsOfFile:fullPath];
if (object) {
drillInViewController = [FLEXObjectExplorerFactory explorerViewControllerForObject:object];
}
}
if (prettyString.length) {
drillInViewController = [[FLEXWebViewController alloc] initWithText:prettyString];
} else if ([FLEXWebViewController supportsPathExtension:pathExtension]) {
drillInViewController = [[FLEXWebViewController alloc] initWithURL:[NSURL fileURLWithPath:fullPath]];
} else if ([FLEXTableListViewController supportsExtension:pathExtension]) {
drillInViewController = [[FLEXTableListViewController alloc] initWithPath:fullPath];
}
else if (!drillInViewController) {
NSString *fileString = [NSString stringWithUTF8String:fileData.bytes];
if (fileString.length) {
drillInViewController = [[FLEXWebViewController alloc] initWithText:fileString];
}
}
}
if (drillInViewController) {
drillInViewController.title = subpath.lastPathComponent;
[self.navigationController pushViewController:drillInViewController animated:YES];
} else {
// Share the file otherwise
[self openFileController:fullPath];
}
}
- (BOOL)tableView:(UITableView *)tableView shouldShowMenuForRowAtIndexPath:(NSIndexPath *)indexPath
{
UIMenuItem *rename = [[UIMenuItem alloc] initWithTitle:@"Rename" action:@selector(fileBrowserRename:)];
UIMenuItem *delete = [[UIMenuItem alloc] initWithTitle:@"Delete" action:@selector(fileBrowserDelete:)];
UIMenuItem *copyPath = [[UIMenuItem alloc] initWithTitle:@"Copy Path" action:@selector(fileBrowserCopyPath:)];
UIMenuItem *share = [[UIMenuItem alloc] initWithTitle:@"Share" action:@selector(fileBrowserShare:)];
UIMenuController.sharedMenuController.menuItems = @[rename, delete, copyPath, share];
return YES;
}
- (BOOL)tableView:(UITableView *)tableView canPerformAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender
{
return action == @selector(fileBrowserDelete:)
|| action == @selector(fileBrowserRename:)
|| action == @selector(fileBrowserCopyPath:)
|| action == @selector(fileBrowserShare:);
}
- (void)tableView:(UITableView *)tableView performAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender
{
// 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.
}
#if FLEX_AT_LEAST_IOS13_SDK
- (UIContextMenuConfiguration *)tableView:(UITableView *)tableView contextMenuConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath point:(CGPoint)point __IOS_AVAILABLE(13.0)
{
__weak typeof(self) weakSelf = self;
return [UIContextMenuConfiguration configurationWithIdentifier:nil
previewProvider:nil
actionProvider:^UIMenu * _Nullable(NSArray<UIMenuElement *> * _Nonnull suggestedActions) {
UITableViewCell * const cell = [tableView cellForRowAtIndexPath:indexPath];
UIAction *rename = [UIAction actionWithTitle:@"Rename"
image:nil
identifier:@"Rename"
handler:^(__kindof UIAction * _Nonnull action) {
[weakSelf fileBrowserRename:cell];
}];
UIAction *delete = [UIAction actionWithTitle:@"Delete"
image:nil
identifier:@"Delete"
handler:^(__kindof UIAction * _Nonnull action) {
[weakSelf fileBrowserDelete:cell];
}];
UIAction *copyPath = [UIAction actionWithTitle:@"Copy Path"
image:nil
identifier:@"Copy Path"
handler:^(__kindof UIAction * _Nonnull action) {
[weakSelf fileBrowserCopyPath:cell];
}];
UIAction *share = [UIAction actionWithTitle:@"Share"
image:nil
identifier:@"Share"
handler:^(__kindof UIAction * _Nonnull action) {
[weakSelf fileBrowserShare:cell];
}];
return [UIMenu menuWithTitle:@"Manage File" image:nil identifier:@"Manage File" options:UIMenuOptionsDisplayInline children:@[rename, delete, copyPath, share]];
}];
}
#endif
- (void)openFileController:(NSString *)fullPath
{
UIDocumentInteractionController *controller = [UIDocumentInteractionController new];
controller.URL = [NSURL fileURLWithPath: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];
BOOL stillExists = [NSFileManager.defaultManager fileExistsAtPath:self.path isDirectory:NULL];
if (stillExists) {
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title([NSString stringWithFormat:@"Rename %@?", fullPath.lastPathComponent]);
make.configuredTextField(^(UITextField *textField) {
textField.placeholder = @"New file name";
textField.text = fullPath.lastPathComponent;
});
make.button(@"Rename").handler(^(NSArray<NSString *> *strings) {
NSString *newFileName = strings.firstObject;
NSString *newPath = [fullPath.stringByDeletingLastPathComponent stringByAppendingPathComponent:newFileName];
[NSFileManager.defaultManager moveItemAtPath:fullPath toPath:newPath error:NULL];
[self reloadDisplayedPaths];
});
make.button(@"Cancel").cancelStyle();
} showFrom:self];
} else {
[FLEXAlert showAlert:@"File Removed" message:@"The file at the specified path no longer exists." from:self];
}
}
- (void)fileBrowserDelete:(UITableViewCell *)sender
{
NSIndexPath *indexPath = [self.tableView indexPathForCell:sender];
NSString *fullPath = [self filePathAtIndexPath:indexPath];
BOOL isDirectory = NO;
BOOL stillExists = [NSFileManager.defaultManager fileExistsAtPath:fullPath isDirectory:&isDirectory];
if (stillExists) {
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(@"Confirm Deletion");
make.message([NSString stringWithFormat:
@"The %@ '%@' will be deleted. This operation cannot be undone",
(isDirectory ? @"directory" : @"file"), fullPath.lastPathComponent
]);
make.button(@"Delete").destructiveStyle().handler(^(NSArray<NSString *> *strings) {
[NSFileManager.defaultManager removeItemAtPath:fullPath error:NULL];
[self reloadDisplayedPaths];
});
make.button(@"Cancel").cancelStyle();
} showFrom:self];
} else {
[FLEXAlert showAlert:@"File Removed" message:@"The file at the specified path no longer exists." from:self];
}
}
- (void)fileBrowserCopyPath:(UITableViewCell *)sender
{
NSIndexPath *indexPath = [self.tableView indexPathForCell:sender];
NSString *fullPath = [self filePathAtIndexPath:indexPath];
UIPasteboard.generalPasteboard.string = fullPath;
}
- (void)fileBrowserShare:(UITableViewCell *)sender
{
NSIndexPath *indexPath = [self.tableView indexPathForCell:sender];
NSString *pathString = [self filePathAtIndexPath:indexPath];
NSURL *filePath = [NSURL fileURLWithPath:pathString];
BOOL isDirectory = NO;
[NSFileManager.defaultManager fileExistsAtPath:pathString isDirectory:&isDirectory];
if (isDirectory) {
// UIDocumentInteractionController for folders
[self openFileController:pathString];
} else {
// Share sheet for files
UIActivityViewController *shareSheet = [[UIActivityViewController alloc] initWithActivityItems:@[filePath] applicationActivities:nil];
[self presentViewController:shareSheet animated:true completion:nil];
}
}
- (void)reloadDisplayedPaths
{
if (self.searchController.isActive) {
[self updateSearchPaths];
} else {
[self reloadCurrentPath];
[self.tableView reloadData];
}
}
- (void)reloadCurrentPath
{
NSMutableArray<NSString *> *childPaths = [NSMutableArray array];
NSArray<NSString *> *subpaths = [NSFileManager.defaultManager contentsOfDirectoryAtPath:self.path error:NULL];
for (NSString *subpath in subpaths) {
[childPaths addObject:[self.path stringByAppendingPathComponent:subpath]];
}
self.childPaths = childPaths;
}
- (void)updateSearchPaths
{
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.searchText];
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)forwardAction:(SEL)action withSender:(id)sender
{
id target = [self.nextResponder targetForAction:action withSender:sender];
[UIApplication.sharedApplication sendAction:action to:target from:self forEvent:nil];
}
- (void)fileBrowserRename:(UIMenuController *)sender
{
[self forwardAction:_cmd withSender:sender];
}
- (void)fileBrowserDelete:(UIMenuController *)sender
{
[self forwardAction:_cmd withSender:sender];
}
- (void)fileBrowserCopyPath:(UIMenuController *)sender
{
[self forwardAction:_cmd withSender:sender];
}
- (void)fileBrowserShare:(UIMenuController *)sender
{
[self forwardAction:_cmd withSender:sender];
}
@end
@@ -0,0 +1,81 @@
//
// FLEXGlobalsEntry.h
// FLEX
//
// Created by Javier Soto on 7/26/14.
// Copyright (c) 2014 f. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "FLEXTableViewSection.h"
@class FLEXGlobalsTableViewController;
typedef NS_ENUM(NSUInteger, FLEXGlobalsRow) {
FLEXGlobalsRowProcessInfo,
FLEXGlobalsRowNetworkHistory,
FLEXGlobalsRowSystemLog,
FLEXGlobalsRowLiveObjects,
FLEXGlobalsRowAddressInspector,
FLEXGlobalsRowCookies,
FLEXGlobalsRowSystemLibraries,
FLEXGlobalsRowAppClasses,
FLEXGlobalsRowAppKeychainItems,
FLEXGlobalsRowAppDelegate,
FLEXGlobalsRowRootViewController,
FLEXGlobalsRowUserDefaults,
FLEXGlobalsRowMainBundle,
FLEXGlobalsRowBrowseBundle,
FLEXGlobalsRowBrowseContainer,
FLEXGlobalsRowApplication,
FLEXGlobalsRowKeyWindow,
FLEXGlobalsRowMainScreen,
FLEXGlobalsRowCurrentDevice,
FLEXGlobalsRowPasteboard,
FLEXGlobalsRowCount
};
typedef NSString *(^FLEXGlobalsEntryNameFuture)(void);
/// Simply return a view controller to be pushed on the navigation stack
typedef UIViewController *(^FLEXGlobalsTableViewControllerViewControllerFuture)(void);
/// Do something like present an alert, then use the host
/// view controller to present or push another view controller.
typedef void (^FLEXGlobalsTableViewControllerRowAction)(FLEXGlobalsTableViewController *host);
/// For view controllers to conform to to indicate they support being used
/// in the globals table view controller. These methods help create concrete entries.
///
/// Previously, the concrete entries relied on "futures" for the view controller and title.
/// With this protocol, the conforming class itself can act as a future, since the methods
/// will not be invoked until the title and view controller / row action are needed.
@protocol FLEXGlobalsEntry <NSObject>
+ (NSString *)globalsEntryTitle:(FLEXGlobalsRow)row;
// Must respond to at least one of the below
@optional
+ (UIViewController *)globalsEntryViewController:(FLEXGlobalsRow)row;
+ (FLEXGlobalsTableViewControllerRowAction)globalsEntryRowAction:(FLEXGlobalsRow)row;
@end
@interface FLEXGlobalsEntry : NSObject <FLEXPatternMatching>
@property (nonatomic, readonly) FLEXGlobalsEntryNameFuture entryNameFuture;
@property (nonatomic, readonly) FLEXGlobalsTableViewControllerViewControllerFuture viewControllerFuture;
@property (nonatomic, readonly) FLEXGlobalsTableViewControllerRowAction rowAction;
+ (instancetype)entryWithEntry:(Class<FLEXGlobalsEntry>)entry row:(FLEXGlobalsRow)row;
+ (instancetype)entryWithNameFuture:(FLEXGlobalsEntryNameFuture)nameFuture viewControllerFuture:(FLEXGlobalsTableViewControllerViewControllerFuture)viewControllerFuture;
+ (instancetype)entryWithNameFuture:(FLEXGlobalsEntryNameFuture)nameFuture action:(FLEXGlobalsTableViewControllerRowAction)rowSelectedAction;
@end
@interface NSObject (FLEXGlobalsEntry)
/// @return The result of passing self to +[FLEXGlobalsEntry entryWithEntry:]
/// if the class conforms to FLEXGlobalsEntry, else, nil.
+ (FLEXGlobalsEntry *)flex_concreteGlobalsEntry:(FLEXGlobalsRow)row;
@end
@@ -0,0 +1,80 @@
//
// FLEXGlobalsEntry.m
// FLEX
//
// Created by Javier Soto on 7/26/14.
// Copyright (c) 2014 f. All rights reserved.
//
#import "FLEXGlobalsEntry.h"
@implementation FLEXGlobalsEntry
+ (instancetype)entryWithEntry:(Class<FLEXGlobalsEntry>)cls row:(FLEXGlobalsRow)row
{
NSParameterAssert(cls);
NSParameterAssert(
[cls respondsToSelector:@selector(globalsEntryViewController:)] ||
[cls respondsToSelector:@selector(globalsEntryRowAction:)]
);
FLEXGlobalsEntry *entry = [self new];
entry->_entryNameFuture = ^{ return [cls globalsEntryTitle:row]; };
if ([cls respondsToSelector:@selector(globalsEntryViewController:)]) {
entry->_viewControllerFuture = ^{ return [cls globalsEntryViewController:row]; };
} else {
entry->_rowAction = [cls globalsEntryRowAction:row];
}
return entry;
}
+ (instancetype)entryWithNameFuture:(FLEXGlobalsEntryNameFuture)nameFuture
viewControllerFuture:(FLEXGlobalsTableViewControllerViewControllerFuture)viewControllerFuture
{
NSParameterAssert(nameFuture);
NSParameterAssert(viewControllerFuture);
FLEXGlobalsEntry *entry = [self new];
entry->_entryNameFuture = [nameFuture copy];
entry->_viewControllerFuture = [viewControllerFuture copy];
return entry;
}
+ (instancetype)entryWithNameFuture:(FLEXGlobalsEntryNameFuture)nameFuture
action:(FLEXGlobalsTableViewControllerRowAction)rowSelectedAction
{
NSParameterAssert(nameFuture);
NSParameterAssert(rowSelectedAction);
FLEXGlobalsEntry *entry = [self new];
entry->_entryNameFuture = [nameFuture copy];
entry->_rowAction = [rowSelectedAction copy];
return entry;
}
#pragma mark FLEXPatternMatching
- (BOOL)matches:(NSString *)query
{
return [self.entryNameFuture() localizedCaseInsensitiveContainsString:query];
}
@end
#pragma mark - flex_concreteGlobalsEntry
@implementation NSObject (FLEXGlobalsEntry)
+ (FLEXGlobalsEntry *)flex_concreteGlobalsEntry:(FLEXGlobalsRow)row {
if ([self conformsToProtocol:@protocol(FLEXGlobalsEntry)]) {
return [FLEXGlobalsEntry entryWithEntry:self row:row];
}
return nil;
}
@end
@@ -6,11 +6,24 @@
// Copyright (c) 2014 Flipboard. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "FLEXTableViewController.h"
@protocol FLEXGlobalsTableViewControllerDelegate;
@interface FLEXGlobalsTableViewController : UITableViewController
typedef NS_ENUM(NSUInteger, FLEXGlobalsSection) {
/// NSProcessInfo, Network history, system log,
/// heap, address explorer, libraries, app classes
FLEXGlobalsSectionProcessAndEvents,
/// Browse container, browse bundle, NSBundle.main,
/// NSUserDefaults.standard, UIApplication,
/// app delegate, key window, root VC, cookies
FLEXGlobalsSectionAppShortcuts,
/// UIPasteBoard.general, UIScreen, UIDevice
FLEXGlobalsSectionMisc,
FLEXGlobalsSectionCustom,
FLEXGlobalsSectionCount
};
@interface FLEXGlobalsTableViewController : FLEXTableViewController
@property (nonatomic, weak) id <FLEXGlobalsTableViewControllerDelegate> delegate;
@@ -0,0 +1,292 @@
//
// 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 "FLEXRuntimeUtility.h"
#import "FLEXLibrariesTableViewController.h"
#import "FLEXClassesTableViewController.h"
#import "FLEXKeychainTableViewController.h"
#import "FLEXObjectExplorerViewController.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXLiveObjectsTableViewController.h"
#import "FLEXFileBrowserTableViewController.h"
#import "FLEXCookiesTableViewController.h"
#import "FLEXGlobalsEntry.h"
#import "FLEXManager+Private.h"
#import "FLEXSystemLogTableViewController.h"
#import "FLEXNetworkHistoryTableViewController.h"
#import "FLEXAddressExplorerCoordinator.h"
#import "FLEXTableViewSection.h"
static __weak UIWindow *s_applicationWindow = nil;
@interface FLEXGlobalsTableViewController ()
@property (nonatomic, readonly) NSArray<FLEXTableViewSection<FLEXGlobalsEntry *> *> *sections;
@property (nonatomic, copy) NSArray<FLEXTableViewSection<FLEXGlobalsEntry *> *> *filteredSections;
@end
@implementation FLEXGlobalsTableViewController
+ (NSString *)globalsTitleForSection:(FLEXGlobalsSection)section
{
switch (section) {
case FLEXGlobalsSectionProcessAndEvents:
return @"Process and Events";
case FLEXGlobalsSectionAppShortcuts:
return @"App Shortcuts";
case FLEXGlobalsSectionMisc:
return @"Miscellaneous";
case FLEXGlobalsSectionCustom:
return @"Custom Additions";
default:
@throw NSInternalInconsistencyException;
}
}
+ (FLEXGlobalsEntry *)globalsEntryForRow:(FLEXGlobalsRow)row
{
switch (row) {
case FLEXGlobalsRowAppClasses:
return [FLEXClassesTableViewController flex_concreteGlobalsEntry:row];
case FLEXGlobalsRowAppKeychainItems:
return [FLEXKeychainTableViewController flex_concreteGlobalsEntry:row];
case FLEXGlobalsRowAddressInspector:
return [FLEXAddressExplorerCoordinator flex_concreteGlobalsEntry:row];
case FLEXGlobalsRowSystemLibraries:
return [FLEXLibrariesTableViewController flex_concreteGlobalsEntry:row];
case FLEXGlobalsRowLiveObjects:
return [FLEXLiveObjectsTableViewController flex_concreteGlobalsEntry:row];
case FLEXGlobalsRowCookies:
return [FLEXCookiesTableViewController flex_concreteGlobalsEntry:row];
case FLEXGlobalsRowBrowseBundle:
case FLEXGlobalsRowBrowseContainer:
return [FLEXFileBrowserTableViewController flex_concreteGlobalsEntry:row];
case FLEXGlobalsRowSystemLog:
return [FLEXSystemLogTableViewController flex_concreteGlobalsEntry:row];
case FLEXGlobalsRowNetworkHistory:
return [FLEXNetworkHistoryTableViewController flex_concreteGlobalsEntry:row];
case FLEXGlobalsRowKeyWindow:
return [FLEXGlobalsEntry
entryWithNameFuture:^NSString *{
return @"🔑 -[UIApplication keyWindow]";
} viewControllerFuture:^UIViewController *{
return [FLEXObjectExplorerFactory explorerViewControllerForObject:s_applicationWindow];
}
];
case FLEXGlobalsRowRootViewController:
case FLEXGlobalsRowProcessInfo:
case FLEXGlobalsRowAppDelegate:
case FLEXGlobalsRowUserDefaults:
case FLEXGlobalsRowMainBundle:
case FLEXGlobalsRowApplication:
case FLEXGlobalsRowMainScreen:
case FLEXGlobalsRowCurrentDevice:
case FLEXGlobalsRowPasteboard:
return [FLEXObjectExplorerFactory flex_concreteGlobalsEntry:row];
default:
@throw [NSException
exceptionWithName:NSInternalInconsistencyException
reason:@"Missing globals case in switch" userInfo:nil
];
}
}
+ (NSArray<FLEXTableViewSection<FLEXGlobalsEntry *> *> *)defaultGlobalSections
{
static NSArray<FLEXTableViewSection<FLEXGlobalsEntry *> *> *sections = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSArray *rows = @[
@[
[self globalsEntryForRow:FLEXGlobalsRowNetworkHistory],
[self globalsEntryForRow:FLEXGlobalsRowSystemLog],
[self globalsEntryForRow:FLEXGlobalsRowProcessInfo],
[self globalsEntryForRow:FLEXGlobalsRowLiveObjects],
[self globalsEntryForRow:FLEXGlobalsRowAddressInspector],
[self globalsEntryForRow:FLEXGlobalsRowSystemLibraries],
[self globalsEntryForRow:FLEXGlobalsRowAppClasses],
],
@[ // FLEXGlobalsSectionAppShortcuts
[self globalsEntryForRow:FLEXGlobalsRowBrowseBundle],
[self globalsEntryForRow:FLEXGlobalsRowBrowseContainer],
[self globalsEntryForRow:FLEXGlobalsRowMainBundle],
[self globalsEntryForRow:FLEXGlobalsRowUserDefaults],
[self globalsEntryForRow:FLEXGlobalsRowAppKeychainItems],
[self globalsEntryForRow:FLEXGlobalsRowApplication],
[self globalsEntryForRow:FLEXGlobalsRowAppDelegate],
[self globalsEntryForRow:FLEXGlobalsRowKeyWindow],
[self globalsEntryForRow:FLEXGlobalsRowRootViewController],
[self globalsEntryForRow:FLEXGlobalsRowCookies],
],
@[ // FLEXGlobalsSectionMisc
[self globalsEntryForRow:FLEXGlobalsRowPasteboard],
[self globalsEntryForRow:FLEXGlobalsRowMainScreen],
[self globalsEntryForRow:FLEXGlobalsRowCurrentDevice],
]
];
NSMutableArray *tmp = [NSMutableArray array];
for (NSInteger i = 0; i < FLEXGlobalsSectionCount - 1; i++) { // Skip custom
NSString *title = [self globalsTitleForSection:i];
[tmp addObject:[FLEXTableViewSection section:i title:title rows:rows[i]]];
}
sections = tmp.copy;
});
return sections;
}
#pragma mark - Public
+ (void)setApplicationWindow:(UIWindow *)applicationWindow
{
s_applicationWindow = applicationWindow;
}
#pragma mark - UIViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.title = @"💪 FLEX";
self.showsSearchBar = YES;
self.searchBarDebounceInterval = kFLEXDebounceInstant;
// Table view data
_sections = [[self class] defaultGlobalSections];
if ([FLEXManager sharedManager].userGlobalEntries.count) {
// Make custom section
NSString *title = [[self class] globalsTitleForSection:FLEXGlobalsSectionCustom];
FLEXTableViewSection *custom = [FLEXTableViewSection
section:FLEXGlobalsSectionCustom
title:title
rows:[FLEXManager sharedManager].userGlobalEntries
];
_sections = [_sections arrayByAddingObject:custom];
}
// Done button
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemDone
target:self
action:@selector(donePressed:)
];
}
#pragma mark - Search Bar
- (void)updateSearchResults:(NSString *)newText {
if (!newText.length) {
self.filteredSections = nil;
[self.tableView reloadData];
return;
}
// Sections are a map of index to rows, since empty sections are omitted
NSMutableArray *filteredSections = [NSMutableArray array];
[self.sections enumerateObjectsUsingBlock:^(FLEXTableViewSection<FLEXGlobalsEntry *> *section, NSUInteger idx, BOOL *stop) {
section = [section newSectionWithRowsMatchingQuery:newText];
if (section) {
[filteredSections addObject:section];
}
}];
self.filteredSections = filteredSections.copy;
[self.tableView reloadData];
}
#pragma mark - Misc
- (void)donePressed:(id)sender
{
[self.delegate globalsViewControllerDidFinish:self];
}
#pragma mark - Table Data Helpers
- (FLEXGlobalsEntry *)globalEntryAtIndexPath:(NSIndexPath *)indexPath
{
if (self.filteredSections) {
return self.filteredSections[indexPath.section][indexPath.row];
} else {
return self.sections[indexPath.section][indexPath.row];
}
}
- (NSString *)titleForSection:(NSInteger)section
{
if (self.filteredSections) {
return self.filteredSections[section].title;
} else {
return self.sections[section].title;
}
}
- (NSString *)titleForRowAtIndexPath:(NSIndexPath *)indexPath
{
FLEXGlobalsEntry *entry = [self globalEntryAtIndexPath:indexPath];
return entry.entryNameFuture();
}
#pragma mark - Table View Data Source
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
return self.filteredSections ? self.filteredSections.count : self.sections.count;
}
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
if (self.filteredSections) {
return self.filteredSections[section].count;
} else {
return self.sections[section].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;
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
return [self titleForSection:section];
}
#pragma mark - Table View Delegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
FLEXGlobalsEntry *entry = [self globalEntryAtIndexPath:indexPath];
if (entry.viewControllerFuture) {
[self.navigationController pushViewController:entry.viewControllerFuture() animated:YES];
} else {
entry.rowAction(self);
}
}
@end
@@ -0,0 +1,141 @@
//
// FLEXKeychain.h
//
// Derived from:
// SSKeychain.h in SSKeychain
// Created by Sam Soffes on 5/19/10.
// Copyright (c) 2010-2014 Sam Soffes. All rights reserved.
//
#import <Foundation/Foundation.h>
/// Error code specific to FLEXKeychain that can be returned in NSError objects.
/// For codes returned by the operating system, refer to SecBase.h for your
/// platform.
typedef NS_ENUM(OSStatus, FLEXKeychainErrorCode) {
/// Some of the arguments were invalid.
FLEXKeychainErrorBadArguments = -1001,
};
/// FLEXKeychain error domain
extern NSString *const kFLEXKeychainErrorDomain;
/// Account name.
extern NSString *const kFLEXKeychainAccountKey;
/// Time the item was created.
///
/// The value will be a string.
extern NSString *const kFLEXKeychainCreatedAtKey;
/// Item class.
extern NSString *const kFLEXKeychainClassKey;
/// Item description.
extern NSString *const kFLEXKeychainDescriptionKey;
/// Item label.
extern NSString *const kFLEXKeychainLabelKey;
/// Time the item was last modified.
///
/// The value will be a string.
extern NSString *const kFLEXKeychainLastModifiedKey;
/// Where the item was created.
extern NSString *const kFLEXKeychainWhereKey;
/// A simple wrapper for accessing accounts, getting passwords,
/// setting passwords, and deleting passwords using the system Keychain.
@interface FLEXKeychain : NSObject
#pragma mark - Classic methods
/// @param serviceName The service for which to return the corresponding password.
/// @param account The account for which to return the corresponding password.
/// @return Returns a string containing the password for a given account and service,
/// or `nil` if the Keychain doesn't have a password for the given parameters.
+ (NSString *)passwordForService:(NSString *)serviceName account:(NSString *)account;
+ (NSString *)passwordForService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error;
/// Returns a nsdata containing the password for a given account and service,
/// or `nil` if the Keychain doesn't have a password for the given parameters.
///
/// @param serviceName The service for which to return the corresponding password.
/// @param account The account for which to return the corresponding password.
/// @return Returns a nsdata containing the password for a given account and service,
/// or `nil` if the Keychain doesn't have a password for the given parameters.
+ (NSData *)passwordDataForService:(NSString *)serviceName account:(NSString *)account;
+ (NSData *)passwordDataForService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error;
/// Deletes a password from the Keychain.
///
/// @param serviceName The service for which to delete the corresponding password.
/// @param account The account for which to delete the corresponding password.
/// @return Returns `YES` on success, or `NO` on failure.
+ (BOOL)deletePasswordForService:(NSString *)serviceName account:(NSString *)account;
+ (BOOL)deletePasswordForService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error;
/// Sets a password in the Keychain.
///
/// @param password The password to store in the Keychain.
/// @param serviceName The service for which to set the corresponding password.
/// @param account The account for which to set the corresponding password.
/// @return Returns `YES` on success, or `NO` on failure.
+ (BOOL)setPassword:(NSString *)password forService:(NSString *)serviceName account:(NSString *)account;
+ (BOOL)setPassword:(NSString *)password forService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error;
/// Sets a password in the Keychain.
///
/// @param password The password to store in the Keychain.
/// @param serviceName The service for which to set the corresponding password.
/// @param account The account for which to set the corresponding password.
/// @return Returns `YES` on success, or `NO` on failure.
+ (BOOL)setPasswordData:(NSData *)password forService:(NSString *)serviceName account:(NSString *)account;
+ (BOOL)setPasswordData:(NSData *)password forService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error;
/// @return An array of dictionaries containing the Keychain's accounts, or `nil` if
/// the Keychain doesn't have any accounts. The order of the objects in the array isn't defined.
///
/// @note See the `NSString` constants declared in FLEXKeychain.h for a list of keys that
/// can be used when accessing the dictionaries returned by this method.
+ (NSArray<NSDictionary<NSString *, id> *> *)allAccounts;
+ (NSArray<NSDictionary<NSString *, id> *> *)allAccounts:(NSError *__autoreleasing *)error;
/// @param serviceName The service for which to return the corresponding accounts.
/// @return An array of dictionaries containing the Keychain's accounts for a given `serviceName`,
/// or `nil` if the Keychain doesn't have any accounts for the given `serviceName`.
/// The order of the objects in the array isn't defined.
///
/// @note See the `NSString` constants declared in FLEXKeychain.h for a list of keys that
/// can be used when accessing the dictionaries returned by this method.
+ (NSArray<NSDictionary<NSString *, id> *> *)accountsForService:(NSString *)serviceName;
+ (NSArray<NSDictionary<NSString *, id> *> *)accountsForService:(NSString *)serviceName error:(NSError *__autoreleasing *)error;
#pragma mark - Configuration
#if __IPHONE_4_0 && TARGET_OS_IPHONE
/// Returns the accessibility type for all future passwords saved to the Keychain.
///
/// @return `NULL` or one of the "Keychain Item Accessibility
/// Constants" used for determining when a keychain item should be readable.
+ (CFTypeRef)accessibilityType;
/// Sets the accessibility type for all future passwords saved to the Keychain.
///
/// @param accessibilityType One of the "Keychain Item Accessibility Constants"
/// used for determining when a keychain item should be readable.
/// If the value is `NULL` (the default), the Keychain default will be used which
/// is highly insecure. You really should use at least `kSecAttrAccessibleAfterFirstUnlock`
/// for background applications or `kSecAttrAccessibleWhenUnlocked` for all
/// other applications.
///
/// @note See Security/SecItem.h
+ (void)setAccessibilityType:(CFTypeRef)accessibilityType;
#endif
@end
@@ -0,0 +1,120 @@
//
// FLEXKeychain.m
//
// Forked from:
// SSKeychain.m in SSKeychain
// Created by Sam Soffes on 5/19/10.
// Copyright (c) 2010-2014 Sam Soffes. All rights reserved.
//
#import "FLEXKeychain.h"
#import "FLEXKeychainQuery.h"
NSString * const kFLEXKeychainErrorDomain = @"com.flipboard.flex";
NSString * const kFLEXKeychainAccountKey = @"acct";
NSString * const kFLEXKeychainCreatedAtKey = @"cdat";
NSString * const kFLEXKeychainClassKey = @"labl";
NSString * const kFLEXKeychainDescriptionKey = @"desc";
NSString * const kFLEXKeychainLabelKey = @"labl";
NSString * const kFLEXKeychainLastModifiedKey = @"mdat";
NSString * const kFLEXKeychainWhereKey = @"svce";
#if __IPHONE_4_0 && TARGET_OS_IPHONE
static CFTypeRef FLEXKeychainAccessibilityType = NULL;
#endif
@implementation FLEXKeychain
+ (NSString *)passwordForService:(NSString *)serviceName account:(NSString *)account {
return [self passwordForService:serviceName account:account error:nil];
}
+ (NSString *)passwordForService:(NSString *)serviceName account:(NSString *)account error:(NSError *__autoreleasing *)error {
FLEXKeychainQuery *query = [FLEXKeychainQuery new];
query.service = serviceName;
query.account = account;
[query fetch:error];
return query.password;
}
+ (NSData *)passwordDataForService:(NSString *)serviceName account:(NSString *)account {
return [self passwordDataForService:serviceName account:account error:nil];
}
+ (NSData *)passwordDataForService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error {
FLEXKeychainQuery *query = [FLEXKeychainQuery new];
query.service = serviceName;
query.account = account;
[query fetch:error];
return query.passwordData;
}
+ (BOOL)deletePasswordForService:(NSString *)serviceName account:(NSString *)account {
return [self deletePasswordForService:serviceName account:account error:nil];
}
+ (BOOL)deletePasswordForService:(NSString *)serviceName account:(NSString *)account error:(NSError *__autoreleasing *)error {
FLEXKeychainQuery *query = [FLEXKeychainQuery new];
query.service = serviceName;
query.account = account;
return [query deleteItem:error];
}
+ (BOOL)setPassword:(NSString *)password forService:(NSString *)serviceName account:(NSString *)account {
return [self setPassword:password forService:serviceName account:account error:nil];
}
+ (BOOL)setPassword:(NSString *)password forService:(NSString *)serviceName account:(NSString *)account error:(NSError *__autoreleasing *)error {
FLEXKeychainQuery *query = [FLEXKeychainQuery new];
query.service = serviceName;
query.account = account;
query.password = password;
return [query save:error];
}
+ (BOOL)setPasswordData:(NSData *)password forService:(NSString *)serviceName account:(NSString *)account {
return [self setPasswordData:password forService:serviceName account:account error:nil];
}
+ (BOOL)setPasswordData:(NSData *)password forService:(NSString *)serviceName account:(NSString *)account error:(NSError **)error {
FLEXKeychainQuery *query = [FLEXKeychainQuery new];
query.service = serviceName;
query.account = account;
query.passwordData = password;
return [query save:error];
}
+ (NSArray *)allAccounts {
return [self allAccounts:nil];
}
+ (NSArray *)allAccounts:(NSError *__autoreleasing *)error {
return [self accountsForService:nil error:error];
}
+ (NSArray *)accountsForService:(NSString *)serviceName {
return [self accountsForService:serviceName error:nil];
}
+ (NSArray *)accountsForService:(NSString *)serviceName error:(NSError *__autoreleasing *)error {
FLEXKeychainQuery *query = [FLEXKeychainQuery new];
query.service = serviceName;
return [query fetchAll:error];
}
#if __IPHONE_4_0 && TARGET_OS_IPHONE
+ (CFTypeRef)accessibilityType {
return FLEXKeychainAccessibilityType;
}
+ (void)setAccessibilityType:(CFTypeRef)accessibilityType {
CFRetain(accessibilityType);
if (FLEXKeychainAccessibilityType) {
CFRelease(FLEXKeychainAccessibilityType);
}
FLEXKeychainAccessibilityType = accessibilityType;
}
#endif
@end
@@ -0,0 +1,112 @@
//
// FLEXKeychainQuery.h
//
// Derived from:
// SSKeychainQuery.h in SSKeychain
// Created by Caleb Davenport on 3/19/13.
// Copyright (c) 2010-2014 Sam Soffes. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <Security/Security.h>
#if __IPHONE_7_0 || __MAC_10_9
// Keychain synchronization available at compile time
#define FLEXKEYCHAIN_SYNCHRONIZATION_AVAILABLE 1
#endif
#if __IPHONE_3_0 || __MAC_10_9
// Keychain access group available at compile time
#define FLEXKEYCHAIN_ACCESS_GROUP_AVAILABLE 1
#endif
#ifdef FLEXKEYCHAIN_SYNCHRONIZATION_AVAILABLE
typedef NS_ENUM(NSUInteger, FLEXKeychainQuerySynchronizationMode) {
FLEXKeychainQuerySynchronizationModeAny,
FLEXKeychainQuerySynchronizationModeNo,
FLEXKeychainQuerySynchronizationModeYes
};
#endif
/// Simple interface for querying or modifying keychain items.
@interface FLEXKeychainQuery : NSObject
/// kSecAttrAccount
@property (nonatomic, copy) NSString *account;
/// kSecAttrService
@property (nonatomic, copy) NSString *service;
/// kSecAttrLabel
@property (nonatomic, copy) NSString *label;
#ifdef FLEXKEYCHAIN_ACCESS_GROUP_AVAILABLE
/// kSecAttrAccessGroup (only used on iOS)
@property (nonatomic, copy) NSString *accessGroup;
#endif
#ifdef FLEXKEYCHAIN_SYNCHRONIZATION_AVAILABLE
/// kSecAttrSynchronizable
@property (nonatomic) FLEXKeychainQuerySynchronizationMode synchronizationMode;
#endif
/// Root storage for password information
@property (nonatomic, copy) NSData *passwordData;
/// This property automatically transitions between an object and the value of
/// `passwordData` using NSKeyedArchiver and NSKeyedUnarchiver.
@property (nonatomic, copy) id<NSCoding> passwordObject;
/// Convenience accessor for setting and getting a password string. Passes through
/// to `passwordData` using UTF-8 string encoding.
@property (nonatomic, copy) NSString *password;
#pragma mark Saving & Deleting
/// Save the receiver's attributes as a keychain item. Existing items with the
/// given account, service, and access group will first be deleted.
///
/// @param error Populated should an error occur.
/// @return `YES` if saving was successful, `NO` otherwise.
- (BOOL)save:(NSError **)error;
/// Delete keychain items that match the given account, service, and access group.
///
/// @param error Populated should an error occur.
/// @return `YES` if saving was successful, `NO` otherwise.
- (BOOL)deleteItem:(NSError **)error;
#pragma mark Fetching
/// Fetch all keychain items that match the given account, service, and access
/// group. The values of `password` and `passwordData` are ignored when fetching.
///
/// @param error Populated should an error occur.
/// @return An array of dictionaries that represent all matching keychain items,
/// or `nil` should an error occur. The order of the items is not determined.
- (NSArray<NSDictionary<NSString *, id> *> *)fetchAll:(NSError **)error;
/// Fetch the keychain item that matches the given account, service, and access
/// group. The `password` and `passwordData` properties will be populated unless
/// an error occurs. The values of `password` and `passwordData` are ignored when
/// fetching.
///
/// @param error Populated should an error occur.
/// @return `YES` if fetching was successful, `NO` otherwise.
- (BOOL)fetch:(NSError **)error;
#pragma mark Synchronization Status
#ifdef FLEXKEYCHAIN_SYNCHRONIZATION_AVAILABLE
/// Returns a boolean indicating if keychain synchronization is available on the device at runtime.
/// The #define FLEXKEYCHAIN_SYNCHRONIZATION_AVAILABLE is only for compile time.
/// If you are checking for the presence of synchronization, you should use this method.
///
/// @return A value indicating if keychain synchronization is available
+ (BOOL)isSynchronizationAvailable;
#endif
@end
@@ -0,0 +1,304 @@
//
// FLEXKeychainQuery.m
// FLEXKeychain
//
// Created by Caleb Davenport on 3/19/13.
// Copyright (c) 2013-2014 Sam Soffes. All rights reserved.
//
#import "FLEXKeychainQuery.h"
#import "FLEXKeychain.h"
@implementation FLEXKeychainQuery
#pragma mark - Public
- (BOOL)save:(NSError *__autoreleasing *)error {
OSStatus status = FLEXKeychainErrorBadArguments;
if (!self.service || !self.account || !self.passwordData) {
if (error) {
*error = [self errorWithCode:status];
}
return NO;
}
NSMutableDictionary *query = nil;
NSMutableDictionary * searchQuery = [self query];
status = SecItemCopyMatching((__bridge CFDictionaryRef)searchQuery, nil);
if (status == errSecSuccess) {//item already exists, update it!
query = [[NSMutableDictionary alloc]init];
query[(__bridge id)kSecValueData] = self.passwordData;
#if __IPHONE_4_0 && TARGET_OS_IPHONE
CFTypeRef accessibilityType = [FLEXKeychain accessibilityType];
if (accessibilityType) {
query[(__bridge id)kSecAttrAccessible] = (__bridge id)accessibilityType;
}
#endif
status = SecItemUpdate((__bridge CFDictionaryRef)(searchQuery), (__bridge CFDictionaryRef)(query));
}else if (status == errSecItemNotFound){//item not found, create it!
query = [self query];
if (self.label) {
query[(__bridge id)kSecAttrLabel] = self.label;
}
query[(__bridge id)kSecValueData] = self.passwordData;
#if __IPHONE_4_0 && TARGET_OS_IPHONE
CFTypeRef accessibilityType = [FLEXKeychain accessibilityType];
if (accessibilityType) {
query[(__bridge id)kSecAttrAccessible] = (__bridge id)accessibilityType;
}
#endif
status = SecItemAdd((__bridge CFDictionaryRef)query, NULL);
}
if (status != errSecSuccess && error != NULL) {
*error = [self errorWithCode:status];
}
return (status == errSecSuccess);
}
- (BOOL)deleteItem:(NSError *__autoreleasing *)error {
OSStatus status = FLEXKeychainErrorBadArguments;
if (!self.service || !self.account) {
if (error) {
*error = [self errorWithCode:status];
}
return NO;
}
NSMutableDictionary *query = [self query];
#if TARGET_OS_IPHONE
status = SecItemDelete((__bridge CFDictionaryRef)query);
#else
// On Mac OS, SecItemDelete will not delete a key created in a different
// app, nor in a different version of the same app.
//
// To replicate the issue, save a password, change to the code and
// rebuild the app, and then attempt to delete that password.
//
// This was true in OS X 10.6 and probably later versions as well.
//
// Work around it by using SecItemCopyMatching and SecKeychainItemDelete.
CFTypeRef result = NULL;
query[(__bridge id)kSecReturnRef] = @YES;
status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
if (status == errSecSuccess) {
status = SecKeychainItemDelete((SecKeychainItemRef)result);
CFRelease(result);
}
#endif
if (status != errSecSuccess && error != NULL) {
*error = [self errorWithCode:status];
}
return (status == errSecSuccess);
}
- (NSArray *)fetchAll:(NSError *__autoreleasing *)error {
NSMutableDictionary *query = [self query];
query[(__bridge id)kSecReturnAttributes] = @YES;
query[(__bridge id)kSecMatchLimit] = (__bridge id)kSecMatchLimitAll;
#if __IPHONE_4_0 && TARGET_OS_IPHONE
CFTypeRef accessibilityType = [FLEXKeychain accessibilityType];
if (accessibilityType) {
query[(__bridge id)kSecAttrAccessible] = (__bridge id)accessibilityType;
}
#endif
CFTypeRef result = NULL;
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
if (status != errSecSuccess && error != NULL) {
*error = [self errorWithCode:status];
return nil;
}
return (__bridge_transfer NSArray *)result;
}
- (BOOL)fetch:(NSError *__autoreleasing *)error {
OSStatus status = FLEXKeychainErrorBadArguments;
if (!self.service || !self.account) {
if (error) {
*error = [self errorWithCode:status];
}
return NO;
}
CFTypeRef result = NULL;
NSMutableDictionary *query = [self query];
query[(__bridge id)kSecReturnData] = @YES;
query[(__bridge id)kSecMatchLimit] = (__bridge id)kSecMatchLimitOne;
status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
if (status != errSecSuccess) {
if (error) {
*error = [self errorWithCode:status];
}
return NO;
}
self.passwordData = (__bridge_transfer NSData *)result;
return YES;
}
#pragma mark - Accessors
- (void)setPasswordObject:(id<NSCoding>)object {
self.passwordData = [NSKeyedArchiver archivedDataWithRootObject:object];
}
- (id<NSCoding>)passwordObject {
if (self.passwordData.length) {
return [NSKeyedUnarchiver unarchiveObjectWithData:self.passwordData];
}
return nil;
}
- (void)setPassword:(NSString *)password {
self.passwordData = [password dataUsingEncoding:NSUTF8StringEncoding];
}
- (NSString *)password {
if (self.passwordData.length) {
return [NSString stringWithCString:self.passwordData.bytes encoding:NSUTF8StringEncoding];
}
return nil;
}
#pragma mark - Synchronization Status
#ifdef FLEXKEYCHAIN_SYNCHRONIZATION_AVAILABLE
+ (BOOL)isSynchronizationAvailable {
#if TARGET_OS_IPHONE
return YES;
#else
return floor(NSFoundationVersionNumber) > NSFoundationVersionNumber10_8_4;
#endif
}
#endif
#pragma mark - Private
- (NSMutableDictionary *)query {
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
dictionary[(__bridge id)kSecClass] = (__bridge id)kSecClassGenericPassword;
if (self.service) {
dictionary[(__bridge id)kSecAttrService] = self.service;
}
if (self.account) {
dictionary[(__bridge id)kSecAttrAccount] = self.account;
}
#ifdef FLEXKEYCHAIN_ACCESS_GROUP_AVAILABLE
#if !TARGET_IPHONE_SIMULATOR
if (self.accessGroup) {
dictionary[(__bridge id)kSecAttrAccessGroup] = self.accessGroup;
}
#endif
#endif
#ifdef FLEXKEYCHAIN_SYNCHRONIZATION_AVAILABLE
if ([[self class] isSynchronizationAvailable]) {
id value;
switch (self.synchronizationMode) {
case FLEXKeychainQuerySynchronizationModeNo: {
value = @NO;
break;
}
case FLEXKeychainQuerySynchronizationModeYes: {
value = @YES;
break;
}
case FLEXKeychainQuerySynchronizationModeAny: {
value = (__bridge id)(kSecAttrSynchronizableAny);
break;
}
}
dictionary[(__bridge id)(kSecAttrSynchronizable)] = value;
}
#endif
return dictionary;
}
- (NSError *)errorWithCode:(OSStatus)code {
static dispatch_once_t onceToken;
static NSBundle *resourcesBundle = nil;
dispatch_once(&onceToken, ^{
NSURL *url = [[NSBundle bundleForClass:[self class]] URLForResource:@"FLEXKeychain" withExtension:@"bundle"];
resourcesBundle = [NSBundle bundleWithURL:url];
});
NSString *message = nil;
switch (code) {
case errSecSuccess: return nil;
case FLEXKeychainErrorBadArguments: message = NSLocalizedStringFromTableInBundle(@"FLEXKeychainErrorBadArguments", @"FLEXKeychain", resourcesBundle, nil); break;
#if TARGET_OS_IPHONE
case errSecUnimplemented: {
message = NSLocalizedStringFromTableInBundle(@"errSecUnimplemented", @"FLEXKeychain", resourcesBundle, nil);
break;
}
case errSecParam: {
message = NSLocalizedStringFromTableInBundle(@"errSecParam", @"FLEXKeychain", resourcesBundle, nil);
break;
}
case errSecAllocate: {
message = NSLocalizedStringFromTableInBundle(@"errSecAllocate", @"FLEXKeychain", resourcesBundle, nil);
break;
}
case errSecNotAvailable: {
message = NSLocalizedStringFromTableInBundle(@"errSecNotAvailable", @"FLEXKeychain", resourcesBundle, nil);
break;
}
case errSecDuplicateItem: {
message = NSLocalizedStringFromTableInBundle(@"errSecDuplicateItem", @"FLEXKeychain", resourcesBundle, nil);
break;
}
case errSecItemNotFound: {
message = NSLocalizedStringFromTableInBundle(@"errSecItemNotFound", @"FLEXKeychain", resourcesBundle, nil);
break;
}
case errSecInteractionNotAllowed: {
message = NSLocalizedStringFromTableInBundle(@"errSecInteractionNotAllowed", @"FLEXKeychain", resourcesBundle, nil);
break;
}
case errSecDecode: {
message = NSLocalizedStringFromTableInBundle(@"errSecDecode", @"FLEXKeychain", resourcesBundle, nil);
break;
}
case errSecAuthFailed: {
message = NSLocalizedStringFromTableInBundle(@"errSecAuthFailed", @"FLEXKeychain", resourcesBundle, nil);
break;
}
default: {
message = NSLocalizedStringFromTableInBundle(@"errSecDefault", @"FLEXKeychain", resourcesBundle, nil);
}
#else
default:
message = (__bridge_transfer NSString *)SecCopyErrorMessageString(code, NULL);
#endif
}
NSDictionary *userInfo = message ? @{ NSLocalizedDescriptionKey : message } : nil;
return [NSError errorWithDomain:kFLEXKeychainErrorDomain code:code userInfo:userInfo];
}
@end
@@ -0,0 +1,14 @@
//
// FLEXKeychainTableViewController.h
// FLEX
//
// Created by ray on 2019/8/17.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import "FLEXGlobalsEntry.h"
#import "FLEXTableViewController.h"
@interface FLEXKeychainTableViewController : FLEXTableViewController <FLEXGlobalsEntry>
@end
@@ -0,0 +1,213 @@
//
// FLEXKeychainTableViewController.m
// FLEX
//
// Created by ray on 2019/8/17.
// Copyright © 2019 Flipboard. All rights reserved.
//
#import "FLEXKeychain.h"
#import "FLEXKeychainQuery.h"
#import "FLEXKeychainTableViewController.h"
#import "FLEXUtility.h"
#import "UIPasteboard+FLEX.h"
@interface FLEXKeychainTableViewController ()
@property (nonatomic) NSMutableArray<NSDictionary *> *keychainItems;
@property (nonatomic) NSString *headerTitle;
@end
@implementation FLEXKeychainTableViewController
- (void)viewDidLoad
{
[super viewDidLoad];
self.navigationItem.rightBarButtonItems = @[
[[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemTrash target:self action:@selector(trashPressed)
],
[[UIBarButtonItem alloc]
initWithBarButtonSystemItem:UIBarButtonSystemItemAdd target:self action:@selector(addPressed)
],
];
[self refreshkeychainItems];
[self updateHeaderTitle];
}
- (void)refreshkeychainItems
{
self.keychainItems = [FLEXKeychain allAccounts].mutableCopy;
}
- (void)updateHeaderTitle
{
self.headerTitle = [NSString stringWithFormat:@"%@ items", @(self.keychainItems.count)];
}
- (FLEXKeychainQuery *)queryForItemAtIndex:(NSInteger)idx
{
NSDictionary *item = self.keychainItems[idx];
FLEXKeychainQuery *query = [FLEXKeychainQuery new];
query.service = item[kFLEXKeychainWhereKey];
query.account = item[kFLEXKeychainAccountKey];
[query fetch:nil];
return query;
}
- (void)deleteItem:(NSDictionary *)item
{
NSError *error = nil;
BOOL success = [FLEXKeychain
deletePasswordForService:item[kFLEXKeychainWhereKey]
account:item[kFLEXKeychainAccountKey]
error:&error
];
if (!success) {
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(@"Error Deleting Item");
make.message(error.localizedDescription);
} showFrom:self];
}
}
#pragma mark Buttons
- (void)trashPressed
{
[FLEXAlert makeSheet:^(FLEXAlert *make) {
make.title(@"Clear Keychain");
make.message(@"This will remove all keychain items for this app.\n");
make.message(@"This action cannot be undone. Are you sure?");
make.button(@"Yes, clear the keychain").destructiveStyle().handler(^(NSArray *strings) {
for (id account in self.keychainItems) {
[self deleteItem:account];
}
[self refreshkeychainItems];
[self.tableView reloadData];
});
make.button(@"Cancel").cancelStyle();
} showFrom:self];
}
- (void)addPressed
{
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(@"Add Keychain Item");
make.textField(@"Service name, i.e. Instagram");
make.textField(@"Account, i.e. username@example.com");
make.textField(@"Password");
make.button(@"Cancel").cancelStyle();
make.button(@"Save").handler(^(NSArray<NSString *> *strings) {
// Display errors
NSError *error = nil;
if (![FLEXKeychain setPassword:strings[2] forService:strings[0] account:strings[1] error:&error]) {
[FLEXAlert showAlert:@"Error" message:error.localizedDescription from:self];
}
[self refreshkeychainItems];
[self.tableView reloadData];
});
} showFrom:self];
}
#pragma mark - FLEXGlobalsEntry
+ (NSString *)globalsEntryTitle:(FLEXGlobalsRow)row
{
return @"🔑 Keychain";
}
+ (UIViewController *)globalsEntryViewController:(FLEXGlobalsRow)row {
FLEXKeychainTableViewController *viewController = [self new];
viewController.title = [self globalsEntryTitle:row];
return viewController;
}
#pragma mark - Table View Data Source
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return self.keychainItems.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];
}
NSDictionary *item = self.keychainItems[indexPath.row];
id account = item[kFLEXKeychainAccountKey];
if ([account isKindOfClass:[NSString class]]) {
cell.textLabel.text = account;
} else {
cell.textLabel.text = [NSString stringWithFormat:
@"[%@]\n\n%@",
NSStringFromClass([account class]),
[account description]
];
}
return cell;
}
- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section
{
return self.headerTitle;
}
- (void)tableView:(UITableView *)tv commitEditingStyle:(UITableViewCellEditingStyle)style forRowAtIndexPath:(NSIndexPath *)ip
{
if (style == UITableViewCellEditingStyleDelete) {
[self deleteItem:self.keychainItems[ip.row]];
[self.keychainItems removeObjectAtIndex:ip.row];
[tv deleteRowsAtIndexPaths:@[ip] withRowAnimation:UITableViewRowAnimationAutomatic];
}
}
#pragma mark - Table View Delegate
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
FLEXKeychainQuery *query = [self queryForItemAtIndex:indexPath.row];
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(query.service);
make.message(@"Service: ").message(query.service);
make.message(@"\nAccount: ").message(query.account);
make.message(@"\nPassword: ").message(query.password);
make.button(@"Copy Service").handler(^(NSArray<NSString *> *strings) {
[UIPasteboard.generalPasteboard flex_copy:query.service];
});
make.button(@"Copy Account").handler(^(NSArray<NSString *> *strings) {
[UIPasteboard.generalPasteboard flex_copy:query.account];
});
make.button(@"Copy Password").handler(^(NSArray<NSString *> *strings) {
[UIPasteboard.generalPasteboard flex_copy:query.password];
});
make.button(@"Dismiss").cancelStyle();
} showFrom:self];
[tableView deselectRowAtIndexPath:indexPath animated:YES];
}
@end
@@ -0,0 +1,20 @@
Copyright (c) 2010-2012 Sam Soffes.
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,207 @@
//
// Taken from https://github.com/llvm-mirror/lldb/blob/master/tools/debugserver/source/MacOSX/DarwinLog/ActivityStreamSPI.h
// by Tanner Bennett on 03/03/2019 with minimal modifications.
//
//===-- ActivityStreamAPI.h -------------------------------------*- C++ -*-===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
#ifndef ActivityStreamSPI_h
#define ActivityStreamSPI_h
#include <sys/time.h>
// #include <xpc/xpc.h>
/* By default, XPC objects are declared as Objective-C types when building with
* an Objective-C compiler. This allows them to participate in ARC, in RR
* management by the Blocks runtime and in leaks checking by the static
* analyzer, and enables them to be added to Cocoa collections.
*
* See <os/object.h> for details.
*/
#if !TARGET_OS_MACCATALYST
#if OS_OBJECT_USE_OBJC
OS_OBJECT_DECL(xpc_object);
#else
typedef void * xpc_object_t;
#endif
#endif
#define OS_ACTIVITY_MAX_CALLSTACK 32
// Enums
typedef NS_ENUM(uint32_t, os_activity_stream_flag_t) {
OS_ACTIVITY_STREAM_PROCESS_ONLY = 0x00000001,
OS_ACTIVITY_STREAM_SKIP_DECODE = 0x00000002,
OS_ACTIVITY_STREAM_PAYLOAD = 0x00000004,
OS_ACTIVITY_STREAM_HISTORICAL = 0x00000008,
OS_ACTIVITY_STREAM_CALLSTACK = 0x00000010,
OS_ACTIVITY_STREAM_DEBUG = 0x00000020,
OS_ACTIVITY_STREAM_BUFFERED = 0x00000040,
OS_ACTIVITY_STREAM_NO_SENSITIVE = 0x00000080,
OS_ACTIVITY_STREAM_INFO = 0x00000100,
OS_ACTIVITY_STREAM_PROMISCUOUS = 0x00000200,
OS_ACTIVITY_STREAM_PRECISE_TIMESTAMPS = 0x00000200
};
typedef NS_ENUM(uint32_t, os_activity_stream_type_t) {
OS_ACTIVITY_STREAM_TYPE_ACTIVITY_CREATE = 0x0201,
OS_ACTIVITY_STREAM_TYPE_ACTIVITY_TRANSITION = 0x0202,
OS_ACTIVITY_STREAM_TYPE_ACTIVITY_USERACTION = 0x0203,
OS_ACTIVITY_STREAM_TYPE_TRACE_MESSAGE = 0x0300,
OS_ACTIVITY_STREAM_TYPE_LOG_MESSAGE = 0x0400,
OS_ACTIVITY_STREAM_TYPE_LEGACY_LOG_MESSAGE = 0x0480,
OS_ACTIVITY_STREAM_TYPE_SIGNPOST_BEGIN = 0x0601,
OS_ACTIVITY_STREAM_TYPE_SIGNPOST_END = 0x0602,
OS_ACTIVITY_STREAM_TYPE_SIGNPOST_EVENT = 0x0603,
OS_ACTIVITY_STREAM_TYPE_STATEDUMP_EVENT = 0x0A00,
};
typedef NS_ENUM(uint32_t, os_activity_stream_event_t) {
OS_ACTIVITY_STREAM_EVENT_STARTED = 1,
OS_ACTIVITY_STREAM_EVENT_STOPPED = 2,
OS_ACTIVITY_STREAM_EVENT_FAILED = 3,
OS_ACTIVITY_STREAM_EVENT_CHUNK_STARTED = 4,
OS_ACTIVITY_STREAM_EVENT_CHUNK_FINISHED = 5,
};
// Types
typedef uint64_t os_activity_id_t;
typedef struct os_activity_stream_s *os_activity_stream_t;
typedef struct os_activity_stream_entry_s *os_activity_stream_entry_t;
#define OS_ACTIVITY_STREAM_COMMON() \
uint64_t trace_id; \
uint64_t timestamp; \
uint64_t thread; \
const uint8_t *image_uuid; \
const char *image_path; \
struct timeval tv_gmt; \
struct timezone tz; \
uint32_t offset
typedef struct os_activity_stream_common_s {
OS_ACTIVITY_STREAM_COMMON();
} * os_activity_stream_common_t;
struct os_activity_create_s {
OS_ACTIVITY_STREAM_COMMON();
const char *name;
os_activity_id_t creator_aid;
uint64_t unique_pid;
};
struct os_activity_transition_s {
OS_ACTIVITY_STREAM_COMMON();
os_activity_id_t transition_id;
};
typedef struct os_log_message_s {
OS_ACTIVITY_STREAM_COMMON();
const char *format;
const uint8_t *buffer;
size_t buffer_sz;
const uint8_t *privdata;
size_t privdata_sz;
const char *subsystem;
const char *category;
uint32_t oversize_id;
uint8_t ttl;
bool persisted;
} * os_log_message_t;
typedef struct os_trace_message_v2_s {
OS_ACTIVITY_STREAM_COMMON();
const char *format;
const void *buffer;
size_t bufferLen;
xpc_object_t __unsafe_unretained payload;
} * os_trace_message_v2_t;
typedef struct os_activity_useraction_s {
OS_ACTIVITY_STREAM_COMMON();
const char *action;
bool persisted;
} * os_activity_useraction_t;
typedef struct os_signpost_s {
OS_ACTIVITY_STREAM_COMMON();
const char *format;
const uint8_t *buffer;
size_t buffer_sz;
const uint8_t *privdata;
size_t privdata_sz;
const char *subsystem;
const char *category;
uint64_t duration_nsec;
uint32_t callstack_depth;
uint64_t callstack[OS_ACTIVITY_MAX_CALLSTACK];
} * os_signpost_t;
typedef struct os_activity_statedump_s {
OS_ACTIVITY_STREAM_COMMON();
char *message;
size_t message_size;
char image_path_buffer[PATH_MAX];
} * os_activity_statedump_t;
struct os_activity_stream_entry_s {
os_activity_stream_type_t type;
// information about the process streaming the data
pid_t pid;
uint64_t proc_id;
const uint8_t *proc_imageuuid;
const char *proc_imagepath;
// the activity associated with this streamed event
os_activity_id_t activity_id;
os_activity_id_t parent_id;
union {
struct os_activity_stream_common_s common;
struct os_activity_create_s activity_create;
struct os_activity_transition_s activity_transition;
struct os_log_message_s log_message;
struct os_trace_message_v2_s trace_message;
struct os_activity_useraction_s useraction;
struct os_signpost_s signpost;
struct os_activity_statedump_s statedump;
};
};
// Blocks
typedef bool (^os_activity_stream_block_t)(os_activity_stream_entry_t entry,
int error);
typedef void (^os_activity_stream_event_block_t)(
os_activity_stream_t stream, os_activity_stream_event_t event);
// SPI entry point prototypes
typedef os_activity_stream_t (*os_activity_stream_for_pid_t)(
pid_t pid, os_activity_stream_flag_t flags,
os_activity_stream_block_t stream_block);
typedef void (*os_activity_stream_resume_t)(os_activity_stream_t stream);
typedef void (*os_activity_stream_cancel_t)(os_activity_stream_t stream);
typedef char *(*os_log_copy_formatted_message_t)(os_log_message_t log_message);
typedef void (*os_activity_stream_set_event_handler_t)(
os_activity_stream_t stream, os_activity_stream_event_block_t block);
#endif /* ActivityStreamSPI_h */

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