Compare commits

..

193 Commits

Author SHA1 Message Date
Tanner Bennett ac50a6d36b Bump version 2022-04-27 13:45:41 -05:00
Tanner Bennett af40ce0909 Fix annoying divider color in editor screens 2022-04-27 13:43:40 -05:00
matrush fb2a33876e Add FLEXTableRowDataViewController for viewing DB rows 2022-04-24 16:30:43 -07:00
Tanner Bennett a42efe17a1 Use host instead of self here 2022-04-24 16:30:43 -07:00
Tanner Bennett 4bc2d1c7a9 Shorten some line lengths 2022-04-24 14:48:47 -07:00
weiminghuaa c7ebecfcb3 Re-filter system log when new messages arrive 2022-04-24 14:48:47 -07:00
weiminghuaa 208f0a31e4 Pin seach bar in system log 2022-04-24 14:48:47 -07:00
Tanner Bennett 2aeb34a67e Cherry pick #592 2022-04-24 16:23:15 -05:00
Tanner Bennett 39f16bd039 Make properties atomic in hopes of resolving #574 2022-04-24 16:23:00 -05:00
Tanner Bennett eea681d6c5 Consolidate duplicate code 2022-04-24 16:22:08 -05:00
Tanner Bennett a8768da4b9 Fix crash where FIRDocumentSnapshot response is nil 2022-04-24 16:15:34 -05:00
Tanner Bennett 4f6fd29d38 Clean up FLEXMITMDataSource interface 2022-04-23 18:45:24 -05:00
Canius Chu ba9cb43b6a Fix compile errors and warnings (#597)
* Update INSTALL_PATH
* Update run path
* Add FLEXFirebaseTransaction.mm into project which break in commit: d2e0db8773
* Fix header imports
* Rollback LD_RUNPATH_SEARCH_PATHS
2022-04-23 18:12:17 -05:00
Tanner Bennett 3af6da8554 Format _manuallyDeactivateSearchOnDisappear 2022-04-23 18:12:17 -05:00
Chaoshuai Lü 6c21395ac8 Fix self.allTransactions race condition in FLEXMITMDataSource
There is a crash caused by this because of race condition. Here we can simply make a copy of the transaction and it will be used and released properly even within the block.
2022-03-30 20:55:19 -07:00
Chaoshuai Lü 456fda31cd Add fallback column query support when PRAGMA table_info is not working (#573)
* Add fallback column queries support when PRAGMA table_info is not working

* Switch to check columnCount instead of rowsAffected

* Use SELECT * FROM pragma_table_info('%@')

Fallback to SELECT * FROM table where 0=1

Co-authored-by: Tanner Bennett <tannerbennett@me.com>
2022-03-30 20:23:55 -07:00
Chaoshuai Lü 9fc8735925 Check rowIDs instead of rowIDs.count to validate input 2022-03-30 23:16:07 -04:00
Tanner Bennett 9683acbe4f Ignore workspace files 2022-03-28 20:55:31 -07:00
Tanner Bennett 86a1cc9324 Remove DEVELOPMENT_TEAM 2022-03-28 20:55:15 -07:00
Tanner Bennett 88b14b3a92 Escape text for webview on a background thread 2022-03-28 20:54:34 -07:00
Tanner Bennett b9cd2682a7 Clean up web vc file 2022-03-28 20:43:48 -07:00
Nick Holub 64d1534fae Add accessibilityIdentifier and accessibilityLabel to views properties 2022-03-22 15:53:57 -07:00
Chaoshuai Lü 8318902826 Fix -Wobjc-signed-char-bool-implicit-int-conversion issue in FLEXNavigationController
This should be checked explicitly to shut up the warning.
2022-03-08 03:16:34 -08:00
Chaoshuai Lu a7f41fe5fc Mark some firebase related functions as static to avoid warnings 2022-03-03 03:14:38 -06:00
Tanner Bennett 2983649cdb Show descriptions of complex FIRQuery objects 2021-12-25 03:27:58 -06:00
Tanner Bennett d2e0db8773 Move FLEXFirebaseTransaction to own file 2021-12-24 22:03:55 -06:00
Tanner Bennett fd98070166 Fix #499, thank you @zhaogyrain 2021-12-24 19:41:52 -06:00
Hossam Ghareeb 4a9fd00eda Add option to copy row as CSV
This will make it easier to insert a new row
2021-12-24 19:35:53 -06:00
Tanner Bennett 97205fc808 Fix bug in rowid selection
`SELECT rowid` returns rowids corresponding to rows in a different order than `SELECT *`. We will now order the rowids in ascending order to match the order of `SELECT *`.

Previously, the mismatched order would mean deleting a row in the UI might delete a different row.
2021-12-24 19:32:01 -06:00
Tanner Bennett 0cffe72a8f Add more assertions to FLEXTableContentVC 2021-12-24 19:32:01 -06:00
Tanner Bennett d5ab2ee916 Revise copy for FLEXTableContentVC add row screen 2021-12-24 19:32:01 -06:00
Tanner Bennett 90c9a48694 Only allow deleting rows if we have a table name 2021-12-24 19:32:01 -06:00
Tanner Bennett 0222c682a0 Improve FLEXTableContentVC toolbar button logic
Only allow adding rows or deleting rows if we have a table name
2021-12-24 19:32:01 -06:00
Tanner Bennett 1e6f6ee110 Clean up FLEXTableContentVC initializers 2021-12-24 19:32:01 -06:00
Hossam Ghareeb 00d6b348f0 Adding an option to add a row in DB table
Added a new toolbar button to insert a new row. Tapping on the button will show an alert to write the values as a comma separated string which will be added in an INSERT statement to be executed.
2021-12-24 19:32:01 -06:00
Tanner Bennett 5404f37d0f Remember the last selected network observer tab 2021-12-24 19:30:18 -06:00
Tanner Bennett 1e539c7129 Pin network history search bar
This works around an ugly visual glitch that happens when the segmented control is visible
2021-12-24 19:30:18 -06:00
Tanner Bennett ca1b202949 For now, just show the transaction object directly 2021-12-24 19:30:18 -06:00
Tanner Bennett 62220a9a65 Record Firebase document creation 2021-12-24 19:30:18 -06:00
Tanner Bennett 02730c6c86 Allow clearing only filtered requests 2021-12-24 19:30:18 -06:00
Tanner Bennett 68789fbe1d Allow filtering Firebase requests with push/pull 2021-12-24 19:30:18 -06:00
Tanner Bennett b4261f8647 Add description for Firebase transaction "push" type 2021-12-24 19:30:18 -06:00
Tanner Bennett 94bf4eac2a Record "push" Firebase network transaction types 2021-12-23 01:35:17 -06:00
Tanner Bennett bed52392e5 Add new Firebase network transaction types 2021-12-23 01:35:17 -06:00
Tanner Bennett c4891840bd Wrap some method names 2021-12-23 01:35:17 -06:00
Tanner Bennett 9720227ac7 Fix logic in network observer mode property 2021-12-23 01:35:17 -06:00
Tanner Bennett 74622aaf10 Initial work for Firebase 2021-12-23 01:35:17 -06:00
Tanner Bennett 0cb5ad8453 Restore -Wno-deprecated-declarations 2021-12-23 01:35:09 -06:00
Chaoshuai Lu def68eae48 Remove unneeded edgesForExtendedLayout setting
Update FLEXTableContentViewController.m

Remove empty lines/spaces changes
2021-12-05 15:29:30 -08:00
Tanner Bennett ffa658c49b Make a bunch of stuff private that should be private 2021-11-18 18:42:04 -06:00
Tanner Bennett 6066de480f FLEXMacros should be public 2021-11-18 18:20:59 -06:00
Tanner Bennett 0fd7dfa002 SPM: clear Headers folder when running script 2021-11-18 17:50:44 -06:00
Tanner Bennett 60403e614d Ignore Classes/Headers in VS Code search 2021-11-18 17:49:03 -06:00
skytoup fa7db997bd Fix realm database viewer crash 2021-11-16 17:34:11 -08:00
Tanner Bennett d15e72c681 Separate example projects for SPM and Cocoapods 2021-11-14 01:01:24 -06:00
Rafael Fernández 4f1ff7784d Add SPM usage in README, close #482 2021-11-14 00:56:24 -06:00
Tanner Bennett 8129a034e3 Clean up Package.swift 2021-11-14 00:56:24 -06:00
Tanner Bennett 838db6954b Add cxx standard setting 2021-11-14 00:56:24 -06:00
Tanner Bennett 5b3d3af99c Update package.swift 2021-11-14 00:56:24 -06:00
Tanner Bennett 2f9f266493 Update import format 2021-11-14 00:56:24 -06:00
Tanner Bennett 9d94979d08 Exclude LICENSE.md 2021-11-14 00:56:24 -06:00
Tanner Bennett 19b83f4404 headerSearchPath 2021-11-14 00:56:24 -06:00
Tanner Bennett 2b13378d98 Remove unsafe flags 😑 2021-11-14 00:56:24 -06:00
Tanner Bennett 4ae9d41104 Add header references 2021-11-14 00:56:24 -06:00
Tanner Bennett 1490170eb4 Add Package.swift, add script to generate headers
Update script
2021-11-14 00:56:24 -06:00
Tanner Bennett cd695ed106 Bump version 2021-11-14 00:53:41 -06:00
Tanner Bennett 69c1719159 Remove redundant/unused methods in arginputfactory 2021-11-14 00:39:44 -06:00
Tanner Bennett 4019518bf5 Fix #564
entry->log_message.format appears to be garbage on iOS 15, and it doesn't look like it is ever really used in practice anyway, as far as I can tell. Thanks @matrush for pointing this out!
2021-11-14 00:39:44 -06:00
Matt Robinson 3fd8e7c77d Fix CLANG_WARN_STRICT_PROTOTYPES/-Wstrict-prototypes issues
This allows `FLEX` to be linked statically to a binary that has `-Wstrict-prototypes` enabled (for example in CocoaPods without `use_frameworks!`.

Changes:
- Apply `void` to all the empty function/block declarations that don't take arguments.
- Apply `SEL`/`UIImage *` to the others that actually take arguments.
- Remove `CLANG_WARN_STRICT_PROTOTYPES = NO` since the default is enabled.
2021-11-14 00:39:44 -06:00
Tanner Bennett 99c3bcb8c5 Remove init exceptions from flex meta classes 2021-11-14 00:39:44 -06:00
Tanner Bennett b587e96e70 Make FLEXMirror protocol 2021-11-14 00:39:43 -06:00
Tanner Bennett ef8f0a303e Add missing nullability to metadata types 2021-11-14 00:36:52 -06:00
Tanner Bennett cfb1e4caab Add NS_SWIFT_NAME to some enums; does not work
Left it commented out. The enums just disappear for some reason?
2021-11-04 19:11:18 -05:00
daniel 2411c331cd Added a dynamic background color to the WebViewControllers response (Depending on being in Light/Dark mode. See this gist for an example: https://gist.githubusercontent.com/dlevi309/e4d48556836b26125e95cbd82d32a9de/raw/d01112d6e0734db11fa74e2fdb0a1330ddcf82a2/dynamicPage.html) 2021-10-27 13:45:33 -05:00
Tanner Bennett fd4b38f46d Fix bug in tab-close logic 2021-10-26 14:43:20 -05:00
Chaoshuai Lu 269e31894c Remove NSParameterAssert check in closeTab method 2021-10-25 18:24:50 -07:00
Tanner Bennett 2f2da50aed Fix potential crash in FLEXPointerIsReadable
vm_read allocates heap memory, this was intended to be an allocation-free check. Some apps may run out of memory in this code path.
2021-10-20 15:21:26 -05:00
Tanner Bennett d87779212c Add data property to FIRDocumentSnapshot at runtime 2021-10-17 17:33:23 -05:00
Tanner Bennett 5db6a12c6e project.pbxcproj formatting? 2021-10-17 17:33:02 -05:00
Tanner Bennett 6d0f776102 Add a FLEXHeapEnumerator test
Ensure we can spoof an object that is found by FLEXHeapEnumerator and also later prove it isn't a real object
2021-10-17 17:32:09 -05:00
Tanner Bennett 6c83ddc2c7 Refactor FLEXHeapEnumerator and FLEXObjectRef
Move logic that was in FLEXLiveObjectsController into FLEXHeapEnumerator. Also adjust FLEXObjectRef initializers to reflect the type of reference you want to hold to the object. FLEXObjectRef now supports unsafe_unretained and retained references.
2021-10-17 17:31:12 -05:00
Tanner Bennett b510d24e13 Improve FLEXPointerIsValidObjcObject
Compare allocation size to expected instance size
2021-10-17 17:28:59 -05:00
Tanner Bennett 6cdf2e61dc Fix websocket activity not being cleared 2021-10-14 00:28:49 -05:00
Tanner Bennett 9b0ed83ff5 Remove old commented out code 2021-10-13 23:41:13 -05:00
Tanner Bennett dbe1b93f48 Add network observer flag to our NSUserDefaults category 2021-10-13 18:45:52 -05:00
Tanner Bennett 06444f1576 Clean up some comments 2021-10-11 20:21:37 -05:00
Tanner Bennett 9bbf1d0d48 Refactor FLEXNetworkTransaction
Add FLEXURLTransaction to differentiate between NSURL-API-related transactions and other transactions, such as lower level protocols or Firebase ;)
2021-10-11 20:11:04 -05:00
Tanner Bennett 0562f15cd0 Fix incoming WS messages not showing correct details 2021-10-11 20:09:58 -05:00
Tanner Bennett eb63c91481 Send example WS message 2021-10-11 20:09:40 -05:00
Tanner Bennett f916174070 Bump version and update URLs 2021-10-06 19:11:01 -05:00
Tanner Bennett 60e23e126e Fix crash for unsupported property type encodings 2021-10-06 19:11:01 -05:00
Tanner Bennett afeff1b562 Add FLEXMITMDataSource, refactor MITMVC 2021-10-06 18:18:54 -05:00
Tanner Bennett 5acb33005b Refactor HTTP transaction detail controller 2021-10-06 18:18:54 -05:00
Tanner Bennett 3446eff353 Hook and record websocket methods 2021-10-06 18:18:54 -05:00
Tanner Bennett ad1f1f579e Move network transaction model logic into model 2021-10-06 18:18:54 -05:00
Tanner Bennett e03b5f7e5d Send example websocket traffic 2021-10-06 18:18:54 -05:00
Tanner Bennett d010c82dd0 Add OSCache for network caching 2021-10-06 18:18:54 -05:00
Tanner Bennett 652d03c39a Add -[NSArray flex_firstWhere:] 2021-10-06 18:18:54 -05:00
Tanner Bennett 1d39669a52 Add button to dlopen a library in Runtime Browser 2021-10-06 18:18:54 -05:00
Iulian Onofrei d558ca6852 Fix code style 2021-09-22 11:49:00 -07:00
Duraid Abdul ad32ca0f05 Update project.pbxproj 2021-08-31 12:04:22 -07:00
Tanner Bennett 67097982ea Bump version 2021-08-19 00:58:13 -05:00
matrush 1342d3029c Add availability check for iOS 11+ only SFSafariViewController properities 2021-08-09 09:37:23 -07:00
Hossam Ghareeb 62dcef4644 Add ability to delete SQLite rows 2021-07-25 18:15:26 -05:00
Tanner Bennett 7ee296143e Add button to clear SQLite table (@hossamghareeb) 2021-07-25 17:31:29 -05:00
Tanner Bennett c6bac54597 Fix #544; utilize smartQuotesType = .no 2021-07-25 17:03:05 -05:00
Tanner Bennett 5f7bce64ed Create FUNDING.yml 2021-07-25 16:52:59 -05:00
Tanner Bennett d2dde55bb1 Lay groundwork for multiple shortcut sections 2021-05-25 17:47:29 -05:00
Tanner Bennett f1a0a5c5e5 Add shortcuts for collections 2021-05-25 17:46:50 -05:00
Tanner Bennett 0db073459e iOS 13 is now the minimum required SDK
Remove FLEX_AT_LEAST_IOS13_SDK and usages. It was causing several issues being in a header that needed to be private but also included in public headers.
2021-05-25 15:30:46 -05:00
Tanner Bennett 7c7ed9286f Fix code typos 2021-05-25 15:10:45 -05:00
Tanner Bennett aac88dd6c8 Fix presentTool:
presentTool
2021-05-07 14:01:57 -05:00
Tanner Bennett d7376b75cd Add flex_observers shortcut to NSNotificationCenter 2021-05-07 13:58:04 -05:00
Tanner Bennett 5b39b3ed03 Add UIPasteboard shortcuts 2021-05-07 13:58:04 -05:00
Tanner Bennett bbaa85bdbf Add WKWebView/SFVC shortcuts 2021-05-07 12:03:06 -05:00
Tanner Bennett 34e27bc5d9 Stable sort and sort integers numerically 2021-05-01 01:04:12 -05:00
Tanner Bennett 714307273e Show full text for DB browser column headers 2021-05-01 01:04:12 -05:00
TekYin 5242d3c5a1 Fix crash when sharing single file on iPad 2021-04-30 23:28:32 -05:00
ray cf2e94a1d2 Fix crash when viewing keychain item 2021-04-30 23:17:01 -05:00
canius 800acb4cad Fix private header warnings and install path (#514)
Co-authored-by: Canius Chu <canius.chu@farfetch.com>
2021-04-30 22:37:39 -05:00
Saafo 368ce64121 Fix iPad/multi-scene FLEX bar layout bugs (#534)
Prior to this, FLEX could appear in the wrong scene or with the wrong width if the scene is not the full width of the screen, which is almost always the case in multi scene environments.

Thanks @Saafo!
2021-04-25 14:54:23 -05:00
Bang Lee 05f03090a9 Display access group for keychain items (#532) 2021-04-12 01:44:29 -05:00
Tanner Bennett a8803781e8 Add missing files to xcodeproj; oops, thanks @BB9z 2021-04-08 16:41:54 -05:00
Tanner Bennett 170f74b297 Don't codesign FLEX.framework 2021-04-08 16:39:39 -05:00
Tanner Bennett 0d0f2a3073 Add FLEXAlert nullabilities for Swift 2021-04-08 16:39:39 -05:00
Michael Gray d6bddf5199 Quote table names in SQL commands (#529)
Sqlite is often smart enough to not need quotes for identifiers, unless you want to use a keyword as a name. Then you need to quote it

https://www.sqlite.org/lang_keywords.html
2021-04-05 15:02:05 -05:00
Tanner Bennett 258ec8697b Bump version 2021-03-30 14:20:02 -05:00
Tanner Bennett 8f9a6e88ec Fix toolbar appearing when empty 2021-03-30 14:20:02 -05:00
Tanner Bennett c37270e6ac Add shortcuts for NSString and NSData 2021-03-30 14:20:02 -05:00
Tanner Bennett ce10d45c29 Fix "hide likely private methods" behavior
Hide relevant private properties too, also class properties and methods
2021-03-30 14:20:02 -05:00
Tanner Bennett b2716c4b2e Catch potential exceptions thrown from descriptions 2021-03-30 14:20:02 -05:00
Tanner Bennett ab135ba94e Remove uninclusive terms
Blacklist → exclude[d] / denylist
Whitelist → allow[ed] / unexcluded
2021-03-30 14:20:02 -05:00
Tanner Bennett bcc04f4113 Make FLEXMacros.h private 2021-03-30 14:20:02 -05:00
Tanner Bennett 470b3fa3b3 Add tintColor as real property 2021-03-30 14:20:02 -05:00
Tanner Bennett e84dfeae5c Fix object explorer class pan gesture… again… 2021-03-30 14:20:02 -05:00
Tanner Bennett 44e86e59b7 Fix Xcode 12 compile errors 2021-03-30 14:20:02 -05:00
Tanner Bennett ac6d9cfa3f Misc refactoring 2021-03-30 14:20:02 -05:00
Tanner Bennett 1a59711760 Fix a crash, add some missing nullabilities 2021-03-30 14:20:01 -05:00
Tanner Bennett b60ce7a057 Add UIAlertController shortcuts
Fix alert controller
2021-03-30 14:20:01 -05:00
Tanner Bennett b4ac210bef Use likely ivar to determine matching property 2021-03-29 13:55:52 -05:00
Tanner Bennett 9360c58975 Add likely ivar data to FLEXProperty 2021-03-29 13:55:52 -05:00
Tanner Bennett 43d9a460ce Make the keyboard seem to appear faster
On screens where the keyboard is set to appear right away, make it appear faster—or at least, make it look like it appears faster…

Also add a property to make the search bar appear initially instead of duplicating becomeFirstResponder code across several classes
2021-03-29 13:55:51 -05:00
ray 15fee5a8a5 Fix keychain viewer crash (#521) 2021-03-23 10:36:29 -05:00
Alexey Salangin fad038392b Don't show toolbar when tapping on a nav bar button (#517) 2021-03-11 13:45:23 -06:00
Tanner Bennett 558d65a0b0 Update license 2021-02-02 14:18:09 -06:00
Tanner Bennett 077fca36c0 Bump deployment targets (oops?) 2021-01-27 23:24:39 -06:00
Tanner Bennett 947769f6f9 Bump version 2021-01-27 17:10:26 -06:00
Tanner Bennett 7c480c5faf Mention tvOS fork in README 2021-01-27 17:09:41 -06:00
Tanner Bennett fa6c72cb08 Fix memory leak 2021-01-27 16:49:10 -06:00
Tanner Bennett 9af2926ec1 Use weakify/strongify throughout project 2021-01-26 12:46:53 -06:00
Tanner Bennett 366a8266bd Add weakify/strongify macros 2021-01-26 12:46:53 -06:00
Tanner Bennett 713fbac54a Move 't' simulator shortcut to the example project
This demonstrates how to register a shortcut from a project with the new pubilc APIs for presenting tools.

I put it in the commit list screen because I didn't feel like rewriting it in Swift.
2021-01-26 12:46:53 -06:00
Tanner Bennett 8198fba689 Add public API to dismiss/present FLEX tools
This API will allow you to present a tool in front of the FLEX toolbar and will automatically dismiss any existing FLEX tool
2021-01-26 12:46:53 -06:00
Tanner Bennett 20e14a36c9 Add -[NSMutableArray flex_filter:] 2021-01-26 11:18:43 -06:00
Tanner Bennett 176e98518d Add context menu item to view references to object 2021-01-20 17:00:37 -06:00
Tanner Bennett 31440056c1 Add option to hide potentially private methods 2021-01-20 17:00:10 -06:00
Tanner Bennett 6c7b39ed03 Fix object explorer swipe gesture precedence 2021-01-20 15:31:25 -06:00
Tanner Bennett 96989f7e0c Refactor FLEXample's AppDelegate.swift 2021-01-20 15:08:06 -06:00
Tanner Bennett 22a23b3b12 Add the Person class back to the example project 2021-01-20 14:59:09 -06:00
Tanner Bennett 90a855a289 Allow registration of global entries with actions
This was previously a private API
2021-01-20 14:37:44 -06:00
matrush fd67373995 Revise object reference section titles logic 2021-01-20 14:37:13 -06:00
Tanner Bennett 3b5e095f74 Add some tests, fix some tests
- Test safeClassNameForObject:
- Test safeObject:respondsToSelector:
- Fix testAssumptionsAboutClasses: which was incorrectly written
2020-12-30 20:06:49 -06:00
Tanner Bennett c7850df186 Add safeClassNameForObject: and use it 2020-12-30 20:06:49 -06:00
Tanner Bennett 6bbcc55cf6 Improve safeObject:respondsToSelector:
The old code assumes the object at least responds to respondsToSelector: which may not always be true. I also I think the old code was not quite correct for all cases anyway.

Using the runtime functions ensures no methods are ever called on the object, and properly handles metaclass objects, in theory.
2020-12-30 20:06:49 -06:00
Tanner Bennett e63ea4bbff Group FLEX objects together in the object refs list 2020-12-30 20:06:49 -06:00
Tanner Bennett 5a760fb1ac Update issue templates 2020-12-30 20:06:49 -06:00
Tanner Bennett e63f2ee3ad Prefix some categories, fix #496 2020-12-30 20:06:49 -06:00
Tanner Bennett 46c6dcb7e6 Fix bug introduced in cb2e0789 2020-12-30 17:31:37 -06:00
Alexey Salangin bf42bbe27b Fix bug with navbar tap gesture
Tap gesture in FLEXNavigationController's navigation bar causes buttons to reportedly become unresponsive on iOS 11 and 12 unless `cancelsTouchesInView` is turned off
2020-12-30 15:27:04 -08:00
Tanner Bennett e89fec4b2d Bump version 2020-12-17 01:57:25 -06:00
Tanner Bennett 715bb92929 Add confirm dialog for clearing the keychain 2020-12-17 01:35:03 -06:00
Tanner Bennett 109074f98e Show service AND account in keychain rows 2020-12-17 01:34:44 -06:00
Tanner Bennett 45fbdb7914 Move keychain buttons to toolbar 2020-12-17 01:34:33 -06:00
Tanner Bennett cb2e0789d8 Fix several memory leaks, fix #483
Also move a bunch of globals related to FLEXShortcutsFactory into ivars of a FLEXShortcutsFactory singleton so that it is easier to trace the origin of the global storage of FLEX[Property|Ivar|Method] objects in a memory graph
2020-12-17 01:17:40 -06:00
vvveiii de1ca783b6 Fix flex_copy: crash when object is nil 2020-12-16 22:30:11 -08:00
vvveiii 3a9c24b784 Add missing else for flex_copy method 2020-12-16 22:30:11 -08:00
Tanner Bennett b57a333fc9 Fix #491, close #473
Remove (most) uses of +[NSString stringWithCString:encoding:]
2020-12-17 00:20:17 -06:00
matrush 288bf1343e Add ability to toggle bg color for image previews
Also fix availability warning
2020-12-15 16:35:27 -08:00
matrush a0b1caed54 Use UITableViewCellAccessoryNone instead of 0 in FLEX[Layer/View]Shortcuts 2020-12-14 18:38:04 -08:00
Tanner Bennett 9282c61183 Allow scrubbing between selected views
Instead of a singular swipe gesture to navigate between views in the view hierarchy at the tap point, we can now pan or drag along that same area to "srub" the hierarchy.
2020-12-14 18:36:44 -06:00
Tanner Bennett ee6677ee08 Add GUI shortcut for initializeWebKitLegacy
We call initializeWebKitLegacy automatically before you search all bundles just to be safe (since touching some classes before WebKit is initialized will initialize it on a thread other than the main thread), but sometimes you can encounter this crash without searching through all bundles, of course.

In this case, you can now long press on the navigation bar to call initializeWebKitLegacy
2020-12-14 17:10:48 -06:00
Tanner Bennett 3276eb3516 Automatically activate search bar in heap explorer
2f952c38 apparently didn't do this, oops
2020-12-14 17:10:48 -06:00
Chaoshuai Lü a3fa7bbadc Fix view/controller preview image issue (#493) 2020-12-14 16:08:58 -06:00
matrush 637074b354 Use id<NSCopying> as key instead of Class<NSCopying> in FLEXObjectExplorerFactory 2020-12-01 16:56:08 -08:00
Tanner Bennett 547bfbaec0 Disallow preview of views with no rect 2020-11-02 20:42:10 -06:00
Tanner Bennett 4e1fcf4682 Bump version, 4.2.2 2020-11-02 19:36:15 -06:00
Tanner Bennett 4d50fd2020 Revert "Use NSString as key …" in d7786449
We NEED to use the class as a key, because you cannot differentiate a class's name from the metaclass's name.
2020-11-02 19:36:15 -06:00
Tanner Bennett 2c510c8ca1 Fix log timer in example app not firing 2020-11-02 17:45:21 -06:00
Tanner Bennett 8283e2a8e7 Fix #415, restore ability to copy log messages 2020-11-02 17:32:29 -06:00
Tanner Bennett 3f82631a95 Fix bug introduced in 4.2.0, unable to edit members
Oopsie
2020-11-02 17:03:37 -06:00
matrush d77864494d Use NSString as key instead of Class<NSCopying> in FLEXObjectExplorerFactory 2020-11-02 16:58:59 -06:00
matrush c5ed6d4ece Add void to function definition of FLEXConstructorsShouldRun 2020-11-02 16:58:59 -06:00
matrush 9412f6eccf Add a clear global entries method for FLEXManager 2020-11-02 16:58:59 -06:00
259 changed files with 6310 additions and 1482 deletions
+3
View File
@@ -0,0 +1,3 @@
# These are supported funding model platforms
github: [NSExceptional]
+27
View File
@@ -0,0 +1,27 @@
---
name: Bug report
about: Report a bug in FLEX
title: ''
labels: bug
assignees: ''
---
### Environment
- Platform+version: **iOS 14** <!--- Change to match your platform and version -->
- FLEX version: **9.9.9** <!--- Change to the version of FLEX you're using -->
<!--- FLEXing / libFLEX users: please include FLEXing and libFLEX versions separately -->
### Bug Report
Here, you can provide a description of the bug. Some tips:
- Please do not paste an entire crash log. Upload the crash log to something like [ghostbin.co](https://ghostbin.co/) or another paste service. Alternatively, you can cut out the relevant stack trace and paste that inside a ` ```code block``` `
- If the bug is more complex than "this button is broken" or a crash, consider including a sample project. For example, if your app's requests aren't showing up in the network history page.
- Providing steps to reproduce is always helpful!
- If you want to include a screenshot or GIF, consider modifying the default markdown for uploaded images to use this code to make the image smaller on desktop:
```
<img width="50%" src=your-image-url >
```
This template is a suggestion. You may format your issue however you want, but generally you should at least include your iOS version and FLEX version.
+10
View File
@@ -0,0 +1,10 @@
---
name: Feature request
about: Suggest a new feature for FLEX
title: ''
labels: enhancement
assignees: ''
---
+1
View File
@@ -20,3 +20,4 @@ DerivedData
/Example/Pods
Podfile.lock
IDEWorkspaceChecks.plist
*.xcworkspace
+5
View File
@@ -0,0 +1,5 @@
{
"search.exclude": {
"Classes/Headers": true
}
}
@@ -9,6 +9,7 @@
#import "FLEXFilteringTableViewController.h"
#import "FLEXTableViewSection.h"
#import "NSArray+FLEX.h"
#import "FLEXMacros.h"
@interface FLEXFilteringTableViewController ()
@@ -73,7 +74,7 @@
#pragma mark - Search
- (void)updateSearchResults:(NSString *)newText {
NSArray *(^filter)() = ^NSArray *{
NSArray *(^filter)(void) = ^NSArray *{
self.filterText = newText;
// Sections will adjust data based on this property
@@ -187,8 +188,6 @@
[self.filterDelegate.sections[indexPath.section] didPressInfoButtonAction:indexPath.row](self);
}
#if FLEX_AT_LEAST_IOS13_SDK
- (UIContextMenuConfiguration *)tableView:(UITableView *)tableView contextMenuConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath point:(CGPoint)point __IOS_AVAILABLE(13.0) {
FLEXTableViewSection *section = self.filterDelegate.sections[indexPath.section];
NSString *title = [section menuTitleForRow:indexPath.row];
@@ -207,6 +206,4 @@
return nil;
}
#endif
@end
@@ -20,6 +20,7 @@
@interface FLEXNavigationController ()
@property (nonatomic, readonly) BOOL toolbarWasHidden;
@property (nonatomic) BOOL waitingToAddTab;
@property (nonatomic, readonly) BOOL canShowToolbar;
@property (nonatomic) BOOL didSetupPendingDismissButtons;
@property (nonatomic) UISwipeGestureRecognizer *navigationBarSwipeGesture;
@end
@@ -36,10 +37,13 @@
self.waitingToAddTab = YES;
// Add gesture to reveal toolbar if hidden
self.navigationBar.userInteractionEnabled = YES;
[self.navigationBar addGestureRecognizer:[[UITapGestureRecognizer alloc]
UITapGestureRecognizer *navbarTapGesture = [[UITapGestureRecognizer alloc]
initWithTarget:self action:@selector(handleNavigationBarTap:)
]];
];
// Don't cancel touches to work around bug on versions of iOS prior to 13
navbarTapGesture.cancelsTouchesInView = NO;
[self.navigationBar addGestureRecognizer:navbarTapGesture];
// Add gesture to dismiss if not presented with a sheet style
if (@available(iOS 13, *)) {
@@ -92,10 +96,17 @@
- (void)dismissAnimated {
// Tabs are only closed if the done button is pressed; this
// allows you to leave a tab open by dragging down to dismiss
[FLEXTabList.sharedList closeTab:self];
if ([self.presentingViewController isKindOfClass:[FLEXExplorerViewController class]]) {
[FLEXTabList.sharedList closeTab:self];
}
[self.presentingViewController dismissViewControllerAnimated:YES completion:nil];
}
- (BOOL)canShowToolbar {
return self.topViewController.toolbarItems.count > 0;
}
- (void)addNavigationBarItemsToViewController:(UINavigationItem *)navigationItem {
if (!self.presentingViewController) {
return;
@@ -145,8 +156,15 @@
}
- (void)handleNavigationBarTap:(UIGestureRecognizer *)sender {
// Don't reveal the toolbar if we were just tapping a button
CGPoint location = [sender locationInView:self.navigationBar];
UIView *hitView = [self.navigationBar hitTest:location withEvent:nil];
if ([hitView isKindOfClass:[UIControl class]]) {
return;
}
if (sender.state == UIGestureRecognizerStateRecognized) {
if (self.toolbarHidden) {
if (self.toolbarHidden && self.canShowToolbar) {
[self setToolbarHidden:NO animated:YES];
}
}
@@ -162,7 +180,7 @@
- (void)_gestureRecognizedInteractiveHide:(UIPanGestureRecognizer *)sender {
if (sender.state == UIGestureRecognizerStateRecognized) {
BOOL show = self.topViewController.toolbarItems.count;
BOOL show = self.canShowToolbar;
CGFloat yTranslation = [sender translationInView:self.view].y;
CGFloat yVelocity = [sender velocityInView:self.view].y;
if (yVelocity > 2000) {
@@ -67,6 +67,10 @@ extern CGFloat const kFLEXDebounceForExpensiveIO;
/// 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;
/// Defaults to NO.
///
/// Setting this to YES will make the search bar activate whenever the view appears.
@property (nonatomic) BOOL activatesSearchBarAutomatically;
/// nil unless showsSearchBar is set to YES.
///
@@ -50,15 +50,12 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
#pragma mark - Initialization
- (id)init {
#if FLEX_AT_LEAST_IOS13_SDK
if (@available(iOS 13.0, *)) {
self = [self initWithStyle:UITableViewStyleInsetGrouped];
} else {
self = [self initWithStyle:UITableViewStyleGrouped];
}
#else
self = [self initWithStyle:UITableViewStyleGrouped];
#endif
return self;
}
@@ -69,9 +66,9 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
_searchBarDebounceInterval = kFLEXDebounceFast;
_showSearchBarInitially = YES;
_style = style;
_manuallyDeactivateSearchOnDisappear = ({
NSProcessInfo.processInfo.operatingSystemVersion.majorVersion < 11;
});
_manuallyDeactivateSearchOnDisappear = (
NSProcessInfo.processInfo.operatingSystemVersion.majorVersion < 11
);
// We will be our own search delegate if we implement this method
if ([self respondsToSelector:@selector(updateSearchResults:)]) {
@@ -106,11 +103,9 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
self.automaticallyShowsSearchBarCancelButton = YES;
#if FLEX_AT_LEAST_IOS13_SDK
if (@available(iOS 13, *)) {
self.searchController.automaticallyShowsScopeBar = NO;
}
#endif
[self addSearchController:self.searchController];
} else {
@@ -124,18 +119,15 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
_showsCarousel = showsCarousel;
if (showsCarousel) {
_carousel = ({
__weak __typeof(self) weakSelf = self;
_carousel = ({ weakify(self)
FLEXScopeCarousel *carousel = [FLEXScopeCarousel new];
carousel.selectedIndexChangedAction = ^(NSInteger idx) {
__typeof(self) self = weakSelf;
carousel.selectedIndexChangedAction = ^(NSInteger idx) { strongify(self);
[self.searchDelegate updateSearchResults:self.searchText];
};
// UITableView won't update the header size unless you reset the header view
[carousel registerBlockForDynamicTypeChanges:^(FLEXScopeCarousel *carousel) {
__typeof(self) self = weakSelf;
[carousel registerBlockForDynamicTypeChanges:^(FLEXScopeCarousel *_) { strongify(self);
[self layoutTableHeaderIfNeeded];
}];
@@ -173,21 +165,17 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
}
- (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;
}
@@ -241,7 +229,7 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
self.tableView.keyboardDismissMode = UIScrollViewKeyboardDismissModeOnDrag;
// Toolbar
self.navigationController.toolbarHidden = NO;
self.navigationController.toolbarHidden = self.toolbarItems.count > 0;
self.navigationController.hidesBarsOnSwipe = YES;
// On iOS 13, the root view controller shows it's search bar no matter what.
@@ -259,12 +247,17 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
- (void)viewWillAppear:(BOOL)animated {
[super viewWillAppear:animated];
// When going back, make the search bar reappear instead of hiding
if (@available(iOS 11.0, *)) {
// When going back, make the search bar reappear instead of hiding
if ((self.pinSearchBar || self.showSearchBarInitially) && !self.didInitiallyRevealSearchBar) {
self.navigationItem.hidesSearchBarWhenScrolling = NO;
}
}
// Make the keyboard seem to appear faster
if (self.activatesSearchBarAutomatically) {
[self makeKeyboardAppearNow];
}
[self setupToolbarItems];
}
@@ -286,6 +279,17 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
}];
}
}
if (self.activatesSearchBarAutomatically) {
// Keyboard has appeared, now we call this as we soon present our search bar
[self removeDummyTextField];
// Activate the search bar
dispatch_async(dispatch_get_main_queue(), ^{
// This doesn't work unless it's wrapped in this dispatch_async call
[self.searchController.searchBar becomeFirstResponder];
});
}
// We only want to reveal the search bar when the view controller first appears.
self.didInitiallyRevealSearchBar = YES;
@@ -525,13 +529,37 @@ CGFloat const kFLEXDebounceForExpensiveIO = 0.5;
#pragma mark - Search Bar
#pragma mark Faster keyboard
static UITextField *kDummyTextField = nil;
/// Make the keyboard appear instantly. We use this to make the
/// keyboard appear faster when the search bar is set to appear initially.
/// You must call \c -removeDummyTextField before your search bar is to appear.
- (void)makeKeyboardAppearNow {
if (!kDummyTextField) {
kDummyTextField = [UITextField new];
kDummyTextField.autocorrectionType = UITextAutocorrectionTypeNo;
}
kDummyTextField.inputAccessoryView = self.searchController.searchBar.inputAccessoryView;
[UIApplication.sharedApplication.keyWindow addSubview:kDummyTextField];
[kDummyTextField becomeFirstResponder];
}
- (void)removeDummyTextField {
if (kDummyTextField.superview) {
[kDummyTextField removeFromSuperview];
}
}
#pragma mark UISearchResultsUpdating
- (void)updateSearchResultsForSearchController:(UISearchController *)searchController {
[self.debounceTimer invalidate];
NSString *text = searchController.searchBar.text;
void (^updateSearchResults)() = ^{
void (^updateSearchResults)(void) = ^{
if (self.searchResultsUpdater) {
[self.searchResultsUpdater updateSearchResults:text];
} else {
-3
View File
@@ -7,7 +7,6 @@
//
#import <UIKit/UIKit.h>
#import "FLEXMacros.h"
#import "NSArray+FLEX.h"
@class FLEXTableView;
@@ -101,7 +100,6 @@ NS_ASSUME_NONNULL_BEGIN
- (nullable void(^)(__kindof UIViewController *host))didPressInfoButtonAction:(NSInteger)row;
#pragma mark - Context Menus
#if FLEX_AT_LEAST_IOS13_SDK
/// By default, this is the title of the row.
/// @return The title of the context menu, if any.
@@ -121,7 +119,6 @@ NS_ASSUME_NONNULL_BEGIN
/// should be a description of what will be copied, and the values should be
/// the strings to copy. Return an empty string as a value to show a disabled action.
- (nullable NSArray<NSString *> *)copyMenuItemsForRow:(NSInteger)row API_AVAILABLE(ios(13.0));
#endif
#pragma mark - Cell Configuration
-4
View File
@@ -64,8 +64,6 @@
return kFLEXDefaultCell;
}
#if FLEX_AT_LEAST_IOS13_SDK
- (NSString *)menuTitleForRow:(NSInteger)row {
NSString *title = [self titleForRow:row];
NSString *subtitle = [self menuSubtitleForRow:row];
@@ -127,8 +125,6 @@
return @[];
}
#endif
- (NSArray<NSString *> *)copyMenuItemsForRow:(NSInteger)row {
return nil;
}
@@ -77,7 +77,7 @@
self.selectionIndicatorStripe.translatesAutoresizingMaskIntoConstraints = NO;
UIView *superview = self.contentView;
[self.titleLabel pinEdgesToSuperviewWithInsets:UIEdgeInsetsMake(10, 15, 8 + stripeHeight, 15)];
[self.titleLabel flex_pinEdgesToSuperviewWithInsets:UIEdgeInsetsMake(10, 15, 8 + stripeHeight, 15)];
[self.selectionIndicatorStripe.leadingAnchor constraintEqualToAnchor:superview.leadingAnchor].active = YES;
[self.selectionIndicatorStripe.bottomAnchor constraintEqualToAnchor:superview.bottomAnchor].active = YES;
@@ -9,6 +9,7 @@
#import "FLEXScopeCarousel.h"
#import "FLEXCarouselCell.h"
#import "FLEXColor.h"
#import "FLEXMacros.h"
#import "UIView+FLEX_Layout.h"
const CGFloat kCarouselItemSpacing = 0;
@@ -72,15 +73,14 @@ NSString * const kCarouselCellReuseIdentifier = @"kCarouselCellReuseIdentifier";
self.sizingCell.title = @"NSObject";
// Dynamic type
__weak __typeof(self) weakSelf = self;
weakify(self);
_dynamicTypeObserver = [NSNotificationCenter.defaultCenter
addObserverForName:UIContentSizeCategoryDidChangeNotification
object:nil queue:nil usingBlock:^(NSNotification *note) {
object:nil queue:nil usingBlock:^(NSNotification *note) { strongify(self)
[self.collectionView setNeedsLayout];
[self setNeedsUpdateConstraints];
// Notify observers
__typeof(self) self = weakSelf;
for (void (^block)(FLEXScopeCarousel *) in self.dynamicTypeHandlers) {
block(self);
}
@@ -118,7 +118,7 @@ NSString * const kCarouselCellReuseIdentifier = @"kCarouselCellReuseIdentifier";
- (void)updateConstraints {
if (!self.constraintsInstalled) {
self.collectionView.translatesAutoresizingMaskIntoConstraints = NO;
[self.collectionView pinEdgesToSuperview];
[self.collectionView flex_pinEdgesToSuperview];
self.constraintsInstalled = YES;
}
-8
View File
@@ -30,29 +30,21 @@ FLEXTableViewCellReuseIdentifier const kFLEXCodeFontCell = @"kFLEXCodeFontCell";
@implementation FLEXTableView
+ (instancetype)flexDefaultTableView {
#if FLEX_AT_LEAST_IOS13_SDK
if (@available(iOS 13.0, *)) {
return [[self alloc] initWithFrame:CGRectZero style:UITableViewStyleInsetGrouped];
} else {
return [[self alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];
}
#else
return [[self alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];
#endif
}
#pragma mark - Initialization
+ (id)groupedTableView {
#if FLEX_AT_LEAST_IOS13_SDK
if (@available(iOS 13.0, *)) {
return [[self alloc] initWithFrame:CGRectZero style:UITableViewStyleInsetGrouped];
} else {
return [[self alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];
}
#else
return [[self alloc] initWithFrame:CGRectZero style:UITableViewStyleGrouped];
#endif
}
+ (id)plainTableView {
@@ -33,6 +33,7 @@
self.inputTextView.delegate = self;
self.inputTextView.inputAccessoryView = [self createToolBar];
if (@available(iOS 11, *)) {
self.inputTextView.smartQuotesType = UITextSmartQuotesTypeNo;
[self.inputTextView.layer setValue:@YES forKey:@"continuousCorners"];
} else {
self.inputTextView.layer.borderWidth = 1.f;
@@ -12,7 +12,7 @@ NS_ASSUME_NONNULL_BEGIN
@interface FLEXDefaultEditorViewController : FLEXVariableEditorViewController
+ (instancetype)target:(NSUserDefaults *)defaults key:(NSString *)key commitHandler:(void(^_Nullable)())onCommit;
+ (instancetype)target:(NSUserDefaults *)defaults key:(NSString *)key commitHandler:(void(^_Nullable)(void))onCommit;
+ (BOOL)canEditDefaultWithValue:(nullable id)currentValue;
@@ -21,7 +21,7 @@
@implementation FLEXDefaultEditorViewController
+ (instancetype)target:(NSUserDefaults *)defaults key:(NSString *)key commitHandler:(void(^_Nullable)())onCommit {
+ (instancetype)target:(NSUserDefaults *)defaults key:(NSString *)key commitHandler:(void(^_Nullable)(void))onCommit {
FLEXDefaultEditorViewController *editor = [self target:defaults data:key commitHandler:onCommit];
editor.title = @"Edit Default";
return editor;
+2 -1
View File
@@ -9,6 +9,7 @@
#import "FLEXFieldEditorView.h"
#import "FLEXArgumentInputView.h"
#import "FLEXUtility.h"
#import "FLEXColor.h"
@interface FLEXFieldEditorView ()
@@ -122,7 +123,7 @@
}
+ (UIColor *)dividerColor {
return UIColor.lightGrayColor;
return FLEXColor.tertiaryBackgroundColor;
}
+ (CGFloat)horizontalPadding {
@@ -15,9 +15,9 @@ NS_ASSUME_NONNULL_BEGIN
@interface FLEXFieldEditorViewController : FLEXVariableEditorViewController
/// @return nil if the property is readonly or if the type is unsupported
+ (nullable instancetype)target:(id)target property:(FLEXProperty *)property commitHandler:(void(^_Nullable)())onCommit;
+ (nullable instancetype)target:(id)target property:(FLEXProperty *)property commitHandler:(void(^_Nullable)(void))onCommit;
/// @return nil if the ivar type is unsupported
+ (nullable instancetype)target:(id)target ivar:(FLEXIvar *)ivar commitHandler:(void(^_Nullable)())onCommit;
+ (nullable instancetype)target:(id)target ivar:(FLEXIvar *)ivar commitHandler:(void(^_Nullable)(void))onCommit;
/// Subclasses can change the button title via the \c title property
@property (nonatomic, readonly) UIBarButtonItem *getterButton;
@@ -30,19 +30,14 @@
#pragma mark - Initialization
+ (instancetype)target:(id)target property:(nonnull FLEXProperty *)property commitHandler:(void(^_Nullable)())onCommit {
id value = [property getValue:target];
if (![self canEditProperty:property onObject:target currentValue:value]) {
return nil;
}
+ (instancetype)target:(id)target property:(nonnull FLEXProperty *)property commitHandler:(void(^_Nullable)(void))onCommit {
FLEXFieldEditorViewController *editor = [self target:target data:property commitHandler:onCommit];
editor.title = [@"Property: " stringByAppendingString:property.name];
editor.property = property;
return editor;
}
+ (instancetype)target:(id)target ivar:(nonnull FLEXIvar *)ivar commitHandler:(void(^_Nullable)())onCommit {
+ (instancetype)target:(id)target ivar:(nonnull FLEXIvar *)ivar commitHandler:(void(^_Nullable)(void))onCommit {
FLEXFieldEditorViewController *editor = [self target:target data:ivar commitHandler:onCommit];
editor.title = [@"Ivar: " stringByAppendingString:ivar.name];
editor.ivar = ivar;
@@ -151,14 +146,4 @@
}
}
+ (BOOL)canEditProperty:(FLEXProperty *)property onObject:(id)object currentValue:(id)value {
const FLEXTypeEncoding *typeEncoding = property.attributes.typeEncoding.UTF8String;
BOOL canEditType = [FLEXArgumentInputViewFactory canEditFieldWithTypeEncoding:typeEncoding currentValue:value];
return canEditType && [object respondsToSelector:property.likelySetter];
}
+ (BOOL)canEditIvar:(Ivar)ivar currentValue:(id)value {
return [FLEXArgumentInputViewFactory canEditFieldWithTypeEncoding:ivar_getTypeEncoding(ivar) currentValue:value];
}
@end
@@ -21,17 +21,17 @@ NS_ASSUME_NONNULL_BEGIN
@protected
id _target;
_Nullable id _data;
void (^_Nullable _commitHandler)();
void (^_Nullable _commitHandler)(void);
}
/// @param target The target of the operation
/// @param data The data associated with the operation
/// @param onCommit An action to perform when the data changes
+ (instancetype)target:(id)target data:(nullable id)data commitHandler:(void(^_Nullable)())onCommit;
+ (instancetype)target:(id)target data:(nullable id)data commitHandler:(void(^_Nullable)(void))onCommit;
/// @param target The target of the operation
/// @param data The data associated with the operation
/// @param onCommit An action to perform when the data changes
- (id)initWithTarget:(id)target data:(nullable id)data commitHandler:(void(^_Nullable)())onCommit;
- (id)initWithTarget:(id)target data:(nullable id)data commitHandler:(void(^_Nullable)(void))onCommit;
@property (nonatomic, readonly) id target;
@@ -25,11 +25,11 @@
#pragma mark - Initialization
+ (instancetype)target:(id)target data:(nullable id)data commitHandler:(void(^_Nullable)())onCommit {
+ (instancetype)target:(id)target data:(nullable id)data commitHandler:(void(^_Nullable)(void))onCommit {
return [[self alloc] initWithTarget:target data:data commitHandler:onCommit];
}
- (id)initWithTarget:(id)target data:(nullable id)data commitHandler:(void(^_Nullable)())onCommit {
- (id)initWithTarget:(id)target data:(nullable id)data commitHandler:(void(^_Nullable)(void))onCommit {
self = [super init];
if (self) {
_target = target;
@@ -21,11 +21,21 @@
- (BOOL)shouldReceiveTouchAtWindowPoint:(CGPoint)pointInWindowCoordinates;
/// @brief Used to present (or dismiss) a modal view controller ("tool"), typically triggered by pressing a button in the toolbar.
/// @brief Used to present (or dismiss) a modal view controller ("tool"),
/// typically triggered by pressing a button in the toolbar.
///
/// If a tool is already presented, this method simply dismisses it and calls the completion block.
/// If no tool is presented, @code future() @endcode is presented and the completion block is called.
- (void)toggleToolWithViewControllerProvider:(UINavigationController *(^)(void))future completion:(void(^)(void))completion;
- (void)toggleToolWithViewControllerProvider:(UINavigationController *(^)(void))future
completion:(void (^)(void))completion;
/// @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 dismisses it and presents the given tool.
/// The completion block is called once the tool has been presented.
- (void)presentTool:(UINavigationController *(^)(void))future
completion:(void (^)(void))completion;
// Keyboard shortcut helpers
@@ -45,6 +45,9 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
/// Only valid while a toolbar drag pan gesture is in progress.
@property (nonatomic) CGRect toolbarFrameBeforeDragging;
/// Only valid while a selected view pan gesture is in progress.
@property (nonatomic) CGFloat selectedViewLastPanX;
/// Borders of all the visible views in the hierarchy at the selection point.
/// The keys are NSValues with the corresponding view (nonretained).
@property (nonatomic) NSDictionary<NSValue *, UIView *> *outlineViewsForVisibleViews;
@@ -58,6 +61,9 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
/// A colored transparent overlay to indicate that the view is selected.
@property (nonatomic) UIView *selectedViewOverlay;
/// Used to actuate changes in view selection on iOS 10+
@property (nonatomic, readonly) UISelectionFeedbackGenerator *selectionFBG API_AVAILABLE(ios(10.0));
/// self.view.window as a \c FLEXWindow
@property (nonatomic, readonly) FLEXWindow *window;
@@ -118,6 +124,11 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
self.movePanGR = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(handleMovePan:)];
self.movePanGR.enabled = self.currentMode == FLEXExplorerModeMove;
[self.view addGestureRecognizer:self.movePanGR];
// Feedback
if (@available(iOS 10.0, *)) {
_selectionFBG = [UISelectionFeedbackGenerator new];
}
}
- (void)viewWillAppear:(BOOL)animated {
@@ -449,17 +460,12 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
initWithTarget:self action:@selector(handleToolbarDetailsTapGesture:)
];
[toolbar.selectedViewDescriptionContainer addGestureRecognizer:self.detailsTapGR];
// Swipe gestures for selecting deeper / higher views at a point
UISwipeGestureRecognizer *leftSwipe = [[UISwipeGestureRecognizer alloc]
UIPanGestureRecognizer *panGesture = [[UIPanGestureRecognizer alloc]
initWithTarget:self action:@selector(handleChangeViewAtPointGesture:)
];
UISwipeGestureRecognizer *rightSwipe = [[UISwipeGestureRecognizer alloc]
initWithTarget:self action:@selector(handleChangeViewAtPointGesture:)
];
leftSwipe.direction = UISwipeGestureRecognizerDirectionLeft;
rightSwipe.direction = UISwipeGestureRecognizerDirectionRight;
[toolbar.selectedViewDescriptionContainer addGestureRecognizer:leftSwipe];
[toolbar.selectedViewDescriptionContainer addGestureRecognizer:rightSwipe];
[toolbar.selectedViewDescriptionContainer addGestureRecognizer:panGesture];
// Long press gesture to present tabs manager
[toolbar.globalsItem addGestureRecognizer:[[UILongPressGestureRecognizer alloc]
@@ -598,19 +604,54 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
}
}
- (void)handleChangeViewAtPointGesture:(UISwipeGestureRecognizer *)sender {
- (void)handleChangeViewAtPointGesture:(UIPanGestureRecognizer *)sender {
NSInteger max = self.viewsAtTapPoint.count - 1;
NSInteger currentIdx = [self.viewsAtTapPoint indexOfObject:self.selectedView];
switch (sender.direction) {
case UISwipeGestureRecognizerDirectionLeft:
self.selectedView = self.viewsAtTapPoint[MIN(max, currentIdx + 1)];
break;
case UISwipeGestureRecognizerDirectionRight:
self.selectedView = self.viewsAtTapPoint[MAX(0, currentIdx - 1)];
CGFloat locationX = [sender locationInView:self.view].x;
// Track the pan gesture: every N points we move along the X axis,
// actuate some haptic feedback and move up or down the hierarchy.
// We only store the "last" location when we've met the threshold.
// We only change the view and actuate feedback if the view selection
// changes; that is, as long as we don't go outside or under the array.
switch (sender.state) {
case UIGestureRecognizerStateBegan: {
self.selectedViewLastPanX = locationX;
break;
}
case UIGestureRecognizerStateChanged: {
static CGFloat kNextLevelThreshold = 20.f;
CGFloat lastX = self.selectedViewLastPanX;
NSInteger newSelection = currentIdx;
// Left, go down the hierarchy
if (locationX < lastX && (lastX - locationX) >= kNextLevelThreshold) {
// Choose a new view index up to the max index
newSelection = MIN(max, currentIdx + 1);
self.selectedViewLastPanX = locationX;
}
// Right, go up the hierarchy
else if (lastX < locationX && (locationX - lastX) >= kNextLevelThreshold) {
// Choose a new view index down to the min index
newSelection = MAX(0, currentIdx - 1);
self.selectedViewLastPanX = locationX;
}
if (currentIdx != newSelection) {
self.selectedView = self.viewsAtTapPoint[newSelection];
[self actuateSelectionChangedFeedback];
}
default:
break;
}
default: break;
}
}
- (void)actuateSelectionChangedFeedback {
if (@available(iOS 10.0, *)) {
[self.selectionFBG selectionChanged];
}
}
@@ -872,20 +913,33 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
[super dismissViewControllerAnimated:animated completion:completion];
}
- (BOOL)wantsWindowToBecomeKey
{
- (BOOL)wantsWindowToBecomeKey {
return self.window.previousKeyWindow != nil;
}
- (void)toggleToolWithViewControllerProvider:(UINavigationController *(^)(void))future
completion:(void(^)(void))completion {
completion:(void (^)(void))completion {
if (self.presentedViewController) {
// We do NOT want to present the future; this is
// a convenience method for toggling the SAME TOOL
[self dismissViewControllerAnimated:YES completion:completion];
} else if (future) {
[self presentViewController:future() animated:YES completion:completion];
}
}
- (void)presentTool:(UINavigationController *(^)(void))future
completion:(void (^)(void))completion {
if (self.presentedViewController) {
// If a tool is already presented, dismiss it first
[self dismissViewControllerAnimated:YES completion:^{
[self presentViewController:future() animated:YES completion:completion];
}];
} else if (future) {
[self presentViewController:future() animated:YES completion:completion];
}
}
- (FLEXWindow *)window {
return (id)self.view.window;
}
@@ -924,11 +978,7 @@ typedef NS_ENUM(NSUInteger, FLEXExplorerMode) {
} else {
return [FLEXHierarchyViewController delegate:self];
}
} completion:^{
if (completion) {
completion();
}
}];
} completion:completion];
}
- (void)toggleMenuTool {
@@ -72,7 +72,7 @@
[self dismissViewControllerAnimated:YES completion:nil];
}
- (void)showRevertOrDismissAlert:(void(^)())revertBlock {
- (void)showRevertOrDismissAlert:(void(^)(void))revertBlock {
[self.tableView deselectRowAtIndexPath:self.tableView.indexPathForSelectedRow animated:YES];
[self reloadData];
[self.tableView reloadData];
+3 -3
View File
@@ -80,10 +80,10 @@
- (void)closeTab:(UINavigationController *)tab {
NSParameterAssert(tab);
NSParameterAssert([self.openTabs containsObject:tab]);
NSInteger idx = [self.openTabs indexOfObject:tab];
[self closeTabAtIndex:idx];
if (idx != NSNotFound) {
[self closeTabAtIndex:idx];
}
}
- (void)closeTabAtIndex:(NSInteger)idx {
+11 -18
View File
@@ -6,22 +6,15 @@
// Copyright © 2020 FLEX Team. All rights reserved.
//
#import "UIBarButtonItem+FLEX.h"
#import "CALayer+FLEX.h"
#import "UIFont+FLEX.h"
#import "UIGestureRecognizer+Blocks.h"
#import "UIPasteboard+FLEX.h"
#import "UIMenu+FLEX.h"
#import "UITextField+Range.h"
#import <FLEX/UIBarButtonItem+FLEX.h>
#import <FLEX/CALayer+FLEX.h>
#import <FLEX/UIFont+FLEX.h>
#import <FLEX/UIGestureRecognizer+Blocks.h>
#import <FLEX/UIView+FLEX_Layout.h>
#import <FLEX/UIPasteboard+FLEX.h>
#import <FLEX/UIMenu+FLEX.h>
#import <FLEX/UITextField+Range.h>
#import <FLEX/NSObject+FLEX_Reflection.h>
#import <FLEX/NSArray+FLEX.h>
#import <FLEX/NSDictionary+ObjcRuntime.h>
#import <FLEX/NSString+ObjcRuntime.h>
#import <FLEX/NSString+FLEX.h>
#import <FLEX/NSUserDefaults+FLEX.h>
#import <FLEX/NSMapTable+FLEX_Subscripting.h>
#import <FLEX/NSTimer+FLEX.h>
#import "NSObject+FLEX_Reflection.h"
#import "NSArray+FLEX.h"
#import "NSUserDefaults+FLEX.h"
#import "NSTimer+FLEX.h"
+11 -12
View File
@@ -6,18 +6,17 @@
// Copyright © 2020 FLEX Team. All rights reserved.
//
#import <FLEX/FLEXFilteringTableViewController.h>
#import <FLEX/FLEXNavigationController.h>
#import <FLEX/FLEXTableViewController.h>
#import <FLEX/FLEXTableView.h>
#import "FLEXFilteringTableViewController.h"
#import "FLEXNavigationController.h"
#import "FLEXTableViewController.h"
#import "FLEXTableView.h"
#import <FLEX/FLEXSingleRowSection.h>
#import <FLEX/FLEXTableViewSection.h>
#import "FLEXSingleRowSection.h"
#import "FLEXTableViewSection.h"
#import <FLEX/FLEXCodeFontCell.h>
#import <FLEX/FLEXSubtitleTableViewCell.h>
#import <FLEX/FLEXTableViewCell.h>
#import <FLEX/FLEXMultilineTableViewCell.h>
#import <FLEX/FLEXKeyValueTableViewCell.h>
#import "FLEXCodeFontCell.h"
#import "FLEXSubtitleTableViewCell.h"
#import "FLEXTableViewCell.h"
#import "FLEXMultilineTableViewCell.h"
#import "FLEXKeyValueTableViewCell.h"
#import <FLEX/FLEXScopeCarousel.h>
+11 -19
View File
@@ -6,25 +6,17 @@
// Copyright © 2020 FLEX Team. All rights reserved.
//
#import <FLEX/FLEXObjectExplorerFactory.h>
#import <FLEX/FLEXObjectExplorerViewController.h>
#import "FLEXObjectExplorerFactory.h"
#import "FLEXObjectExplorerViewController.h"
#import <FLEX/FLEXObjectExplorer.h>
#import "FLEXObjectExplorer.h"
#import <FLEX/FLEXShortcut.h>
#import <FLEX/FLEXShortcutsFactory+Defaults.h>
#import <FLEX/FLEXShortcutsSection.h>
#import <FLEX/FLEXBlockShortcuts.h>
#import <FLEX/FLEXBundleShortcuts.h>
#import <FLEX/FLEXClassShortcuts.h>
#import <FLEX/FLEXImageShortcuts.h>
#import <FLEX/FLEXLayerShortcuts.h>
#import <FLEX/FLEXViewControllerShortcuts.h>
#import <FLEX/FLEXViewShortcuts.h>
#import "FLEXShortcut.h"
#import "FLEXShortcutsSection.h"
#import <FLEX/FLEXCollectionContentSection.h>
#import <FLEX/FLEXColorPreviewSection.h>
#import <FLEX/FLEXDefaultsContentSection.h>
#import <FLEX/FLEXMetadataSection.h>
#import <FLEX/FLEXMutableListSection.h>
#import <FLEX/FLEXObjectInfoSection.h>
#import "FLEXCollectionContentSection.h"
#import "FLEXColorPreviewSection.h"
#import "FLEXDefaultsContentSection.h"
#import "FLEXMetadataSection.h"
#import "FLEXMutableListSection.h"
#import "FLEXObjectInfoSection.h"
+15 -15
View File
@@ -6,20 +6,20 @@
// Copyright © 2020 FLEX Team. All rights reserved.
//
#import <FLEX/FLEXObjcInternal.h>
#import <FLEX/FLEXRuntimeSafety.h>
#import <FLEX/FLEXBlockDescription.h>
#import <FLEX/FLEXTypeEncodingParser.h>
#import "FLEXObjcInternal.h"
#import "FLEXRuntimeSafety.h"
#import "FLEXBlockDescription.h"
#import "FLEXTypeEncodingParser.h"
#import <FLEX/FLEXMirror.h>
#import <FLEX/FLEXProtocol.h>
#import <FLEX/FLEXProperty.h>
#import <FLEX/FLEXIvar.h>
#import <FLEX/FLEXMethodBase.h>
#import <FLEX/FLEXMethod.h>
#import <FLEX/FLEXPropertyAttributes.h>
#import <FLEX/FLEXRuntime+Compare.h>
#import <FLEX/FLEXRuntime+UIKitHelpers.h>
#import "FLEXMirror.h"
#import "FLEXProtocol.h"
#import "FLEXProperty.h"
#import "FLEXIvar.h"
#import "FLEXMethodBase.h"
#import "FLEXMethod.h"
#import "FLEXPropertyAttributes.h"
#import "FLEXRuntime+Compare.h"
#import "FLEXRuntime+UIKitHelpers.h"
#import <FLEX/FLEXProtocolBuilder.h>
#import <FLEX/FLEXClassBuilder.h>
#import "FLEXProtocolBuilder.h"
#import "FLEXClassBuilder.h"
+13 -13
View File
@@ -7,19 +7,19 @@
// Copyright (c) 2020 FLEX Team. All rights reserved.
//
#import <FLEX/FLEXManager.h>
#import <FLEX/FLEXManager+Extensibility.h>
#import <FLEX/FLEXManager+Networking.h>
#import "FLEXManager.h"
#import "FLEXManager+Extensibility.h"
#import "FLEXManager+Networking.h"
#import <FLEX/FLEXExplorerToolbar.h>
#import <FLEX/FLEXExplorerToolbarItem.h>
#import <FLEX/FLEXGlobalsEntry.h>
#import "FLEXExplorerToolbar.h"
#import "FLEXExplorerToolbarItem.h"
#import "FLEXGlobalsEntry.h"
#import <FLEX/FLEX-Core.h>
#import <FLEX/FLEX-Runtime.h>
#import <FLEX/FLEX-Categories.h>
#import <FLEX/FLEX-ObjectExploring.h>
#import "FLEX-Core.h"
#import "FLEX-Runtime.h"
#import "FLEX-Categories.h"
#import "FLEX-ObjectExploring.h"
#import <FLEX/FLEXMacros.h>
#import <FLEX/FLEXAlert.h>
#import <FLEX/FLEXResources.h>
#import "FLEXMacros.h"
#import "FLEXAlert.h"
#import "FLEXResources.h"
@@ -8,12 +8,21 @@
#import <UIKit/UIKit.h>
@class FLEXDBQueryRowCell;
extern NSString * const kFLEXDBQueryRowCellReuse;
@protocol FLEXDBQueryRowCellLayoutSource <NSObject>
- (CGFloat)dbQueryRowCell:(FLEXDBQueryRowCell *)dbQueryRowCell minXForColumn:(NSUInteger)column;
- (CGFloat)dbQueryRowCell:(FLEXDBQueryRowCell *)dbQueryRowCell widthForColumn:(NSUInteger)column;
@end
@interface FLEXDBQueryRowCell : UITableViewCell
/// An array of NSString, NSNumber, or NSData objects
@property (nonatomic) NSArray *data;
@property (nonatomic, weak) id<FLEXDBQueryRowCellLayoutSource> layoutSource;
@end
@@ -63,11 +63,12 @@ NSString * const kFLEXDBQueryRowCellReuse = @"kFLEXDBQueryRowCellReuse";
- (void)layoutSubviews {
[super layoutSubviews];
CGFloat width = self.contentView.frame.size.width / self.labels.count;
CGFloat height = self.contentView.frame.size.height;
[self.labels flex_forEach:^(UILabel *label, NSUInteger i) {
label.frame = CGRectMake(width * i + 5, 0, (width - 10), height);
CGFloat width = [self.layoutSource dbQueryRowCell:self widthForColumn:i];
CGFloat minX = [self.layoutSource dbQueryRowCell:self minXForColumn:i];
label.frame = CGRectMake(minX + 5, 0, (width - 10), height);
}];
}
@@ -29,6 +29,7 @@
@optional
- (NSArray<NSString *> *)queryRowIDsInTable:(NSString *)tableName;
- (FLEXSQLResult *)executeStatement:(NSString *)SQLStatement;
@end
@@ -29,7 +29,7 @@
- (NSString *)rowTitle:(NSInteger)row;
- (NSArray<NSString *> *)contentForRow:(NSInteger)row;
- (CGFloat)multiColumnTableView:(FLEXMultiColumnTableView *)tableView widthForContentCellInColumn:(NSInteger)column;
- (CGFloat)multiColumnTableView:(FLEXMultiColumnTableView *)tableView minWidthForContentCellInColumn:(NSInteger)column;
- (CGFloat)multiColumnTableView:(FLEXMultiColumnTableView *)tableView heightForContentCellInRow:(NSInteger)row;
- (CGFloat)heightForTopHeaderInTableView:(FLEXMultiColumnTableView *)tableView;
- (CGFloat)widthForLeftHeaderInTableView:(FLEXMultiColumnTableView *)tableView;
@@ -9,10 +9,12 @@
#import "FLEXMultiColumnTableView.h"
#import "FLEXDBQueryRowCell.h"
#import "FLEXTableLeftCell.h"
#import "NSArray+FLEX.h"
#import "FLEXColor.h"
@interface FLEXMultiColumnTableView () <
UITableViewDataSource, UITableViewDelegate, UIScrollViewDelegate
UITableViewDataSource, UITableViewDelegate,
UIScrollViewDelegate, FLEXDBQueryRowCellLayoutSource
>
@property (nonatomic) UIScrollView *contentScrollView;
@@ -21,12 +23,12 @@
@property (nonatomic) UITableView *contentTableView;
@property (nonatomic) UIView *leftHeader;
@property (nonatomic) NSArray<UIView *> *headerViews;
/// \c NSNotFound if no column selected
@property (nonatomic) NSInteger sortColumn;
@property (nonatomic) FLEXTableColumnHeaderSortType sortType;
@property (nonatomic) NSArray *rowData;
@property (nonatomic, readonly) NSInteger numberOfColumns;
@property (nonatomic, readonly) NSInteger numberOfRows;
@property (nonatomic, readonly) CGFloat topHeaderHeight;
@@ -71,9 +73,9 @@ static const CGFloat kColumnMargin = 1;
}
CGFloat contentWidth = 0.0;
NSInteger rowsCount = self.numberOfColumns;
for (int i = 0; i < rowsCount; i++) {
contentWidth += [self contentWidthForColumn:i];
NSInteger columnsCount = self.numberOfColumns;
for (int i = 0; i < columnsCount; i++) {
contentWidth += CGRectGetWidth(self.headerViews[i].bounds);
}
CGFloat contentHeight = height - topheaderHeight - topInsets;
@@ -147,26 +149,30 @@ static const CGFloat kColumnMargin = 1;
#pragma mark - Data
- (void)reloadData {
[self loadHeaderData];
[self loadLeftViewData];
[self loadContentData];
[self loadHeaderData];
}
- (void)loadHeaderData {
// Remove existing headers, if any
for (UIView *subview in self.headerScrollView.subviews) {
for (UIView *subview in self.headerViews) {
[subview removeFromSuperview];
}
CGFloat xOffset = 0.0;
for (NSInteger column = 0; column < self.numberOfColumns; column++) {
CGFloat width = [self contentWidthForColumn:column] + self.columnMargin;
FLEXTableColumnHeader *header = [[FLEXTableColumnHeader alloc]
initWithFrame:CGRectMake(xOffset, 0, width, self.topHeaderHeight - 1)
];
__block CGFloat xOffset = 0;
self.headerViews = [NSArray flex_forEachUpTo:self.numberOfColumns map:^id(NSUInteger column) {
FLEXTableColumnHeader *header = [FLEXTableColumnHeader new];
header.titleLabel.text = [self columnTitle:column];
CGSize fittingSize = CGSizeMake(CGFLOAT_MAX, self.topHeaderHeight - 1);
CGFloat width = self.columnMargin + MAX(
[self minContentWidthForColumn:column],
[header sizeThatFits:fittingSize].width
);
header.frame = CGRectMake(xOffset, 0, width, self.topHeaderHeight - 1);
if (column == self.sortColumn) {
header.sortType = self.sortType;
}
@@ -178,21 +184,22 @@ static const CGFloat kColumnMargin = 1;
[header addGestureRecognizer:gesture];
header.userInteractionEnabled = YES;
[self.headerScrollView addSubview:header];
xOffset += width;
}
[self.headerScrollView addSubview:header];
return header;
}];
}
- (void)contentHeaderTap:(UIGestureRecognizer *)gesture {
NSInteger newSortColumn = [self.headerScrollView.subviews indexOfObject:gesture.view];
NSInteger newSortColumn = [self.headerViews indexOfObject:gesture.view];
FLEXTableColumnHeaderSortType newType = FLEXNextTableColumnHeaderSortType(self.sortType);
// Reset old header
FLEXTableColumnHeader *oldHeader = (id)self.headerScrollView.subviews[self.sortColumn];
FLEXTableColumnHeader *oldHeader = (id)self.headerViews[self.sortColumn];
oldHeader.sortType = FLEXTableColumnHeaderSortTypeNone;
// Update new header
FLEXTableColumnHeader *newHeader = (id)self.headerScrollView.subviews[newSortColumn];
FLEXTableColumnHeader *newHeader = (id)self.headerViews[newSortColumn];
newHeader.sortType = newType;
// Update self
@@ -227,13 +234,13 @@ static const CGFloat kColumnMargin = 1;
}
// Right side table view for data
else {
self.rowData = [self.dataSource contentForRow:indexPath.row];
FLEXDBQueryRowCell *cell = [tableView
dequeueReusableCellWithIdentifier:kFLEXDBQueryRowCellReuse forIndexPath:indexPath
];
cell.contentView.backgroundColor = backgroundColor;
cell.data = [self.dataSource contentForRow:indexPath.row];
cell.layoutSource = self;
NSAssert(cell.data.count == self.numberOfColumns, @"Count of data provided was incorrect");
return cell;
}
@@ -280,6 +287,17 @@ static const CGFloat kColumnMargin = 1;
}
#pragma mark FLEXDBQueryRowCellLayoutSource
- (CGFloat)dbQueryRowCell:(FLEXDBQueryRowCell *)dbQueryRowCell minXForColumn:(NSUInteger)column {
return CGRectGetMinX(self.headerViews[column].frame);
}
- (CGFloat)dbQueryRowCell:(FLEXDBQueryRowCell *)dbQueryRowCell widthForColumn:(NSUInteger)column {
return CGRectGetWidth(self.headerViews[column].bounds);
}
#pragma mark DataSource Accessor
- (NSInteger)numberOfRows {
@@ -298,8 +316,8 @@ static const CGFloat kColumnMargin = 1;
return [self.dataSource rowTitle:row];
}
- (CGFloat)contentWidthForColumn:(NSInteger)column {
return [self.dataSource multiColumnTableView:self widthForContentCellInColumn:column];
- (CGFloat)minContentWidthForColumn:(NSInteger)column {
return [self.dataSource multiColumnTableView:self minWidthForContentCellInColumn:column];
}
- (CGFloat)contentHeightForRow:(NSInteger)row {
@@ -13,7 +13,7 @@
@synthesize keyedRows = _keyedRows;
+ (instancetype)message:(NSString *)message {
return [[self alloc] initWithmessage:message columns:nil rows:nil];
return [[self alloc] initWithMessage:message columns:nil rows:nil];
}
+ (instancetype)error:(NSString *)message {
@@ -23,12 +23,12 @@
}
+ (instancetype)columns:(NSArray<NSString *> *)columnNames rows:(NSArray<NSArray<NSString *> *> *)rowData {
return [[self alloc] initWithmessage:nil columns:columnNames rows:rowData];
return [[self alloc] initWithMessage:nil columns:columnNames rows:rowData];
}
- (id)initWithmessage:(NSString *)message columns:(NSArray *)columns rows:(NSArray<NSArray *> *)rows {
- (instancetype)initWithMessage:(NSString *)message columns:(NSArray<NSString *> *)columns rows:(NSArray<NSArray<NSString *> *> *)rows {
NSParameterAssert(message || (columns && rows));
NSParameterAssert(columns.count == rows.firstObject.count);
NSParameterAssert(rows.count == 0 || columns.count == rows.firstObject.count);
self = [super init];
if (self) {
@@ -12,7 +12,10 @@
#import "FLEXRuntimeConstants.h"
#import <sqlite3.h>
static NSString * const QUERY_TABLENAMES = @"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name";
#define kQuery(name, str) static NSString * const QUERY_##name = str
kQuery(TABLENAMES, @"SELECT name FROM sqlite_master WHERE type='table' ORDER BY name");
kQuery(ROWIDS, @"SELECT rowid FROM \"%@\" ORDER BY rowid ASC");
@interface FLEXSQLiteDatabaseManager ()
@property (nonatomic) sqlite3 *db;
@@ -30,7 +33,7 @@ static NSString * const QUERY_TABLENAMES = @"SELECT name FROM sqlite_master WHER
- (instancetype)initWithPath:(NSString *)path {
self = [super init];
if (self) {
self.path = path;;
self.path = path;
}
return self;
@@ -107,16 +110,36 @@ static NSString * const QUERY_TABLENAMES = @"SELECT name FROM sqlite_master WHER
- (NSArray<NSString *> *)queryAllColumnsOfTable:(NSString *)tableName {
NSString *sql = [NSString stringWithFormat:@"PRAGMA table_info('%@')",tableName];
FLEXSQLResult *results = [self executeStatement:sql];
// https://github.com/FLEXTool/FLEX/issues/554
if (!results.keyedRows.count) {
sql = [NSString stringWithFormat:@"SELECT * FROM pragma_table_info('%@')", tableName];
results = [self executeStatement:sql];
// Fallback to empty query
if (!results.keyedRows.count) {
sql = [NSString stringWithFormat:@"SELECT * FROM \"%@\" where 0=1", tableName];
return [self executeStatement:sql].columns ?: @[];
}
}
return [results.keyedRows flex_mapped:^id(NSDictionary *column, NSUInteger idx) {
return column[@"name"];
}] ?: @[];
}
- (NSArray<NSArray *> *)queryAllDataInTable:(NSString *)tableName {
return [self executeStatement:[@"SELECT * FROM "
stringByAppendingString:tableName
]].rows ?: @[];
NSString *command = [NSString stringWithFormat:@"SELECT * FROM \"%@\"", tableName];
return [self executeStatement:command].rows ?: @[];
}
- (NSArray<NSString *> *)queryRowIDsInTable:(NSString *)tableName {
NSString *command = [NSString stringWithFormat:QUERY_ROWIDS, tableName];
NSArray<NSArray<NSString *> *> *data = [self executeStatement:command].rows ?: @[];
return [data flex_mapped:^id(NSArray<NSString *> *obj, NSUInteger idx) {
return obj.firstObject;
}];
}
- (FLEXSQLResult *)executeStatement:(NSString *)sql {
@@ -138,7 +161,7 @@ static NSString * const QUERY_TABLENAMES = @"SELECT name FROM sqlite_master WHER
return self.lastResult;
}
// Grab columns
// Grab columns (columnCount will be 0 for insert/update/delete)
int columnCount = sqlite3_column_count(pstmt);
NSArray<NSString *> *columns = [NSArray flex_forEachUpTo:columnCount map:^id(NSUInteger i) {
return @(sqlite3_column_name(pstmt, (int)i));
@@ -156,8 +179,9 @@ static NSString * const QUERY_TABLENAMES = @"SELECT name FROM sqlite_master WHER
}
if (status == SQLITE_DONE) {
if (rows.count) {
// We selected some rows
// columnCount will be 0 for insert/update/delete
if (rows.count || columnCount > 0) {
// We executed a SELECT query
result = _lastResult = [FLEXSQLResult columns:columns rows:rows];
} else {
// We executed a query like INSERT, UDPATE, or DELETE
@@ -257,7 +281,7 @@ static NSString * const QUERY_TABLENAMES = @"SELECT name FROM sqlite_master WHER
- (FLEXSQLResult *)errorResult:(NSString *)description {
const char *error = sqlite3_errmsg(_db);
NSString *message = error ? @(error) : [NSString
stringWithFormat:@"(%@: empty error", description
stringWithFormat:@"(%@: empty error)", description
];
return [FLEXSQLResult error:message];
@@ -11,6 +11,9 @@
#import "UIFont+FLEX.h"
#import "FLEXUtility.h"
static const CGFloat kMargin = 5;
static const CGFloat kArrowWidth = 20;
@interface FLEXTableColumnHeader ()
@property (nonatomic, readonly) UILabel *arrowLabel;
@property (nonatomic, readonly) UIView *lineView;
@@ -60,9 +63,16 @@
CGSize size = self.frame.size;
self.titleLabel.frame = CGRectMake(5, 0, size.width - 25, size.height);
self.arrowLabel.frame = CGRectMake(size.width - 20, 0, 20, size.height);
self.titleLabel.frame = CGRectMake(kMargin, 0, size.width - kArrowWidth - kMargin, size.height);
self.arrowLabel.frame = CGRectMake(size.width - kArrowWidth, 0, kArrowWidth, size.height);
self.lineView.frame = CGRectMake(size.width - 1, 2, FLEXPointsToPixels(1), size.height - 4);
}
- (CGSize)sizeThatFits:(CGSize)size {
CGFloat margins = kArrowWidth - 2 * kMargin;
size = CGSizeMake(size.width - margins, size.height);
CGFloat width = [_titleLabel sizeThatFits:size].width + margins;
return CGSizeMake(width, size.height);
}
@end
@@ -7,10 +7,30 @@
//
#import <UIKit/UIKit.h>
#import "FLEXDatabaseManager.h"
NS_ASSUME_NONNULL_BEGIN
@interface FLEXTableContentViewController : UIViewController
/// Display a mutable table with the given columns, rows, and name.
///
/// @param columnNames self explanatory.
/// @param rowData an array of rows, where each row is an array of column data.
/// @param rowIDs an array of stringy row IDs. Required for deleting rows.
/// @param tableName an optional name of the table being viewed, if any. Enables adding rows.
/// @param databaseManager an optional manager to allow modifying the table.
/// Required for deleting rows. Required for adding rows if \c tableName is supplied.
+ (instancetype)columns:(NSArray<NSString *> *)columnNames
rows:(NSArray<NSArray<NSString *> *> *)rowData
rowIDs:(NSArray<NSString *> *)rowIDs
tableName:(NSString *)tableName
database:(id<FLEXDatabaseManager>)databaseManager;
/// Display an immutable table with the given columns and rows.
+ (instancetype)columns:(NSArray<NSString *> *)columnNames
rows:(NSArray<NSArray<NSString *> *> *)rowData;
@end
NS_ASSUME_NONNULL_END
@@ -7,15 +7,22 @@
//
#import "FLEXTableContentViewController.h"
#import "FLEXTableRowDataViewController.h"
#import "FLEXMultiColumnTableView.h"
#import "FLEXWebViewController.h"
#import "FLEXUtility.h"
#import "UIBarButtonItem+FLEX.h"
@interface FLEXTableContentViewController () <
FLEXMultiColumnTableViewDataSource, FLEXMultiColumnTableViewDelegate
>
@property (nonatomic, readonly) NSArray<NSString *> *columns;
@property (nonatomic, copy) NSArray<NSArray *> *rows;
@property (nonatomic) NSMutableArray<NSArray *> *rows;
@property (nonatomic, readonly) NSString *tableName;
@property (nonatomic, nullable) NSMutableArray<NSString *> *rowIDs;
@property (nonatomic, readonly, nullable) id<FLEXDatabaseManager> databaseManager;
@property (nonatomic, readonly) BOOL canRefresh;
@property (nonatomic) FLEXMultiColumnTableView *multiColumnView;
@end
@@ -23,11 +30,44 @@
@implementation FLEXTableContentViewController
+ (instancetype)columns:(NSArray<NSString *> *)columnNames
rows:(NSArray<NSArray<NSString *> *> *)rowData
rowIDs:(NSArray<NSString *> *)rowIDs
tableName:(NSString *)tableName
database:(id<FLEXDatabaseManager>)databaseManager {
return [[self alloc]
initWithColumns:columnNames
rows:rowData
rowIDs:rowIDs
tableName:tableName
database:databaseManager
];
}
+ (instancetype)columns:(NSArray<NSString *> *)cols
rows:(NSArray<NSArray<NSString *> *> *)rowData {
FLEXTableContentViewController *controller = [self new];
controller->_columns = columnNames;
controller->_rows = rowData;
return controller;
return [[self alloc] initWithColumns:cols rows:rowData rowIDs:nil tableName:nil database:nil];
}
- (instancetype)initWithColumns:(NSArray<NSString *> *)columnNames
rows:(NSArray<NSArray<NSString *> *> *)rowData
rowIDs:(nullable NSArray<NSString *> *)rowIDs
tableName:(nullable NSString *)tableName
database:(nullable id<FLEXDatabaseManager>)databaseManager {
// Must supply all optional parameters as one, or none
BOOL all = rowIDs && tableName && databaseManager;
BOOL none = !rowIDs && !tableName && !databaseManager;
NSParameterAssert(all || none);
self = [super init];
if (self) {
self->_columns = columnNames.copy;
self->_rows = rowData.mutableCopy;
self->_rowIDs = rowIDs.mutableCopy;
self->_tableName = tableName.copy;
self->_databaseManager = databaseManager;
}
return self;
}
- (void)loadView {
@@ -38,9 +78,9 @@
- (void)viewDidLoad {
[super viewDidLoad];
self.edgesForExtendedLayout = UIRectEdgeNone;
self.title = self.tableName;
[self.multiColumnView reloadData];
[self setupToolbarItems];
}
- (FLEXMultiColumnTableView *)multiColumnView {
@@ -56,6 +96,10 @@
return _multiColumnView;
}
- (BOOL)canRefresh {
return self.databaseManager && self.tableName;
}
#pragma mark MultiColumnTableView DataSource
- (NSInteger)numberOfColumnsInTableView:(FLEXMultiColumnTableView *)tableView {
@@ -84,8 +128,8 @@
}
- (CGFloat)multiColumnTableView:(FLEXMultiColumnTableView *)tableView
widthForContentCellInColumn:(NSInteger)column {
return 120;
minWidthForContentCellInColumn:(NSInteger)column {
return 100;
}
- (CGFloat)heightForTopHeaderInTableView:(FLEXMultiColumnTableView *)tableView {
@@ -111,6 +155,10 @@
return [NSString stringWithFormat:@"%@:\n%@", self.columns[idx], field];
}];
NSArray<NSString *> *values = [self.rows[row] flex_mapped:^id(NSString *value, NSUInteger idx) {
return [NSString stringWithFormat:@"'%@'", value];
}];
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title([@"Row " stringByAppendingString:@(row).stringValue]);
NSString *message = [fields componentsJoinedByString:@"\n\n"];
@@ -118,6 +166,34 @@
make.button(@"Copy").handler(^(NSArray<NSString *> *strings) {
UIPasteboard.generalPasteboard.string = message;
});
make.button(@"Copy as CSV").handler(^(NSArray<NSString *> *strings) {
UIPasteboard.generalPasteboard.string = [values componentsJoinedByString:@", "];
});
make.button(@"Focus on Row").handler(^(NSArray<NSString *> *strings) {
UIViewController *focusedRow = [FLEXTableRowDataViewController
rows:[NSDictionary dictionaryWithObjects:self.rows[row] forKeys:self.columns]
];
[self.navigationController pushViewController:focusedRow animated:YES];
});
// Option to delete row
BOOL hasRowID = self.rows.count && row < self.rows.count;
if (hasRowID && self.canRefresh) {
make.button(@"Delete").destructiveStyle().handler(^(NSArray<NSString *> *strings) {
NSString *deleteRow = [NSString stringWithFormat:
@"DELETE FROM %@ WHERE rowid = %@",
self.tableName, self.rowIDs[row]
];
[self executeStatementAndShowResult:deleteRow completion:^(BOOL success) {
// Remove deleted row and reload view
if (success) {
[self reloadTableDataFromDB];
}
}];
});
}
make.button(@"Dismiss").cancelStyle();
} showFrom:self];
}
@@ -127,7 +203,8 @@
sortType:(FLEXTableColumnHeaderSortType)sortType {
NSArray<NSArray *> *sortContentData = [self.rows
sortedArrayUsingComparator:^NSComparisonResult(NSArray *obj1, NSArray *obj2) {
sortedArrayWithOptions:NSSortStable
usingComparator:^NSComparisonResult(NSArray *obj1, NSArray *obj2) {
id a = obj1[column], b = obj2[column];
if (a == NSNull.null) {
return NSOrderedAscending;
@@ -135,6 +212,11 @@
if (b == NSNull.null) {
return NSOrderedDescending;
}
if ([a respondsToSelector:@selector(compare:options:)] &&
[b respondsToSelector:@selector(compare:options:)]) {
return [a compare:b options:NSNumericSearch];
}
if ([a respondsToSelector:@selector(compare:)] && [b respondsToSelector:@selector(compare:)]) {
return [a compare:b];
@@ -148,12 +230,11 @@
sortContentData = sortContentData.reverseObjectEnumerator.allObjects.copy;
}
self.rows = sortContentData;
self.rows = sortContentData.mutableCopy;
[self.multiColumnView reloadData];
}
#pragma mark -
#pragma mark About Transition
#pragma mark - About Transition
- (void)willTransitionToTraitCollection:(UITraitCollection *)newCollection
withTransitionCoordinator:(id <UIViewControllerTransitionCoordinator>)coordinator {
@@ -171,4 +252,108 @@
} completion:nil];
}
#pragma mark - Toolbar
- (void)setupToolbarItems {
// We do not support modifying realm databases
if (![self.databaseManager respondsToSelector:@selector(executeStatement:)]) {
return;
}
UIBarButtonItem *trashButton = FLEXBarButtonItemSystem(Trash, self, @selector(trashPressed));
UIBarButtonItem *addButton = FLEXBarButtonItemSystem(Add, self, @selector(addPressed));
// Only allow adding rows or deleting rows if we have a table name
trashButton.enabled = self.canRefresh;
addButton.enabled = self.canRefresh;
self.toolbarItems = @[
UIBarButtonItem.flex_flexibleSpace,
addButton,
UIBarButtonItem.flex_flexibleSpace,
[trashButton flex_withTintColor:UIColor.redColor],
];
}
- (void)trashPressed {
NSParameterAssert(self.tableName);
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(@"Delete All Rows");
make.message(@"All rows in this table will be permanently deleted.\nDo you want to proceed?");
make.button(@"Yes, I'm sure").destructiveStyle().handler(^(NSArray<NSString *> *strings) {
NSString *deleteAll = [NSString stringWithFormat:@"DELETE FROM %@", self.tableName];
[self executeStatementAndShowResult:deleteAll completion:^(BOOL success) {
// Only dismiss on success
if (success) {
[self.navigationController popViewControllerAnimated:YES];
}
}];
});
make.button(@"Cancel").cancelStyle();
} showFrom:self];
}
- (void)addPressed {
NSParameterAssert(self.tableName);
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(@"Add a New Row");
make.message(@"Comma separate values to use in an INSERT statement.\n\n");
make.message(@"INSERT INTO [table] VALUES (your_input)");
make.textField(@"5, 'John Smith', 14,...");
make.button(@"Insert").handler(^(NSArray<NSString *> *strings) {
NSString *statement = [NSString stringWithFormat:
@"INSERT INTO %@ VALUES (%@)", self.tableName, strings[0]
];
[self executeStatementAndShowResult:statement completion:^(BOOL success) {
if (success) {
[self reloadTableDataFromDB];
}
}];
});
make.button(@"Cancel").cancelStyle();
} showFrom:self];
}
#pragma mark - Helpers
- (void)executeStatementAndShowResult:(NSString *)statement
completion:(void (^_Nullable)(BOOL success))completion {
NSParameterAssert(self.databaseManager);
FLEXSQLResult *result = [self.databaseManager executeStatement:statement];
[FLEXAlert makeAlert:^(FLEXAlert *make) {
if (result.isError) {
make.title(@"Error");
}
make.message(result.message ?: @"<no output>");
make.button(@"Dismiss").cancelStyle().handler(^(NSArray<NSString *> *_) {
if (completion) {
completion(!result.isError);
}
});
} showFrom:self];
}
- (void)reloadTableDataFromDB {
if (!self.canRefresh) {
return;
}
NSArray<NSArray *> *rows = [self.databaseManager queryAllDataInTable:self.tableName];
NSArray<NSString *> *rowIDs = nil;
if ([self.databaseManager respondsToSelector:@selector(queryRowIDsInTable:)]) {
rowIDs = [self.databaseManager queryRowIDsInTable:self.tableName];
}
self.rows = rows.mutableCopy;
self.rowIDs = rowIDs.mutableCopy;
[self.multiColumnView reloadData];
}
@end
@@ -14,6 +14,7 @@
#import "FLEXMutableListSection.h"
#import "NSArray+FLEX.h"
#import "FLEXAlert.h"
#import "FLEXMacros.h"
@interface FLEXTableListViewController ()
@property (nonatomic, readonly) id<FLEXDatabaseManager> dbm;
@@ -70,9 +71,13 @@
self.tables.selectionHandler = ^(FLEXTableListViewController *host, NSString *tableName) {
NSArray *rows = [host.dbm queryAllDataInTable:tableName];
NSArray *columns = [host.dbm queryAllColumnsOfTable:tableName];
UIViewController *resultsScreen = [FLEXTableContentViewController columns:columns rows:rows];
resultsScreen.title = tableName;
NSArray *rowIDs = nil;
if ([host.dbm respondsToSelector:@selector(queryRowIDsInTable:)]) {
rowIDs = [host.dbm queryRowIDsInTable:tableName];
}
UIViewController *resultsScreen = [FLEXTableContentViewController
columns:columns rows:rows rowIDs:rowIDs tableName:tableName database:host.dbm
];
[host.navigationController pushViewController:resultsScreen animated:YES];
};
@@ -0,0 +1,14 @@
//
// FLEXTableRowDataViewController.h
// FLEX
//
// Created by Chaoshuai Lu on 7/8/20.
//
#import "FLEXFilteringTableViewController.h"
@interface FLEXTableRowDataViewController : FLEXFilteringTableViewController
+ (instancetype)rows:(NSDictionary<NSString *, id> *)rowData;
@end
@@ -0,0 +1,54 @@
//
// FLEXTableRowDataViewController.m
// FLEX
//
// Created by Chaoshuai Lu on 7/8/20.
//
#import "FLEXTableRowDataViewController.h"
#import "FLEXMutableListSection.h"
#import "FLEXAlert.h"
@interface FLEXTableRowDataViewController ()
@property (nonatomic) NSDictionary<NSString *, NSString *> *rowsByColumn;
@end
@implementation FLEXTableRowDataViewController
#pragma mark - Initialization
+ (instancetype)rows:(NSDictionary<NSString *, id> *)rowData {
FLEXTableRowDataViewController *controller = [self new];
controller.rowsByColumn = rowData;
return controller;
}
#pragma mark - Overrides
- (NSArray<FLEXTableViewSection *> *)makeSections {
NSDictionary<NSString *, NSString *> *rowsByColumn = self.rowsByColumn;
FLEXMutableListSection<NSString *> *section = [FLEXMutableListSection list:self.rowsByColumn.allKeys
cellConfiguration:^(UITableViewCell *cell, NSString *column, NSInteger row) {
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
cell.textLabel.text = column;
cell.detailTextLabel.text = rowsByColumn[column].description;
} filterMatcher:^BOOL(NSString *filterText, NSString *column) {
return [column localizedCaseInsensitiveContainsString:filterText] ||
[rowsByColumn[column] localizedCaseInsensitiveContainsString:filterText];
}
];
section.selectionHandler = ^(UIViewController *host, NSString *column) {
UIPasteboard.generalPasteboard.string = rowsByColumn[column].description;
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(@"Column Copied to Clipboard");
make.message(rowsByColumn[column].description);
make.button(@"Dismiss").cancelStyle();
} showFrom:host];
};
return @[section];
}
@end
@@ -35,6 +35,7 @@ static const NSInteger kFLEXLiveObjectsSortBySizeIndex = 2;
self.showsSearchBar = YES;
self.showSearchBarInitially = YES;
self.activatesSearchBarAutomatically = YES;
self.searchBarDebounceInterval = kFLEXDebounceInstant;
self.showsCarousel = YES;
self.carousel.items = @[@"A→Z", @"Count", @"Size"];
@@ -225,7 +226,10 @@ static const NSInteger kFLEXLiveObjectsSortBySizeIndex = 2;
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NSString *className = self.filteredClassNames[indexPath.row];
UIViewController *instances = [FLEXObjectListViewController instancesOfClassWithName:className];
UIViewController *instances = [FLEXObjectListViewController
instancesOfClassWithName:className
retained:YES
];
[self.navigationController pushViewController:instances animated:YES];
}
@@ -12,8 +12,8 @@
/// This will either return a list of the instances, or take you straight
/// to the explorer itself if there is only one instance.
+ (UIViewController *)instancesOfClassWithName:(NSString *)className;
+ (UIViewController *)instancesOfClassWithName:(NSString *)className retained:(BOOL)retain;
+ (instancetype)subclassesOfClassWithName:(NSString *)className;
+ (instancetype)objectsWithReferencesToObject:(id)object;
+ (instancetype)objectsWithReferencesToObject:(id)object retained:(BOOL)retain;
@end
@@ -20,11 +20,25 @@
#import <malloc/malloc.h>
typedef NS_ENUM(NSUInteger, FLEXObjectReferenceSection) {
FLEXObjectReferenceSectionMain,
FLEXObjectReferenceSectionAutoLayout,
FLEXObjectReferenceSectionKVO,
FLEXObjectReferenceSectionFLEX,
FLEXObjectReferenceSectionCount
};
@interface FLEXObjectListViewController ()
@property (nonatomic, readonly, class) NSArray<NSPredicate *> *defaultPredicates;
@property (nonatomic, readonly, class) NSArray<NSString *> *defaultSectionTitles;
@property (nonatomic, copy) NSArray<FLEXMutableListSection *> *sections;
@property (nonatomic, copy) NSArray<FLEXMutableListSection *> *allSections;
@property (nonatomic, readonly) NSArray<FLEXObjectRef *> *references;
@property (nonatomic, readonly, nullable) NSArray<FLEXObjectRef *> *references;
@property (nonatomic, readonly) NSArray<NSPredicate *> *predicates;
@property (nonatomic, readonly) NSArray<NSString *> *sectionTitles;
@@ -38,7 +52,7 @@
+ (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) {
BOOL(^isKVORelated)(FLEXObjectRef *, NSDictionary *) = ^BOOL(FLEXObjectRef *ref, NSDictionary *bindings) {
NSString *row = ref.reference;
return [row isEqualToString:@"__NSObserver object"] ||
[row isEqualToString:@"_CFXNotificationObjcObserverRegistration _object"];
@@ -65,34 +79,50 @@
([row hasPrefix:@"_NSAutoresizingMask"] && [row hasSuffix:@" _referenceItem"]) ||
[ignored containsObject:row];
};
/// These are FLEX classes and usually you aren't looking for FLEX references inside FLEX itself
BOOL(^isFLEXClass)(FLEXObjectRef *, NSDictionary *) = ^BOOL(FLEXObjectRef *ref, NSDictionary *bindings) {
return [ref.reference hasPrefix:@"FLEX"];
};
BOOL(^isEssential)(FLEXObjectRef *, NSDictionary *) = ^BOOL(FLEXObjectRef *ref, NSDictionary *bindings) {
return !(isObserver(ref, bindings) || isConstraintRelated(ref, bindings));
return !(
isKVORelated(ref, bindings) ||
isConstraintRelated(ref, bindings) ||
isFLEXClass(ref, bindings)
);
};
switch (section) {
case 0: return [NSPredicate predicateWithBlock:isEssential];
case 1: return [NSPredicate predicateWithBlock:isConstraintRelated];
case 2: return [NSPredicate predicateWithBlock:isObserver];
case FLEXObjectReferenceSectionMain:
return [NSPredicate predicateWithBlock:isEssential];
case FLEXObjectReferenceSectionAutoLayout:
return [NSPredicate predicateWithBlock:isConstraintRelated];
case FLEXObjectReferenceSectionKVO:
return [NSPredicate predicateWithBlock:isKVORelated];
case FLEXObjectReferenceSectionFLEX:
return [NSPredicate predicateWithBlock:isFLEXClass];
default: return nil;
}
}
+ (NSArray<NSPredicate *> *)defaultPredicates {
return @[[self defaultPredicateForSection:0],
[self defaultPredicateForSection:1],
[self defaultPredicateForSection:2]];
return [NSArray flex_forEachUpTo:FLEXObjectReferenceSectionCount map:^id(NSUInteger i) {
return [self defaultPredicateForSection:i];
}];
}
+ (NSArray<NSString *> *)defaultSectionTitles {
return @[@"", @"AutoLayout", @"Trivial"];
return @[
@"", @"AutoLayout", @"Key-Value Observing", @"FLEX"
];
}
#pragma mark - Initialization
- (id)initWithReferences:(NSArray<FLEXObjectRef *> *)references {
- (id)initWithReferences:(nullable NSArray<FLEXObjectRef *> *)references {
return [self initWithReferences:references predicates:nil sectionTitles:nil];
}
@@ -111,45 +141,33 @@
return self;
}
+ (UIViewController *)instancesOfClassWithName:(NSString *)className {
const char *classNameCString = className.UTF8String;
NSMutableArray *instances = [NSMutableArray new];
[FLEXHeapEnumerator enumerateLiveObjectsUsingBlock:^(__unsafe_unretained id object, __unsafe_unretained Class actualClass) {
if (strcmp(classNameCString, class_getName(actualClass)) == 0) {
// Note: objects of certain classes crash when retain is called.
// It is up to the user to avoid tapping into instance lists for these classes.
// Ex. OS_dispatch_queue_specific_queue
// In the future, we could provide some kind of warning for classes that are known to be problematic.
if (malloc_size((__bridge const void *)(object)) > 0) {
[instances addObject:object];
}
}
}];
NSArray<FLEXObjectRef *> *references = [FLEXObjectRef referencingAll:instances];
+ (UIViewController *)instancesOfClassWithName:(NSString *)className retained:(BOOL)retain {
NSArray<FLEXObjectRef *> *references = [FLEXHeapEnumerator
instancesOfClassWithName:className retained:retain
];
if (references.count == 1) {
return [FLEXObjectExplorerFactory
explorerViewControllerForObject:references.firstObject.object
explorerViewControllerForObject:references.firstObject.object
];
}
FLEXObjectListViewController *controller = [[self alloc] initWithReferences:references];
controller.title = [NSString stringWithFormat:@"%@ (%lu)", className, (unsigned long)instances.count];
controller.title = [NSString stringWithFormat:@"%@ (%@)", className, @(references.count)];
return controller;
}
+ (instancetype)subclassesOfClassWithName:(NSString *)className {
NSArray<Class> *classes = FLEXGetAllSubclasses(NSClassFromString(className), NO);
NSArray<FLEXObjectRef *> *references = [FLEXObjectRef referencingClasses:classes];
NSArray<FLEXObjectRef *> *references = [FLEXHeapEnumerator subclassesOfClassWithName:className];
FLEXObjectListViewController *controller = [[self alloc] initWithReferences:references];
controller.title = [NSString stringWithFormat:@"Subclasses of %@ (%lu)",
className, (unsigned long)classes.count
controller.title = [NSString stringWithFormat:@"Subclasses of %@ (%@)",
className, @(references.count)
];
return controller;
}
+ (instancetype)objectsWithReferencesToObject:(id)object {
+ (instancetype)objectsWithReferencesToObject:(id)object retained:(BOOL)retain {
static Class SwiftObjectClass = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
@@ -159,44 +177,17 @@
}
});
NSMutableArray<FLEXObjectRef *> *instances = [NSMutableArray new];
[FLEXHeapEnumerator enumerateLiveObjectsUsingBlock:^(__unsafe_unretained id tryObject, __unsafe_unretained Class actualClass) {
// Get all the ivars on the object. Start with the class and and travel up the inheritance chain.
// Once we find a match, record it and move on to the next object. There's no reason to find multiple matches within the same object.
Class tryClass = actualClass;
while (tryClass) {
unsigned int ivarCount = 0;
Ivar *ivars = class_copyIvarList(tryClass, &ivarCount);
NSArray<FLEXObjectRef *> *instances = [FLEXHeapEnumerator
objectsWithReferencesToObject:object retained:retain
];
for (unsigned int ivarIndex = 0; ivarIndex < ivarCount; ivarIndex++) {
Ivar ivar = ivars[ivarIndex];
NSString *typeEncoding = @(ivar_getTypeEncoding(ivar) ?: "");
if (typeEncoding.flex_typeIsObjectOrClass) {
ptrdiff_t offset = ivar_getOffset(ivar);
uintptr_t *fieldPointer = (__bridge void *)tryObject + offset;
if (*fieldPointer == (uintptr_t)(__bridge void *)object) {
NSString *ivarName = @(ivar_getName(ivar) ?: "???");
[instances addObject:[FLEXObjectRef referencing:tryObject ivar:ivarName]];
return;
}
}
}
tryClass = class_getSuperclass(tryClass);
}
}];
NSArray<NSPredicate *> *predicates = [self defaultPredicates];
NSArray<NSString *> *sectionTitles = [self defaultSectionTitles];
FLEXObjectListViewController *viewController = [[self alloc]
initWithReferences:instances
predicates:predicates
sectionTitles:sectionTitles
predicates:self.defaultPredicates
sectionTitles:self.defaultSectionTitles
];
viewController.title = [NSString stringWithFormat:@"Referencing %@ %p",
NSStringFromClass(object_getClass(object)), object
[FLEXRuntimeUtility safeClassNameForObject:object], object
];
return viewController;
}
@@ -246,14 +237,10 @@
}
];
__weak __typeof(self) weakSelf = self;
section.selectionHandler = ^(__kindof UIViewController *host, FLEXObjectRef *ref) {
__strong __typeof(self) strongSelf = weakSelf;
if (strongSelf) {
[strongSelf.navigationController pushViewController:[
FLEXObjectExplorerFactory explorerViewControllerForObject:ref.object
] animated:YES];
}
section.selectionHandler = ^(UIViewController *host, FLEXObjectRef *ref) {
[host.navigationController pushViewController:[
FLEXObjectExplorerFactory explorerViewControllerForObject:ref.object
] animated:YES];
};
section.customTitle = title;
+18 -4
View File
@@ -10,10 +10,19 @@
@interface FLEXObjectRef : NSObject
+ (instancetype)referencing:(id)object;
+ (instancetype)referencing:(id)object ivar:(NSString *)ivarName;
/// Reference an object without affecting its lifespan or or emitting reference-counting operations.
+ (instancetype)unretained:(__unsafe_unretained id)object;
+ (instancetype)unretained:(__unsafe_unretained id)object ivar:(NSString *)ivarName;
+ (NSArray<FLEXObjectRef *> *)referencingAll:(NSArray *)objects;
/// Reference an object and control its lifespan.
+ (instancetype)retained:(id)object;
+ (instancetype)retained:(id)object ivar:(NSString *)ivarName;
/// Reference an object and conditionally choose to retain it or not.
+ (instancetype)referencing:(__unsafe_unretained id)object retained:(BOOL)retain;
+ (instancetype)referencing:(__unsafe_unretained id)object ivar:(NSString *)ivarName retained:(BOOL)retain;
+ (NSArray<FLEXObjectRef *> *)referencingAll:(NSArray *)objects retained:(BOOL)retain;
/// Classes do not have a summary, and the reference is just the class name.
+ (NSArray<FLEXObjectRef *> *)referencingClasses:(NSArray<Class> *)classes;
@@ -22,6 +31,11 @@
/// For instances, this is the result of -[FLEXRuntimeUtility summaryForObject:]
/// For classes, there is no summary.
@property (nonatomic, readonly) NSString *summary;
@property (nonatomic, readonly) id object;
@property (nonatomic, readonly, unsafe_unretained) id object;
/// Retains the referenced object if it is not already retained
- (void)retainObject;
/// Releases the referenced object if it is already retained
- (void)releaseObject;
@end
+48 -12
View File
@@ -10,44 +10,70 @@
#import "FLEXRuntimeUtility.h"
#import "NSArray+FLEX.h"
@interface FLEXObjectRef ()
@interface FLEXObjectRef () {
/// Used to retain the object if desired
id _retainer;
}
@property (nonatomic, readonly) BOOL wantsSummary;
@end
@implementation FLEXObjectRef
@synthesize summary = _summary;
+ (instancetype)referencing:(id)object {
return [self referencing:object showSummary:YES];
+ (instancetype)unretained:(__unsafe_unretained id)object {
return [self referencing:object showSummary:YES retained:NO];
}
+ (instancetype)referencing:(id)object showSummary:(BOOL)showSummary {
return [[self alloc] initWithObject:object ivarName:nil showSummary:showSummary];
+ (instancetype)unretained:(__unsafe_unretained id)object ivar:(NSString *)ivarName {
return [[self alloc] initWithObject:object ivarName:ivarName showSummary:YES retained:NO];
}
+ (instancetype)referencing:(id)object ivar:(NSString *)ivarName {
return [[self alloc] initWithObject:object ivarName:ivarName showSummary:YES];
+ (instancetype)retained:(id)object {
return [self referencing:object showSummary:YES retained:YES];
}
+ (NSArray<FLEXObjectRef *> *)referencingAll:(NSArray *)objects {
+ (instancetype)retained:(id)object ivar:(NSString *)ivarName {
return [[self alloc] initWithObject:object ivarName:ivarName showSummary:YES retained:YES];
}
+ (instancetype)referencing:(__unsafe_unretained id)object retained:(BOOL)retain {
return retain ? [self retained:object] : [self unretained:object];
}
+ (instancetype)referencing:(__unsafe_unretained id)object ivar:(NSString *)ivarName retained:(BOOL)retain {
return retain ? [self retained:object ivar:ivarName] : [self unretained:object ivar:ivarName];
}
+ (instancetype)referencing:(__unsafe_unretained id)object showSummary:(BOOL)showSummary retained:(BOOL)retain {
return [[self alloc] initWithObject:object ivarName:nil showSummary:showSummary retained:retain];
}
+ (NSArray<FLEXObjectRef *> *)referencingAll:(NSArray *)objects retained:(BOOL)retain {
return [objects flex_mapped:^id(id obj, NSUInteger idx) {
return [self referencing:obj showSummary:YES];
return [self referencing:obj showSummary:YES retained:retain];
}];
}
+ (NSArray<FLEXObjectRef *> *)referencingClasses:(NSArray<Class> *)classes {
return [classes flex_mapped:^id(id obj, NSUInteger idx) {
return [self referencing:obj showSummary:NO];
return [self referencing:obj showSummary:NO retained:NO];
}];
}
- (id)initWithObject:(id)object ivarName:(NSString *)ivar showSummary:(BOOL)showSummary {
- (id)initWithObject:(__unsafe_unretained id)object
ivarName:(NSString *)ivar
showSummary:(BOOL)showSummary
retained:(BOOL)retain {
self = [super init];
if (self) {
_object = object;
_wantsSummary = showSummary;
if (retain) {
_retainer = object;
}
NSString *class = NSStringFromClass(object_getClass(object));
NSString *class = [FLEXRuntimeUtility safeClassNameForObject:object];
if (ivar) {
_reference = [NSString stringWithFormat:@"%@ %@", class, ivar];
} else if (showSummary) {
@@ -73,4 +99,14 @@
}
}
- (void)retainObject {
if (!_retainer) {
_retainer = _object;
}
}
- (void)releaseObject {
_retainer = nil;
}
@end
@@ -25,7 +25,7 @@
WKWebViewConfiguration *configuration = [WKWebViewConfiguration new];
if (@available(iOS 10.0, *)) {
configuration.dataDetectorTypes = UIDataDetectorTypeLink;
configuration.dataDetectorTypes = WKDataDetectorTypeLink;
}
self.webView = [[WKWebView alloc] initWithFrame:CGRectZero configuration:configuration];
@@ -38,9 +38,26 @@
self = [self initWithNibName:nil bundle:nil];
if (self) {
self.originalText = text;
NSString *htmlString = [NSString stringWithFormat:@"<head><meta name='viewport' content='initial-scale=1.0'></head><body><pre>%@</pre></body>", [FLEXUtility stringByEscapingHTMLEntitiesInString:text]];
[self.webView loadHTMLString:htmlString baseURL:nil];
NSString *html = @"<head><style>:root{ color-scheme: light dark; }</style>"
"<meta name='viewport' content='initial-scale=1.0'></head><body><pre>%@</pre></body>";
// Loading message for when input text takes a long time to escape
NSString *loadingMessage = [NSString stringWithFormat:html, @"Loading..."];
[self.webView loadHTMLString:loadingMessage baseURL:nil];
// Escape HTML on a background thread
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSString *escapedText = [FLEXUtility stringByEscapingHTMLEntitiesInString:text];
NSString *htmlString = [NSString stringWithFormat:html, escapedText];
// Update webview on the main thread
dispatch_async(dispatch_get_main_queue(), ^{
[self.webView loadHTMLString:htmlString baseURL:nil];
});
});
}
return self;
}
@@ -50,14 +67,8 @@
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[self.webView loadRequest:request];
}
return self;
}
- (void)dealloc {
// WKWebView's delegate is assigned so we need to clear it manually.
if (_webView.navigationDelegate == self) {
_webView.navigationDelegate = nil;
}
return self;
}
- (void)viewDidLoad {
@@ -68,7 +79,9 @@
self.webView.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
if (self.originalText.length > 0) {
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"Copy" style:UIBarButtonItemStylePlain target:self action:@selector(copyButtonTapped:)];
self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc]
initWithTitle:@"Copy" style:UIBarButtonItemStylePlain target:self action:@selector(copyButtonTapped:)
];
}
}
@@ -79,20 +92,23 @@
#pragma mark - WKWebView Delegate
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction
decisionHandler:(void (^)(WKNavigationActionPolicy))handler {
WKNavigationActionPolicy policy = WKNavigationActionPolicyCancel;
if (navigationAction.navigationType == WKNavigationTypeOther) {
// Allow the initial load
policy = WKNavigationActionPolicyAllow;
} else {
// For clicked links, push another web view controller onto the navigation stack so that hitting the back button works as expected.
// 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 to handle the navigation.
NSURLRequest *request = navigationAction.request;
FLEXWebViewController *webVC = [[[self class] alloc] initWithURL:[request URL]];
webVC.title = [[request URL] absoluteString];
FLEXWebViewController *webVC = [[[self class] alloc] initWithURL:request.URL];
webVC.title = request.URL.absoluteString;
[self.navigationController pushViewController:webVC animated:YES];
}
decisionHandler(policy);
handler(policy);
}
@@ -101,7 +117,7 @@
+ (BOOL)supportsPathExtension:(NSString *)extension {
BOOL supported = NO;
NSSet<NSString *> *supportedExtensions = [self webViewSupportedPathExtensions];
if ([supportedExtensions containsObject:[extension lowercaseString]]) {
if ([supportedExtensions containsObject:extension.lowercaseString]) {
supported = YES;
}
return supported;
@@ -113,11 +129,14 @@
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/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"]];
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 pathExtensions;
}
@@ -54,9 +54,8 @@ typedef NS_ENUM(NSUInteger, FLEXFileBrowserSortAttribute) {
self.title = [path lastPathComponent];
self.operationQueue = [NSOperationQueue new];
//computing path size
FLEXFileBrowserController *__weak weakSelf = self;
// Compute path size
weakify(self)
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSFileManager *fileManager = NSFileManager.defaultManager;
NSDictionary<NSString *, id> *attributes = [fileManager attributesOfItemAtPath:path error:NULL];
@@ -66,16 +65,15 @@ typedef NS_ENUM(NSUInteger, FLEXFileBrowserSortAttribute) {
attributes = [fileManager attributesOfItemAtPath:[path stringByAppendingPathComponent:fileName] error:NULL];
totalSize += [attributes fileSize];
// Bail if the interested view controller has gone away.
if (!weakSelf) {
// Bail if the interested view controller has gone away
if (!self) {
return;
}
}
dispatch_async(dispatch_get_main_queue(), ^{
FLEXFileBrowserController *__strong strongSelf = weakSelf;
strongSelf.recursiveSize = @(totalSize);
[strongSelf.tableView reloadData];
dispatch_async(dispatch_get_main_queue(), ^{ strongify(self)
self.recursiveSize = @(totalSize);
[self.tableView reloadData];
});
});
@@ -358,44 +356,43 @@ typedef NS_ENUM(NSUInteger, FLEXFileBrowserSortAttribute) {
// 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]];
}];
- (UIContextMenuConfiguration *)tableView:(UITableView *)tableView
contextMenuConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath
point:(CGPoint)point __IOS_AVAILABLE(13.0) {
weakify(self)
return [UIContextMenuConfiguration configurationWithIdentifier:nil previewProvider:nil
actionProvider:^UIMenu *(NSArray<UIMenuElement *> *suggestedActions) {
UITableViewCell * const cell = [tableView cellForRowAtIndexPath:indexPath];
UIAction *rename = [UIAction actionWithTitle:@"Rename" image:nil identifier:@"Rename"
handler:^(UIAction *action) { strongify(self)
[self fileBrowserRename:cell];
}
];
UIAction *delete = [UIAction actionWithTitle:@"Delete" image:nil identifier:@"Delete"
handler:^(UIAction *action) { strongify(self)
[self fileBrowserDelete:cell];
}
];
UIAction *copyPath = [UIAction actionWithTitle:@"Copy Path" image:nil identifier:@"Copy Path"
handler:^(UIAction *action) { strongify(self)
[self fileBrowserCopyPath:cell];
}
];
UIAction *share = [UIAction actionWithTitle:@"Share" image:nil identifier:@"Share"
handler:^(UIAction *action) { strongify(self)
[self 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];
@@ -473,6 +470,9 @@ typedef NS_ENUM(NSUInteger, FLEXFileBrowserSortAttribute) {
} else {
// Share sheet for files
UIActivityViewController *shareSheet = [[UIActivityViewController alloc] initWithActivityItems:@[filePath] applicationActivities:nil];
if (UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad) {
shareSheet.popoverPresentationController.sourceView = sender;
}
[self presentViewController:shareSheet animated:true completion:nil];
}
}
@@ -34,6 +34,9 @@ extern NSString *const kFLEXKeychainClassKey;
/// Item description.
extern NSString *const kFLEXKeychainDescriptionKey;
/// Item group.
extern NSString *const kFLEXKeychainGroupKey;
/// Item label.
extern NSString *const kFLEXKeychainLabelKey;
@@ -15,6 +15,7 @@ NSString * const kFLEXKeychainAccountKey = @"acct";
NSString * const kFLEXKeychainCreatedAtKey = @"cdat";
NSString * const kFLEXKeychainClassKey = @"labl";
NSString * const kFLEXKeychainDescriptionKey = @"desc";
NSString * const kFLEXKeychainGroupKey = @"agrp";
NSString * const kFLEXKeychainLabelKey = @"labl";
NSString * const kFLEXKeychainLastModifiedKey = @"mdat";
NSString * const kFLEXKeychainWhereKey = @"svce";
@@ -170,7 +170,7 @@
- (NSString *)password {
if (self.passwordData.length) {
return [NSString stringWithCString:self.passwordData.bytes encoding:NSUTF8StringEncoding];
return [[NSString alloc] initWithData:self.passwordData encoding:NSUTF8StringEncoding];
}
return nil;
@@ -30,10 +30,10 @@
- (void)viewDidLoad {
[super viewDidLoad];
self.navigationItem.rightBarButtonItems = @[
[UIBarButtonItem flex_systemItem:UIBarButtonSystemItemTrash target:self action:@selector(trashPressed:)],
[UIBarButtonItem flex_systemItem:UIBarButtonSystemItemAdd target:self action:@selector(addPressed)],
];
[self addToolbarItems:@[
FLEXBarButtonItemSystem(Add, self, @selector(addPressed)),
[FLEXBarButtonItemSystem(Trash, self, @selector(trashPressed:)) flex_withTintColor:UIColor.redColor],
]];
[self reloadData];
}
@@ -43,14 +43,15 @@
cellConfiguration:^(__kindof FLEXTableViewCell *cell, NSDictionary *item, NSInteger row) {
cell.accessoryType = UITableViewCellAccessoryDisclosureIndicator;
id account = item[kFLEXKeychainAccountKey];
if ([account isKindOfClass:[NSString class]]) {
cell.textLabel.text = account;
id service = item[kFLEXKeychainWhereKey];
if ([service isKindOfClass:[NSString class]]) {
cell.textLabel.text = service;
cell.detailTextLabel.text = [item[kFLEXKeychainAccountKey] description];
} else {
cell.textLabel.text = [NSString stringWithFormat:
@"[%@]\n\n%@",
NSStringFromClass([account class]),
[account description]
NSStringFromClass([service class]),
[service description]
];
}
} filterMatcher:^BOOL(NSString *filterText, NSDictionary *item) {
@@ -98,8 +99,9 @@
NSDictionary *item = self.section.filteredList[idx];
FLEXKeychainQuery *query = [FLEXKeychainQuery new];
query.service = item[kFLEXKeychainWhereKey];
query.account = item[kFLEXKeychainAccountKey];
query.service = [item[kFLEXKeychainWhereKey] description];
query.account = [item[kFLEXKeychainAccountKey] description];
query.accessGroup = [item[kFLEXKeychainGroupKey] description];
[query fetch:nil];
return query;
@@ -129,6 +131,18 @@
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) {
[self confirmClearKeychain];
});
make.button(@"Cancel").cancelStyle();
} showFrom:self source:sender];
}
- (void)confirmClearKeychain {
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(@"ARE YOU SURE?");
make.message(@"This action CANNOT BE UNDONE.\nAre you sure you want to continue?\n");
make.message(@"If you're sure, scroll to confirm.");
make.button(@"Yes, clear the keychain").destructiveStyle().handler(^(NSArray *strings) {
for (id account in self.section.list) {
[self deleteItem:account];
@@ -136,8 +150,12 @@
[self reloadData];
});
make.button(@"Cancel"); make.button(@"Cancel"); make.button(@"Cancel"); make.button(@"Cancel");
make.button(@"Cancel"); make.button(@"Cancel"); make.button(@"Cancel"); make.button(@"Cancel");
make.button(@"Cancel"); make.button(@"Cancel"); make.button(@"Cancel"); make.button(@"Cancel");
make.button(@"Cancel"); make.button(@"Cancel"); make.button(@"Cancel"); make.button(@"Cancel");
make.button(@"Cancel").cancelStyle();
} showFrom:self source:sender];
} showFrom:self];
}
- (void)addPressed {
@@ -215,6 +233,7 @@
make.message(@"Service: ").message(query.service);
make.message(@"\nAccount: ").message(query.account);
make.message(@"\nPassword: ").message(query.password);
make.message(@"\nGroup: ").message(query.accessGroup);
make.button(@"Copy Service").handler(^(NSArray<NSString *> *strings) {
[UIPasteboard.generalPasteboard flex_copy:query.service];
@@ -195,7 +195,7 @@ static inline NSString * TBWildcardMap(NSString *token, NSString *candidate, TBW
"/System/Library/PrivateFrameworks/WebKitLegacy.framework/WebKitLegacy",
RTLD_LAZY
);
void (*WebKitInitialize)() = dlsym(handle, "WebKitInitialize");
void (*WebKitInitialize)(void) = dlsym(handle, "WebKitInitialize");
if (WebKitInitialize) {
NSAssert(NSThread.isMainThread,
@"WebKitInitialize can only be called on the main thread"
@@ -303,14 +303,14 @@ static inline NSString * TBWildcardMap(NSString *token, NSString *candidate, TBW
if (options == TBWildcardOptionsAny) {
return [[bundles flex_flatmapped:^NSArray *(NSString *bundlePath, NSUInteger idx) {
return [self classNamesInImageAtPath:bundlePath];
}] sortedUsingSelector:@selector(caseInsensitiveCompare:)];
}] flex_sortedUsingSelector:@selector(caseInsensitiveCompare:)];
}
return [[bundles flex_flatmapped:^NSArray *(NSString *bundlePath, NSUInteger idx) {
return [[self classNamesInImageAtPath:bundlePath] flex_mapped:^id(NSString *className, NSUInteger idx) {
return TBWildcardMap(query, className, options);
}];
}] sortedUsingSelector:@selector(caseInsensitiveCompare:)];
}] flex_sortedUsingSelector:@selector(caseInsensitiveCompare:)];
}
}
@@ -51,7 +51,7 @@
[self sizeToFit];
if (@available(iOS 13, *)) {
self.appearance = UIKeyboardTypeDefault;
self.appearance = UIKeyboardAppearanceDefault;
} else {
self.appearance = UIKeyboardAppearanceLight;
}
@@ -84,7 +84,6 @@
switch (_appearance) {
default:
case UIKeyboardAppearanceDefault:
#if FLEX_AT_LEAST_IOS13_SDK
if (@available(iOS 13, *)) {
titleColor = UIColor.labelColor;
@@ -97,7 +96,6 @@
}
break;
}
#endif
case UIKeyboardAppearanceLight:
titleColor = UIColor.blackColor;
backgroundColor = lightColor;
@@ -61,6 +61,7 @@
searchBar.keyboardType = UIKeyboardTypeWebSearch;
searchBar.autocorrectionType = UITextAutocorrectionTypeNo;
if (@available(iOS 11, *)) {
searchBar.smartQuotesType = UITextSmartQuotesTypeNo;
searchBar.smartInsertDeleteType = UITextSmartInsertDeleteTypeNo;
}
@@ -101,7 +102,7 @@
// Change "Bundle.fooba" to "Bundle.foobar."
NSString *orig = self.delegate.searchController.searchBar.text;
NSString *keyPath = [orig stringByReplacingLastKeyPathComponent:text];
NSString *keyPath = [orig flex_stringByReplacingLastKeyPathComponent:text];
self.delegate.searchController.searchBar.text = keyPath;
self.keyPath = [FLEXRuntimeKeyPathTokenizer tokenizeString:keyPath];
@@ -130,7 +131,7 @@
// Available since at least iOS 9, still present in iOS 13
UITextField *field = [searchBar valueForKey:@"_searchBarTextField"];
if ([self searchBar:searchBar shouldChangeTextInRange:field.selectedRange replacementText:text]) {
if ([self searchBar:searchBar shouldChangeTextInRange:field.flex_selectedRange replacementText:text]) {
[field replaceRange:field.selectedTextRange withText:text];
}
}
@@ -266,7 +267,7 @@
self.filteredClasses = nil;
}
self.timer = [NSTimer fireSecondsFromNow:0.15 block:^{
self.timer = [NSTimer flex_fireSecondsFromNow:0.15 block:^{
[self updateTable];
}];
}
@@ -40,7 +40,7 @@
self.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight;
if (@available(iOS 13, *)) {
self.appearance = UIKeyboardTypeDefault;
self.appearance = UIKeyboardAppearanceDefault;
} else {
self.appearance = UIKeyboardAppearanceLight;
}
@@ -106,7 +106,6 @@
switch (_appearance) {
case UIKeyboardAppearanceDefault:
#if FLEX_AT_LEAST_IOS13_SDK
if (@available(iOS 13, *)) {
borderColor = UIColor.systemBackgroundColor;
@@ -119,7 +118,6 @@
}
break;
}
#endif
case UIKeyboardAppearanceLight: {
borderColor = UIColor.clearColor;
backgroundColor = lightColor;
@@ -10,9 +10,12 @@
#import "FLEXKeyPathSearchController.h"
#import "FLEXRuntimeBrowserToolbar.h"
#import "UIGestureRecognizer+Blocks.h"
#import "UIBarButtonItem+FLEX.h"
#import "FLEXTableView.h"
#import "FLEXObjectExplorerFactory.h"
#import "FLEXAlert.h"
#import "FLEXRuntimeClient.h"
#import <dlfcn.h>
@interface FLEXObjcRuntimeViewController () <FLEXKeyPathSearchControllerDelegate>
@@ -28,9 +31,26 @@
- (void)viewDidLoad {
[super viewDidLoad];
// Long press on navigation bar to initialize webkit legacy
//
// We call initializeWebKitLegacy automatically before you search
// all bundles just to be safe (since touching some classes before
// WebKit is initialized will initialize it on a thread other than
// the main thread), but sometimes you can encounter this crash
// without searching through all bundles, of course.
[self.navigationController.navigationBar addGestureRecognizer:[
[UILongPressGestureRecognizer alloc]
initWithTarget:[FLEXRuntimeClient class]
action:@selector(initializeWebKitLegacy)
]
];
[self addToolbarItems:@[FLEXBarButtonItem(@"dlopen()", self, @selector(dlopenPressed:))]];
// Search bar stuff, must be first because this creates self.searchController
self.showsSearchBar = YES;
self.showSearchBarInitially = YES;
self.activatesSearchBarAutomatically = YES;
// Using pinSearchBar on this screen causes a weird visual
// thing on the next view controller that gets pushed.
//
@@ -57,13 +77,61 @@
[self.tableView deselectRowAtIndexPath:self.tableView.indexPathForSelectedRow animated:YES];
}
- (void)viewDidAppear:(BOOL)animated {
[super viewDidAppear:animated];
dispatch_async(dispatch_get_main_queue(), ^{
// This doesn't work unless it's wrapped in this dispatch_async call
[self.searchController.searchBar becomeFirstResponder];
});
#pragma mark dlopen
/// Prompt user for dlopen shortcuts to choose from
- (void)dlopenPressed:(id)sender {
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(@"Dynamically Open Library");
make.message(@"Invoke dlopen() with the given path. Choose an option below.");
make.button(@"System Framework").handler(^(NSArray<NSString *> *_) {
[self dlopenWithFormat:@"/System/Library/Frameworks/%@.framework/%@"];
});
make.button(@"System Private Framework").handler(^(NSArray<NSString *> *_) {
[self dlopenWithFormat:@"/System/Library/PrivateFrameworks/%@.framework/%@"];
});
make.button(@"Arbitrary Binary").handler(^(NSArray<NSString *> *_) {
[self dlopenWithFormat:nil];
});
make.button(@"Cancel").cancelStyle();
} showFrom:self];
}
/// Prompt user for input and dlopen
- (void)dlopenWithFormat:(NSString *)format {
[FLEXAlert makeAlert:^(FLEXAlert *make) {
make.title(@"Dynamically Open Library");
if (format) {
make.message(@"Pass in a framework name, such as CarKit or FrontBoard.");
} else {
make.message(@"Pass in an absolute path to a binary.");
}
make.textField(format ? @"ARKit" : @"/System/Library/Frameworks/ARKit.framework/ARKit");
make.button(@"Cancel").cancelStyle();
make.button(@"Open").destructiveStyle().handler(^(NSArray<NSString *> *strings) {
NSString *path = strings[0];
if (path.length < 2) {
[self dlopenInvalidPath];
} else if (format) {
path = [NSString stringWithFormat:format, path, path];
}
dlopen(path.UTF8String, RTLD_NOW);
});
} showFrom:self];
}
- (void)dlopenInvalidPath {
[FLEXAlert makeAlert:^(FLEXAlert * _Nonnull make) {
make.title(@"Path or Name Too Short");
make.button(@"Dismiss").cancelStyle();
} showFrom:self];
}
@@ -168,11 +168,10 @@ static uint8_t (*OSLogGetType)(void *);
NSDate *date = [NSDate dateWithTimeIntervalSince1970:log_message->tv_gmt.tv_sec];
// Get log message text
const char *messageText = OSLogCopyFormattedMessage(log_message);
// https://github.com/limneos/oslog/issues/1
if (entry->log_message.format && !(strcmp(entry->log_message.format, messageText))) {
messageText = (char *)entry->log_message.format;
}
// https://github.com/FLEXTool/FLEX/issues/564
const char *messageText = OSLogCopyFormattedMessage(log_message) ?: "";
// move messageText from stack to heap
NSString *msg = [NSString stringWithUTF8String:messageText];
@@ -98,12 +98,11 @@ static BOOL my_os_log_shim_enabled(void *addr) {
[super viewDidLoad];
self.showsSearchBar = YES;
self.showSearchBarInitially = NO;
self.pinSearchBar = YES;
__weak __typeof(self) weakSelf = self;
id logHandler = ^(NSArray<FLEXSystemLogMessage *> *newMessages) {
__strong __typeof(weakSelf) strongSelf = weakSelf;
[strongSelf handleUpdateWithNewMessages:newMessages];
weakify(self)
id logHandler = ^(NSArray<FLEXSystemLogMessage *> *newMessages) { strongify(self)
[self handleUpdateWithNewMessages:newMessages];
};
if (FLEXOSLogAvailable() && !FLEXNSLogHookWorks) {
@@ -137,20 +136,18 @@ static BOOL my_os_log_shim_enabled(void *addr) {
[self.logController startMonitoring];
}
- (NSArray<FLEXTableViewSection *> *)makeSections {
__weak __typeof(self) weakSelf = self;
- (NSArray<FLEXTableViewSection *> *)makeSections { weakify(self)
_logMessages = [FLEXMutableListSection list:@[]
cellConfiguration:^(FLEXSystemLogCell *cell, FLEXSystemLogMessage *message, NSInteger row) {
__strong __typeof(self) strongSelf = weakSelf;
if (strongSelf) {
cell.logMessage = message;
cell.highlightedText = strongSelf.filterText;
strongify(self)
cell.logMessage = message;
cell.highlightedText = self.filterText;
if (row % 2 == 0) {
cell.backgroundColor = FLEXColor.primaryBackgroundColor;
} else {
cell.backgroundColor = FLEXColor.secondaryBackgroundColor;
}
if (row % 2 == 0) {
cell.backgroundColor = FLEXColor.primaryBackgroundColor;
} else {
cell.backgroundColor = FLEXColor.secondaryBackgroundColor;
}
} filterMatcher:^BOOL(NSString *filterText, FLEXSystemLogMessage *message) {
NSString *displayedText = [FLEXSystemLogCell displayedTextForLogMessage:message];
@@ -178,9 +175,15 @@ static BOOL my_os_log_shim_enabled(void *addr) {
[self.logMessages mutate:^(NSMutableArray *list) {
[list addObjectsFromArray:newMessages];
}];
// Re-filter messages to filter against new messages
if (self.filterText.length) {
[self updateSearchResults:self.filterText];
}
// "Follow" the log as new messages stream in if we were previously near the bottom.
BOOL wasNearBottom = self.tableView.contentOffset.y >= self.tableView.contentSize.height - self.tableView.frame.size.height - 100.0;
UITableView *tv = self.tableView;
BOOL wasNearBottom = tv.contentOffset.y >= tv.contentSize.height - tv.frame.size.height - 100.0;
[self reloadData];
if (wasNearBottom) {
[self scrollToLastRow];
@@ -190,8 +193,8 @@ static BOOL my_os_log_shim_enabled(void *addr) {
- (void)scrollToLastRow {
NSInteger numberOfRows = [self.tableView numberOfRowsInSection:0];
if (numberOfRows > 0) {
NSIndexPath *lastIndexPath = [NSIndexPath indexPathForRow:numberOfRows - 1 inSection:0];
[self.tableView scrollToRowAtIndexPath:lastIndexPath atScrollPosition:UITableViewScrollPositionBottom animated:YES];
NSIndexPath *last = [NSIndexPath indexPathForRow:numberOfRows - 1 inSection:0];
[self.tableView scrollToRowAtIndexPath:last atScrollPosition:UITableViewScrollPositionBottom animated:YES];
}
}
@@ -265,8 +268,26 @@ static BOOL my_os_log_shim_enabled(void *addr) {
- (void)tableView:(UITableView *)tableView performAction:(SEL)action forRowAtIndexPath:(NSIndexPath *)indexPath withSender:(id)sender {
if (action == @selector(copy:)) {
// We usually only want to copy the log message itself, not any metadata associated with it.
UIPasteboard.generalPasteboard.string = self.logMessages.filteredList[indexPath.row].messageText;
UIPasteboard.generalPasteboard.string = self.logMessages.filteredList[indexPath.row].messageText ?: @"";
}
}
- (UIContextMenuConfiguration *)tableView:(UITableView *)tableView
contextMenuConfigurationForRowAtIndexPath:(NSIndexPath *)indexPath
point:(CGPoint)point __IOS_AVAILABLE(13.0) {
weakify(self)
return [UIContextMenuConfiguration configurationWithIdentifier:nil previewProvider:nil
actionProvider:^UIMenu *(NSArray<UIMenuElement *> *suggestedActions) {
UIAction *copy = [UIAction actionWithTitle:@"Copy"
image:nil
identifier:@"Copy"
handler:^(UIAction *action) { strongify(self)
// We usually only want to copy the log message itself, not any metadata associated with it.
UIPasteboard.generalPasteboard.string = self.logMessages.filteredList[indexPath.row].messageText ?: @"";
}];
return [UIMenu menuWithTitle:@"" image:nil identifier:nil options:UIMenuOptionsDisplayInline children:@[copy]];
}
];
}
@end
+1
View File
@@ -0,0 +1 @@
../../Classes/Utility/Categories/CALayer+FLEX.h
+1
View File
@@ -0,0 +1 @@
../../Classes/FLEX-Categories.h
+1
View File
@@ -0,0 +1 @@
../../Classes/FLEX-Core.h
+1
View File
@@ -0,0 +1 @@
../../Classes/FLEX-ObjectExploring.h
+1
View File
@@ -0,0 +1 @@
../../Classes/FLEX-Runtime.h
+1
View File
@@ -0,0 +1 @@
../../Classes/FLEX.h
+1
View File
@@ -0,0 +1 @@
../../Classes/Utility/FLEXAlert.h
+1
View File
@@ -0,0 +1 @@
../../Classes/Utility/Runtime/Objc/Reflection/FLEXBlockDescription.h
+1
View File
@@ -0,0 +1 @@
../../Classes/Utility/Runtime/Objc/Reflection/FLEXClassBuilder.h
+1
View File
@@ -0,0 +1 @@
../../Classes/Core/Views/Cells/FLEXCodeFontCell.h
+1
View File
@@ -0,0 +1 @@
../../Classes/ObjectExplorers/Sections/FLEXCollectionContentSection.h
+1
View File
@@ -0,0 +1 @@
../../Classes/ObjectExplorers/Sections/FLEXColorPreviewSection.h
+1
View File
@@ -0,0 +1 @@
../../Classes/ObjectExplorers/Sections/FLEXDefaultsContentSection.h
+1
View File
@@ -0,0 +1 @@
../../Classes/Toolbar/FLEXExplorerToolbar.h
+1
View File
@@ -0,0 +1 @@
../../Classes/Toolbar/FLEXExplorerToolbarItem.h
+1
View File
@@ -0,0 +1 @@
../../Classes/Core/Controllers/FLEXFilteringTableViewController.h
+1
View File
@@ -0,0 +1 @@
../../Classes/GlobalStateExplorers/Globals/FLEXGlobalsEntry.h
+1
View File
@@ -0,0 +1 @@
../../Classes/Utility/Runtime/Objc/Reflection/FLEXIvar.h
+1
View File
@@ -0,0 +1 @@
../../Classes/Core/Views/Cells/FLEXKeyValueTableViewCell.h
+1
View File
@@ -0,0 +1 @@
../../Classes/Utility/FLEXMacros.h
+1
View File
@@ -0,0 +1 @@
../../Classes/Manager/FLEXManager+Extensibility.h
+1
View File
@@ -0,0 +1 @@
../../Classes/Manager/FLEXManager+Networking.h
+1
View File
@@ -0,0 +1 @@
../../Classes/Manager/FLEXManager.h
+1
View File
@@ -0,0 +1 @@
../../Classes/ObjectExplorers/Sections/FLEXMetadataSection.h
+1
View File
@@ -0,0 +1 @@
../../Classes/Utility/Runtime/Objc/Reflection/FLEXMethod.h
+1
View File
@@ -0,0 +1 @@
../../Classes/Utility/Runtime/Objc/Reflection/FLEXMethodBase.h
+1
View File
@@ -0,0 +1 @@
../../Classes/Utility/Runtime/Objc/Reflection/FLEXMirror.h
+1
View File
@@ -0,0 +1 @@
../../Classes/Core/Views/Cells/FLEXMultilineTableViewCell.h
+1
View File
@@ -0,0 +1 @@
../../Classes/ObjectExplorers/Sections/FLEXMutableListSection.h
+1
View File
@@ -0,0 +1 @@
../../Classes/Core/Controllers/FLEXNavigationController.h
+1
View File
@@ -0,0 +1 @@
../../Classes/Utility/Runtime/Objc/FLEXObjcInternal.h
+1
View File
@@ -0,0 +1 @@
../../Classes/ObjectExplorers/FLEXObjectExplorer.h
+1
View File
@@ -0,0 +1 @@
../../Classes/ObjectExplorers/FLEXObjectExplorerFactory.h
+1
View File
@@ -0,0 +1 @@
../../Classes/ObjectExplorers/FLEXObjectExplorerViewController.h
+1
View File
@@ -0,0 +1 @@
../../Classes/ObjectExplorers/Sections/FLEXObjectInfoSection.h
+1
View File
@@ -0,0 +1 @@
../../Classes/Utility/Runtime/Objc/Reflection/FLEXProperty.h
+1
View File
@@ -0,0 +1 @@
../../Classes/Utility/Runtime/Objc/Reflection/FLEXPropertyAttributes.h
+1
View File
@@ -0,0 +1 @@
../../Classes/Utility/Runtime/Objc/Reflection/FLEXProtocol.h

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